Skip to content

Commit 16045bf

Browse files
authored
Allow preserving existing values during DBAL upsert (#2130)
* Allow preserving existing values during DBAL upsert * Cleanup prepare delete in all SQL dialects
1 parent 590b00d commit 16045bf

File tree

10 files changed

+208
-63
lines changed

10 files changed

+208
-63
lines changed

src/lib/doctrine-dbal-bulk/src/Flow/Doctrine/Bulk/Dialect/MySQLDialect.php

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,6 @@ public function prepareDelete(TableDefinition $table, BulkData $bulkData) : stri
2424
{
2525
$columns = $bulkData->columns()->all();
2626

27-
if (count($columns) === 1) {
28-
$column = $columns[0];
29-
30-
return \sprintf(
31-
'DELETE FROM %s WHERE %s IN (%s)',
32-
$table->name(),
33-
$this->platform->quoteIdentifier($column),
34-
$bulkData->toSqlPlaceholders()
35-
);
36-
}
37-
3827
return \sprintf(
3928
'DELETE FROM %s WHERE (%s) IN (%s)',
4029
$table->name(),
@@ -78,7 +67,7 @@ public function prepareInsert(TableDefinition $table, BulkData $bulkData, ?Inser
7867
\implode(',', \array_map(fn (string $column) : string => $this->platform->quoteIdentifier($column), $bulkData->columns()->all())),
7968
$bulkData->toSqlPlaceholders(),
8069
\count($options->updateColumns)
81-
? $this->updateSelectedColumns($options->updateColumns, $bulkData->columns())
70+
? $this->updateSelectedColumns($options->updateColumns, $bulkData->columns(), $table->name(), $options->preserveExistingValues)
8271
: $this->updateAllColumns($bulkData->columns())
8372
);
8473
}
@@ -129,10 +118,18 @@ private function updateAllColumns(Columns $columns) : string
129118
*
130119
* @return string
131120
*/
132-
private function updateSelectedColumns(array $updateColumns, Columns $columns) : string
121+
private function updateSelectedColumns(array $updateColumns, Columns $columns, string $tableName, ?bool $preserveExistingValues = null) : string
133122
{
134123
return \count($updateColumns)
135-
? \implode(',', \array_map(fn (string $column) : string => "{$this->platform->quoteIdentifier($column)} = VALUES({$this->platform->quoteIdentifier($column)})", $updateColumns))
124+
? \implode(',', \array_map(function (string $column) use ($tableName, $preserveExistingValues) : string {
125+
$clause = "{$this->platform->quoteIdentifier($column)} = ";
126+
127+
if (true === $preserveExistingValues) {
128+
return $clause . "COALESCE(VALUES({$this->platform->quoteIdentifier($column)}), {$tableName}.{$this->platform->quoteIdentifier($column)})";
129+
}
130+
131+
return $clause . "VALUES({$this->platform->quoteIdentifier($column)})";
132+
}, $updateColumns))
136133
: $this->updateAllColumns($columns);
137134
}
138135
}

src/lib/doctrine-dbal-bulk/src/Flow/Doctrine/Bulk/Dialect/MySQLInsertOptions.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public function __construct(
1818
public ?bool $skipConflicts = null,
1919
public ?bool $upsert = null,
2020
public array $updateColumns = [],
21+
public ?bool $preserveExistingValues = null,
2122
) {
2223
}
2324

@@ -29,13 +30,15 @@ public static function fromArray(array $options) : InsertOptions
2930
'skip_conflicts' => type_optional(type_boolean()),
3031
'upsert' => type_optional(type_boolean()),
3132
'update_columns' => type_list(type_string()),
33+
'preserve_existing_values' => type_optional(type_boolean()),
3234
]
3335
)->assert($options);
3436

3537
return new self(
3638
$options['skip_conflicts'] ?? null,
3739
$options['upsert'] ?? null,
3840
$options['update_columns'] ?? [],
41+
$options['preserve_existing_values'] ?? null,
3942
);
4043
}
4144

@@ -52,9 +55,9 @@ public function skipConflicts(bool $skip = true) : self
5255
/**
5356
* @param array<string> $updateColumns
5457
*/
55-
public function updateColumns(array $updateColumns) : self
58+
public function updateColumns(array $updateColumns, ?bool $preserveExistingValues = null) : self
5659
{
57-
return new self($this->skipConflicts, $this->upsert, $updateColumns);
60+
return new self($this->skipConflicts, $this->upsert, $updateColumns, $preserveExistingValues);
5861
}
5962

6063
public function upsert(bool $upsert = true) : self

src/lib/doctrine-dbal-bulk/src/Flow/Doctrine/Bulk/Dialect/PostgreSQLDialect.php

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,6 @@ public function prepareDelete(TableDefinition $table, BulkData $bulkData) : stri
2424
{
2525
$columns = $bulkData->columns()->all();
2626

27-
if (count($columns) === 1) {
28-
$column = $columns[0];
29-
30-
return \sprintf(
31-
'DELETE FROM %s WHERE %s IN (%s)',
32-
$table->name(),
33-
$this->platform->quoteIdentifier($column),
34-
$bulkData->toSqlPlaceholders()
35-
);
36-
}
37-
3827
return \sprintf(
3928
'DELETE FROM %s WHERE (%s) IN (%s)',
4029
$table->name(),
@@ -67,7 +56,7 @@ public function prepareInsert(TableDefinition $table, BulkData $bulkData, ?Inser
6756
$bulkData->toSqlPlaceholders(),
6857
\implode(',', $options->conflictColumns),
6958
\count($options->updateColumns)
70-
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns())
59+
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns(), $table->name(), $options->preserveExistingValues)
7160
: $this->updateAllColumns($bulkData->columns())
7261
);
7362
}
@@ -80,7 +69,7 @@ public function prepareInsert(TableDefinition $table, BulkData $bulkData, ?Inser
8069
$bulkData->toSqlPlaceholders(),
8170
$options->constraint,
8271
\count($options->updateColumns)
83-
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns())
72+
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns(), $table->name(), $options->preserveExistingValues)
8473
: $this->updateAllColumns($bulkData->columns())
8574
);
8675
}
@@ -132,19 +121,14 @@ public function prepareUpdate(TableDefinition $table, BulkData $bulkData, ?Updat
132121
'UPDATE %s as existing_table SET %s FROM (VALUES %s) as excluded (%s) WHERE %s',
133122
$table->name(),
134123
\count($options->updateColumns)
135-
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns()->without(...$options->primaryKeyColumns))
124+
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns()->without(...$options->primaryKeyColumns), $table->name(), $options->preserveExistingValues)
136125
: $this->updateAllColumns($bulkData->columns()->without(...$options->primaryKeyColumns)),
137126
$bulkData->toSqlCastedPlaceholders($table),
138127
\implode(',', \array_map(fn (string $column) : string => $this->platform->quoteIdentifier($column), $bulkData->columns()->all())),
139128
$this->updatedIndexColumns($options->primaryKeyColumns)
140129
);
141130
}
142131

143-
/**
144-
* @param Columns $columns
145-
*
146-
* @return string
147-
*/
148132
private function updateAllColumns(Columns $columns) : string
149133
{
150134
/**
@@ -162,8 +146,6 @@ private function updateAllColumns(Columns $columns) : string
162146

163147
/**
164148
* @param array<string> $updateColumns
165-
*
166-
* @return string
167149
*/
168150
private function updatedIndexColumns(array $updateColumns) : string
169151
{
@@ -172,19 +154,24 @@ private function updatedIndexColumns(array $updateColumns) : string
172154

173155
/**
174156
* @param array<string> $updateColumns
175-
* @param Columns $columns
176-
*
177-
* @return string
178157
*/
179-
private function updatedSelectedColumns(array $updateColumns, Columns $columns) : string
158+
private function updatedSelectedColumns(array $updateColumns, Columns $columns, string $tableName, ?bool $preserveExistingValues = null) : string
180159
{
181160
/**
182161
* https://www.postgresql.org/docs/9.5/sql-insert.html#SQL-ON-CONFLICT
183162
* The SET and WHERE clauses in ON CONFLICT DO UPDATE have access to the existing row using the
184163
* table's name (or an alias), and to rows proposed for insertion using the special EXCLUDED table.
185164
*/
186165
return \count($updateColumns)
187-
? \implode(',', \array_map(fn (string $column) : string => "{$this->platform->quoteIdentifier($column)} = {$this->platform->quoteIdentifier('excluded.' . $column)}", $updateColumns))
166+
? \implode(',', \array_map(function (string $column) use ($tableName, $preserveExistingValues) : string {
167+
$clause = "{$this->platform->quoteIdentifier($column)} = ";
168+
169+
if (true === $preserveExistingValues) {
170+
return $clause . "COALESCE({$this->platform->quoteIdentifier('excluded.' . $column)}, {$tableName}.{$this->platform->quoteIdentifier($column)})";
171+
}
172+
173+
return $clause . "{$this->platform->quoteIdentifier('excluded.' . $column)}";
174+
}, $updateColumns))
188175
: $this->updateAllColumns($columns);
189176
}
190177
}

src/lib/doctrine-dbal-bulk/src/Flow/Doctrine/Bulk/Dialect/PostgreSQLInsertOptions.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public function __construct(
1818
public ?string $constraint = null,
1919
public array $conflictColumns = [],
2020
public array $updateColumns = [],
21+
public ?bool $preserveExistingValues = null,
2122
) {
2223
}
2324

@@ -27,11 +28,13 @@ public function __construct(
2728
public static function fromArray(array $options) : InsertOptions
2829
{
2930
$options = type_structure(
30-
optional_elements: [
31+
[],
32+
[
3133
'skip_conflicts' => type_optional(type_boolean()),
3234
'constraint' => type_optional(type_string()),
3335
'conflict_columns' => type_list(type_string()),
3436
'update_columns' => type_list(type_string()),
37+
'preserve_existing_values' => type_optional(type_boolean()),
3538
]
3639
)->assert($options);
3740

@@ -40,6 +43,7 @@ public static function fromArray(array $options) : InsertOptions
4043
$options['constraint'] ?? null,
4144
$options['conflict_columns'] ?? [],
4245
$options['update_columns'] ?? [],
46+
$options['preserve_existing_values'] ?? null,
4347
);
4448
}
4549

@@ -69,8 +73,8 @@ public function skipConflicts(bool $skip = true) : self
6973
/**
7074
* @param array<string> $updateColumns
7175
*/
72-
public function updateColumns(array $updateColumns) : self
76+
public function updateColumns(array $updateColumns, ?bool $preserveExistingValues = null) : self
7377
{
74-
return new self($this->skipConflicts, $this->constraint, $this->conflictColumns, $updateColumns);
78+
return new self($this->skipConflicts, $this->constraint, $this->conflictColumns, $updateColumns, $preserveExistingValues);
7579
}
7680
}

src/lib/doctrine-dbal-bulk/src/Flow/Doctrine/Bulk/Dialect/PostgreSQLUpdateOptions.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Flow\Doctrine\Bulk\Dialect;
66

7-
use function Flow\Types\DSL\{type_list, type_string, type_structure};
7+
use function Flow\Types\DSL\{type_boolean, type_list, type_optional, type_string, type_structure};
88
use Flow\Doctrine\Bulk\UpdateOptions;
99

1010
final readonly class PostgreSQLUpdateOptions implements UpdateOptions
@@ -16,6 +16,7 @@
1616
public function __construct(
1717
public array $primaryKeyColumns = [],
1818
public array $updateColumns = [],
19+
public ?bool $preserveExistingValues = null,
1920
) {
2021
}
2122

@@ -28,12 +29,14 @@ public static function fromArray(array $options) : UpdateOptions
2829
optional_elements: [
2930
'primary_key_columns' => type_list(type_string()),
3031
'update_columns' => type_list(type_string()),
32+
'preserve_existing_values' => type_optional(type_boolean()),
3133
]
3234
)->assert($options);
3335

3436
return new self(
3537
$options['primary_key_columns'] ?? [],
3638
$options['update_columns'] ?? [],
39+
$options['preserve_existing_values'] ?? null,
3740
);
3841
}
3942

src/lib/doctrine-dbal-bulk/src/Flow/Doctrine/Bulk/Dialect/SqliteDialect.php

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,6 @@ public function prepareDelete(TableDefinition $table, BulkData $bulkData) : stri
2323
{
2424
$columns = $bulkData->columns()->all();
2525

26-
if (count($columns) === 1) {
27-
$column = $columns[0];
28-
29-
return \sprintf(
30-
'DELETE FROM %s WHERE %s IN (%s)',
31-
$table->name(),
32-
$this->platform->quoteIdentifier($column),
33-
$bulkData->toSqlPlaceholders()
34-
);
35-
}
36-
3726
return \sprintf(
3827
'DELETE FROM %s WHERE (%s) IN (%s)',
3928
$table->name(),
@@ -60,7 +49,7 @@ public function prepareInsert(TableDefinition $table, BulkData $bulkData, ?Inser
6049
$bulkData->toSqlPlaceholders(),
6150
\implode(',', $options->conflictColumns),
6251
\count($options->updateColumns)
63-
? $this->updateSelectedColumns($options->updateColumns, $bulkData->columns())
52+
? $this->updateSelectedColumns($options->updateColumns, $bulkData->columns(), $table->name(), $options->preserveExistingValues)
6453
: $this->updateAllColumns($bulkData->columns())
6554
);
6655
}
@@ -82,7 +71,7 @@ public function prepareInsert(TableDefinition $table, BulkData $bulkData, ?Inser
8271
);
8372
}
8473

85-
public function prepareUpdate(TableDefinition $table, BulkData $bulkData, ?UpdateOptions $updateOptions = null) : string
74+
public function prepareUpdate(TableDefinition $table, BulkData $bulkData, ?UpdateOptions $options = null) : string
8675
{
8776
return \sprintf(
8877
'REPLACE INTO %s (%s) VALUES %s',
@@ -105,10 +94,18 @@ private function updateAllColumns(Columns $columns) : string
10594
/**
10695
* @param array<string> $updateColumns
10796
*/
108-
private function updateSelectedColumns(array $updateColumns, Columns $columns) : string
97+
private function updateSelectedColumns(array $updateColumns, Columns $columns, string $tableName, ?bool $preserveExistingValues = null) : string
10998
{
11099
return [] !== $updateColumns
111-
? \implode(',', \array_map(fn (string $column) : string => "{$this->platform->quoteIdentifier($column)} = {$this->platform->quoteIdentifier('excluded.' . $column)}", $updateColumns))
100+
? \implode(',', \array_map(function (string $column) use ($tableName, $preserveExistingValues) : string {
101+
$clause = "{$this->platform->quoteIdentifier($column)} = ";
102+
103+
if (true === $preserveExistingValues) {
104+
return $clause . "COALESCE({$this->platform->quoteIdentifier('excluded.' . $column)}, {$tableName}.{$this->platform->quoteIdentifier($column)})";
105+
}
106+
107+
return $clause . "{$this->platform->quoteIdentifier('excluded.' . $column)}";
108+
}, $updateColumns))
112109
: $this->updateAllColumns($columns);
113110
}
114111
}

src/lib/doctrine-dbal-bulk/src/Flow/Doctrine/Bulk/Dialect/SqliteInsertOptions.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public function __construct(
1717
public ?bool $skipConflicts = null,
1818
public array $conflictColumns = [],
1919
public array $updateColumns = [],
20+
public ?bool $preserveExistingValues = null,
2021
) {
2122
}
2223

@@ -31,13 +32,15 @@ public static function fromArray(array $options) : InsertOptions
3132
'skip_conflicts' => type_optional(type_boolean()),
3233
'conflict_columns' => type_list(type_string()),
3334
'update_columns' => type_list(type_string()),
35+
'preserve_existing_values' => type_optional(type_boolean()),
3436
]
3537
)->assert($options);
3638

3739
return new self(
3840
$options['skip_conflicts'] ?? null,
3941
$options['conflict_columns'] ?? [],
4042
$options['update_columns'] ?? [],
43+
$options['preserve_existing_values'] ?? null,
4144
);
4245
}
4346

@@ -62,8 +65,8 @@ public function skipConflicts(bool $skip = true) : self
6265
/**
6366
* @param array<string> $updateColumns
6467
*/
65-
public function updateColumns(array $updateColumns) : self
68+
public function updateColumns(array $updateColumns, ?bool $preserveExistingValues = null) : self
6669
{
67-
return new self($this->skipConflicts, $this->conflictColumns, $updateColumns);
70+
return new self($this->skipConflicts, $this->conflictColumns, $updateColumns, $preserveExistingValues);
6871
}
6972
}

0 commit comments

Comments
 (0)