Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions spec/Builder/Binding.spec.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
$collection->add(true);
$collection->add(null);

expect($collection->count())->toBe(4);
expect($collection->getOrdered())->toBe(['value1', 123, true, null]);
// le null n'est pas pris en charge par le binding
expect($collection->count())->toBe(3);
expect($collection->getOrdered())->toBe(['value1', 123, true]);
});

it(": BindingCollection ajout nommé", function() {
Expand Down
116 changes: 113 additions & 3 deletions src/Builder/BaseBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,8 @@ public function set($key, $value = ''): static
foreach ($key as $k => $v) {
if ($v instanceof Expression) {
$this->values[$k] = $v;
} elseif ($v === null) {
$this->values[$k] = null;
} else {
$this->values[$k] = $v;
$this->bindings->add($v, 'values');
Expand Down Expand Up @@ -643,6 +645,107 @@ public function update(array|object $data = [])
});
}

/**
* Met à jour plusieurs enregistrements en une seule requête
*
* @param list<array|object> $data Tableau de données à mettre à jour, où chaque élément est un tableau associatif
* @param array|Expression|string $constraints Colonne utilisée pour identifier les enregistrements à mettre à jour (par défaut 'id')
* @param int $chunkSize Taille des lots pour le traitement
*
* @return int|string Nombre de lignes affectées ou la requête SQL en mode test
*
* @throws BadMethodCallException
* @throws InvalidArgumentException
*
* @example
* // Mise à jour simple
* $builder->bulkUpdate([
* ['id' => 1, 'name' => 'John', 'email' => 'john@example.com'],
* ['id' => 2, 'name' => 'Jane', 'email' => 'jane@example.com'],
* ]);
*
* // Mise à jour avec colonne personnalisée
* $builder->bulkUpdate([
* ['code' => 'ABC', 'price' => 100],
* ['code' => 'DEF', 'price' => 150],
* ], 'code');
*/
public function bulkUpdate(array $data, string $column = 'id', int $chunkSize = 100): int|string
{
if ($data === []) {
return 0;
}

// Vérifier la structure des données
$firstRow = reset($data);
if (!is_array($firstRow)) {
throw new InvalidArgumentException('Each row must be an associative array.');
}

// Vérifier que la colonne d'identification existe dans chaque ligne
foreach ($data as $index => $row) {
if (!array_key_exists($column, $row)) {
throw new InvalidArgumentException(
"Column '{$column}' not found in row at index {$index}. Each row must contain the identifier column."
);
}
}

// Extraire les colonnes à mettre à jour (toutes sauf la colonne d'identification)
$updateColumns = array_diff(array_keys($firstRow), [$column]);

if (empty($updateColumns)) {
return 0; // Rien à mettre à jour
}

// Traitement par lots
$totalAffected = 0;
$chunks = array_chunk($data, $chunkSize);
$allSql = [];

$callback = function() use ($chunks, $column, $updateColumns, &$totalAffected, &$allSql) {
foreach ($chunks as $chunk) {
$sql = $this->compiler->compileBulkUpdate($this, $chunk, $column, $updateColumns);

if ($this->testMode) {
$allSql[] = $sql;
} else {
$bindings = $this->buildBulkUpdateBindings($chunk, $column, $updateColumns);
$totalAffected += $this->db->affectingStatement($sql, $bindings);
}
}

return [$allSql, $totalAffected];
};

[$allSql, $totalAffected] = $this->db->transaction($callback);

return $this->testMode ? implode('; ', $allSql) : $totalAffected;
}

/**
* Construit les bindings pour une requête bulk update
*/
protected function buildBulkUpdateBindings(array $chunk, string $column, array $updateColumns): array
{
$bindings = [];

foreach ($updateColumns as $updateColumn) {
foreach ($chunk as $row) {
$value = $row[$updateColumn];
if (!($value instanceof Expression) && $value !== null) {
$bindings[] = $value;
}
$bindings[] = $row[$column];
}
}

$ids = array_column($chunk, $column);
$bindings = array_merge($bindings, $ids);

return $bindings;
}

/**
* Exécute une requête de remplacement.
*
Expand Down Expand Up @@ -1134,9 +1237,16 @@ public function getBindings(): array
default => [], // Fallback à tous
};

return $types === null
? []
: $this->db->prepareBindings($this->bindings->getOrdered($types));
if($types === null) {
return [];
}

$bindings = $this->bindings->getOrdered($types);
$bindings = array_filter($bindings, function($binding) {
return $binding !== '__NULL__' && !($binding instanceof Expression);
});

return $this->db->prepareBindings(array_values($bindings));
}

/**
Expand Down
57 changes: 41 additions & 16 deletions src/Builder/BindingCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ public function add(mixed $value, string $type = 'where', ?int $pdoType = null):
throw new InvalidArgumentException("Type de binding invalide: {$type}");
}

if ($value === null) {
$this->bindings[$type][] = '__NULL__';
$this->types[$type][] = PDO::PARAM_NULL;

return $this;
}

if ($value instanceof Expression) {
$this->bindings[$type][] = $value;
$this->types[$type][] = null;

return $this;
}

$this->bindings[$type][] = $value;
$this->types[$type][] = $pdoType ?? $this->guessType($value);

Expand Down Expand Up @@ -134,34 +148,45 @@ public function getOrdered(array $contexts = []): array

foreach ($contexts as $context) {
if (! empty($this->bindings[$context])) {
array_push($result, ...$this->bindings[$context]);
foreach ($this->bindings[$context] as $binding) {
if ($binding === '__NULL__' || $binding instanceof Expression) {
continue;
}
$result[] = $binding;
}
}
}

return $result;
}

/**
* Récupère tous les types dans l'ordre
*
* @param list<string> $types
* Récupère les types PDO dans l'ordre
*
* @param list<string> $contexts
*
* @return list<int>
*/
public function getTypesOrdered(array $types = []): array
public function getTypesOrdered(array $contexts = []): array
{
if ($types === []) {
$types = self::TYPES;
if ($contexts === []) {
$contexts = self::TYPES;
}

$result = [];

foreach ($types as $type) {
if (! empty($this->types[$type])) {
array_push($result, ...$this->types[$type]);
foreach ($contexts as $context) {
if (!empty($this->types[$context])) {
foreach ($this->types[$context] as $index => $type) {
$binding = $this->bindings[$context][$index] ?? null;
if ($binding === '__NULL__') {
$result[] = PDO::PARAM_NULL;
} elseif ($type !== null) {
$result[] = $type;
}
}
}
}

return $result;
}

Expand All @@ -178,11 +203,11 @@ public function has(string $context): bool
*/
public function count(?string $context = null): int
{
if ($context !== null) {
return count($this->bindings[$context] ?? []);
}
$context = $context === null ? [] : [$context];

$bindings = $this->getOrdered($context);

return array_sum(array_map('count', $this->bindings));
return count($bindings);
}

/**
Expand Down
78 changes: 75 additions & 3 deletions src/Builder/Compilers/QueryCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,52 @@ public function compileUpdate(BaseBuilder $builder): string
return $this->compileUpdateStandard($builder);
}

/**
* Compile une requête de mise à jour en masse
*
* @param array $chunk Données du lot
* @param string $column Colonne d'identification
* @param array $updateColumns Colonnes à mettre à jour
*/
public function compileBulkUpdate(BaseBuilder $builder, array $chunk, string $column, array $updateColumns): string
{
$table = $this->db->escapeIdentifiers($builder->getTable());
$columnEscaped = $this->db->escapeIdentifiers($column);

// Construction du CASE WHEN pour chaque colonne à mettre à jour
$updateParts = [];
foreach ($updateColumns as $updateColumn) {
$caseStatement = $this->buildCaseStatement($chunk, $updateColumn, $column);
$updateParts[] = $this->db->escapeIdentifiers($updateColumn) . ' = ' . $caseStatement;
}

// Construction de la clause WHERE IN
$ids = array_column($chunk, $column);
$placeholders = implode(', ', array_fill(0, count($ids), '?'));

return "UPDATE {$table} SET " . implode(', ', $updateParts) . " WHERE {$columnEscaped} IN ({$placeholders})";
}

/**
* Construit une clause CASE WHEN pour une colonne spécifique
*
* @param array $chunk Données du lot
* @param string $updateColumn Colonne à mettre à jour
* @param string $column Colonne d'identification
*/
protected function buildCaseStatement(array $chunk, string $updateColumn, string $column): string
{
$cases = [];
$column = $this->db->escapeIdentifiers($column);

foreach ($chunk as $row) {
$value = $this->wrapValue($row[$updateColumn]);
$cases[] = "WHEN {$column} = ? THEN {$value}";
}

return "CASE " . implode(' ', $cases) . " END";
}

/**
* Compilation standard sans jointure
*/
Expand Down Expand Up @@ -434,16 +480,38 @@ protected function compileWhere(array $where): string
$column = $this->db->escapeIdentifiers($where['column']);
$operator = $this->translateOperator($where['operator']);

if (isset($where['value']) && $where['value'] === null) {
return "{$column} IS NULL";
}
if (isset($where['value']) && $where['value'] instanceof Expression) {
return "{$column} {$operator} {$where['value']}";
}

return "{$column} {$operator} ?";

case 'in':
$column = $this->db->escapeIdentifiers($where['column']);
$placeholders = implode(', ', array_fill(0, count($where['values']), '?'));

$column = $this->db->escapeIdentifiers($where['column']);
$hasNull = false;
$values = [];

foreach ($where['values'] as $value) {
if ($value === null) {
$hasNull = true;
} else {
$values[] = $value;
}
}

if ($values === [] && $hasNull) {
return "{$column} IS NULL";
}

if ($hasNull) {
$placeholders = implode(', ', array_fill(0, count($values), '?'));
return "({$column} IN ({$placeholders}) OR {$column} IS NULL)";
}

$placeholders = implode(', ', array_fill(0, count($values), '?'));
return "{$column} {$where['operator']} ({$placeholders})";

case 'insub':
Expand Down Expand Up @@ -634,6 +702,10 @@ protected function wrapValue($value): string
if ($value instanceof Expression) {
return (string) $value;
}

if ($value === null) {
return 'NULL';
}

return '?';
}
Expand Down
5 changes: 5 additions & 0 deletions src/Query/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ public function columnData(): array
*/
public function get(int|string $type = PDO::FETCH_OBJ): array
{
$type = match($type) {
'array' => PDO::FETCH_ASSOC,
'object' => PDO::FETCH_OBJ,
default => $type,
};
$data = is_string($type) ? $this->resultClass($type) : $this->result($type);

$this->details['num_rows'] = count($data);
Expand Down