Skip to content

Commit bc3f342

Browse files
committed
Allow preserving existing values during DBAL upsert
1 parent 12eda07 commit bc3f342

File tree

10 files changed

+208
-30
lines changed

10 files changed

+208
-30
lines changed

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function prepareInsert(TableDefinition $table, BulkData $bulkData, ?Inser
7878
\implode(',', \array_map(fn (string $column) : string => $this->platform->quoteIdentifier($column), $bulkData->columns()->all())),
7979
$bulkData->toSqlPlaceholders(),
8080
\count($options->updateColumns)
81-
? $this->updateSelectedColumns($options->updateColumns, $bulkData->columns())
81+
? $this->updateSelectedColumns($options->updateColumns, $bulkData->columns(), $table->name(), $options->preserveExistingValues)
8282
: $this->updateAllColumns($bulkData->columns())
8383
);
8484
}
@@ -129,10 +129,18 @@ private function updateAllColumns(Columns $columns) : string
129129
*
130130
* @return string
131131
*/
132-
private function updateSelectedColumns(array $updateColumns, Columns $columns) : string
132+
private function updateSelectedColumns(array $updateColumns, Columns $columns, string $tableName, ?bool $preserveExistingValues = null) : string
133133
{
134134
return \count($updateColumns)
135-
? \implode(',', \array_map(fn (string $column) : string => "{$this->platform->quoteIdentifier($column)} = VALUES({$this->platform->quoteIdentifier($column)})", $updateColumns))
135+
? \implode(',', \array_map(function (string $column) use ($tableName, $preserveExistingValues) : string {
136+
$clause = "{$this->platform->quoteIdentifier($column)} = ";
137+
138+
if (true === $preserveExistingValues) {
139+
return $clause . "COALESCE(VALUES({$this->platform->quoteIdentifier($column)}), {$tableName}.{$this->platform->quoteIdentifier($column)})";
140+
}
141+
142+
return $clause . "VALUES({$this->platform->quoteIdentifier($column)})";
143+
}, $updateColumns))
136144
: $this->updateAllColumns($columns);
137145
}
138146
}

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 & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function prepareInsert(TableDefinition $table, BulkData $bulkData, ?Inser
6767
$bulkData->toSqlPlaceholders(),
6868
\implode(',', $options->conflictColumns),
6969
\count($options->updateColumns)
70-
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns())
70+
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns(), $table->name(), $options->preserveExistingValues)
7171
: $this->updateAllColumns($bulkData->columns())
7272
);
7373
}
@@ -80,7 +80,7 @@ public function prepareInsert(TableDefinition $table, BulkData $bulkData, ?Inser
8080
$bulkData->toSqlPlaceholders(),
8181
$options->constraint,
8282
\count($options->updateColumns)
83-
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns())
83+
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns(), $table->name(), $options->preserveExistingValues)
8484
: $this->updateAllColumns($bulkData->columns())
8585
);
8686
}
@@ -132,19 +132,14 @@ public function prepareUpdate(TableDefinition $table, BulkData $bulkData, ?Updat
132132
'UPDATE %s as existing_table SET %s FROM (VALUES %s) as excluded (%s) WHERE %s',
133133
$table->name(),
134134
\count($options->updateColumns)
135-
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns()->without(...$options->primaryKeyColumns))
135+
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns()->without(...$options->primaryKeyColumns), $table->name(), $options->preserveExistingValues)
136136
: $this->updateAllColumns($bulkData->columns()->without(...$options->primaryKeyColumns)),
137137
$bulkData->toSqlCastedPlaceholders($table),
138138
\implode(',', \array_map(fn (string $column) : string => $this->platform->quoteIdentifier($column), $bulkData->columns()->all())),
139139
$this->updatedIndexColumns($options->primaryKeyColumns)
140140
);
141141
}
142142

143-
/**
144-
* @param Columns $columns
145-
*
146-
* @return string
147-
*/
148143
private function updateAllColumns(Columns $columns) : string
149144
{
150145
/**
@@ -162,8 +157,6 @@ private function updateAllColumns(Columns $columns) : string
162157

163158
/**
164159
* @param array<string> $updateColumns
165-
*
166-
* @return string
167160
*/
168161
private function updatedIndexColumns(array $updateColumns) : string
169162
{
@@ -172,19 +165,24 @@ private function updatedIndexColumns(array $updateColumns) : string
172165

173166
/**
174167
* @param array<string> $updateColumns
175-
* @param Columns $columns
176-
*
177-
* @return string
178168
*/
179-
private function updatedSelectedColumns(array $updateColumns, Columns $columns) : string
169+
private function updatedSelectedColumns(array $updateColumns, Columns $columns, string $tableName, ?bool $preserveExistingValues = null) : string
180170
{
181171
/**
182172
* https://www.postgresql.org/docs/9.5/sql-insert.html#SQL-ON-CONFLICT
183173
* The SET and WHERE clauses in ON CONFLICT DO UPDATE have access to the existing row using the
184174
* table's name (or an alias), and to rows proposed for insertion using the special EXCLUDED table.
185175
*/
186176
return \count($updateColumns)
187-
? \implode(',', \array_map(fn (string $column) : string => "{$this->platform->quoteIdentifier($column)} = {$this->platform->quoteIdentifier('excluded.' . $column)}", $updateColumns))
177+
? \implode(',', \array_map(function (string $column) use ($tableName, $preserveExistingValues) : string {
178+
$clause = "{$this->platform->quoteIdentifier($column)} = ";
179+
180+
if (true === $preserveExistingValues) {
181+
return $clause . "COALESCE({$this->platform->quoteIdentifier('excluded.' . $column)}, {$tableName}.{$this->platform->quoteIdentifier($column)})";
182+
}
183+
184+
return $clause . "{$this->platform->quoteIdentifier('excluded.' . $column)}";
185+
}, $updateColumns))
188186
: $this->updateAllColumns($columns);
189187
}
190188
}

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 & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public function prepareInsert(TableDefinition $table, BulkData $bulkData, ?Inser
6060
$bulkData->toSqlPlaceholders(),
6161
\implode(',', $options->conflictColumns),
6262
\count($options->updateColumns)
63-
? $this->updateSelectedColumns($options->updateColumns, $bulkData->columns())
63+
? $this->updateSelectedColumns($options->updateColumns, $bulkData->columns(), $table->name(), $options->preserveExistingValues)
6464
: $this->updateAllColumns($bulkData->columns())
6565
);
6666
}
@@ -82,7 +82,7 @@ public function prepareInsert(TableDefinition $table, BulkData $bulkData, ?Inser
8282
);
8383
}
8484

85-
public function prepareUpdate(TableDefinition $table, BulkData $bulkData, ?UpdateOptions $updateOptions = null) : string
85+
public function prepareUpdate(TableDefinition $table, BulkData $bulkData, ?UpdateOptions $options = null) : string
8686
{
8787
return \sprintf(
8888
'REPLACE INTO %s (%s) VALUES %s',
@@ -105,10 +105,18 @@ private function updateAllColumns(Columns $columns) : string
105105
/**
106106
* @param array<string> $updateColumns
107107
*/
108-
private function updateSelectedColumns(array $updateColumns, Columns $columns) : string
108+
private function updateSelectedColumns(array $updateColumns, Columns $columns, string $tableName, ?bool $preserveExistingValues = null) : string
109109
{
110110
return [] !== $updateColumns
111-
? \implode(',', \array_map(fn (string $column) : string => "{$this->platform->quoteIdentifier($column)} = {$this->platform->quoteIdentifier('excluded.' . $column)}", $updateColumns))
111+
? \implode(',', \array_map(function (string $column) use ($tableName, $preserveExistingValues) : string {
112+
$clause = "{$this->platform->quoteIdentifier($column)} = ";
113+
114+
if (true === $preserveExistingValues) {
115+
return $clause . "COALESCE({$this->platform->quoteIdentifier('excluded.' . $column)}, {$tableName}.{$this->platform->quoteIdentifier($column)})";
116+
}
117+
118+
return $clause . "{$this->platform->quoteIdentifier('excluded.' . $column)}";
119+
}, $updateColumns))
112120
: $this->updateAllColumns($columns);
113121
}
114122
}

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
}

src/lib/doctrine-dbal-bulk/tests/Flow/Doctrine/Bulk/Tests/Integration/MySqlBulkInsertTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,57 @@ public function test_inserts_new_rows_and_update_already_existed() : void
155155
);
156156
}
157157

158+
public function test_inserts_new_rows_and_update_selected_columns_and_preserve_existing_values() : void
159+
{
160+
$this->databaseContext->createTable(
161+
(new Table(
162+
$table = 'flow_doctrine_bulk_test',
163+
[
164+
new Column('id', Type::getType(Types::INTEGER), ['notnull' => true]),
165+
new Column('name', Type::getType(Types::STRING), ['notnull' => false, 'length' => 255]),
166+
new Column('description', Type::getType(Types::STRING), ['notnull' => false, 'length' => 255]),
167+
new Column('active', Type::getType(Types::BOOLEAN), ['notnull' => true]),
168+
],
169+
))
170+
->setPrimaryKey(['id'])
171+
);
172+
173+
Bulk::create()->insert(
174+
$this->databaseContext->connection(),
175+
$table,
176+
new BulkData([
177+
['id' => 1, 'name' => 'Name One', 'description' => 'Description One', 'active' => true],
178+
['id' => 2, 'name' => 'Name Two', 'description' => 'Description Two', 'active' => true],
179+
['id' => 3, 'name' => 'Name Three', 'description' => 'Description Three', 'active' => true],
180+
])
181+
);
182+
183+
Bulk::create()->insert(
184+
$this->databaseContext->connection(),
185+
$table,
186+
new BulkData([
187+
['id' => 2, 'name' => 'New Name Two', 'description' => null, 'active' => true],
188+
['id' => 3, 'name' => null, 'description' => 'DESCRIPTION', 'active' => true],
189+
]),
190+
MySQLInsertOptions::fromArray([
191+
'upsert' => true,
192+
'update_columns' => ['name', 'description'],
193+
'preserve_existing_values' => true,
194+
])
195+
);
196+
197+
self::assertEquals(3, $this->databaseContext->tableCount($table));
198+
self::assertEquals(2, $this->executedQueriesCount());
199+
self::assertEquals(
200+
[
201+
['id' => 1, 'name' => 'Name One', 'description' => 'Description One', 'active' => true],
202+
['id' => 2, 'name' => 'New Name Two', 'description' => 'Description Two', 'active' => true],
203+
['id' => 3, 'name' => 'Name Three', 'description' => 'DESCRIPTION', 'active' => true],
204+
],
205+
$this->databaseContext->selectAll($table)
206+
);
207+
}
208+
158209
public function test_inserts_new_rows_and_update_selected_columns_only_of_already_existed() : void
159210
{
160211
$this->databaseContext->createTable(

0 commit comments

Comments
 (0)