Skip to content

Commit 13ad610

Browse files
authored
1713 proposal bulk data parameters (#1777)
* BulkData - positional & named parameters support Allow user to decide if he wants to use positional or named parameters when using BulkData (positional are set as default). Additionally we moved a method generating cased parameters to BulkData from TableDefinition. * Make generate casted sql parameters consistnt with other methods When column type is passed to constructor, method generating sql casted parametrs will prioritize it over detecting column type by executing sql query. * Fixed generating casted sql placeholders * Fixed failing tests
1 parent 1da7a23 commit 13ad610

File tree

9 files changed

+866
-56
lines changed

9 files changed

+866
-56
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function delete(Connection $connection, string $table, BulkData $bulkData
3131
$connection->executeStatement(
3232
$this->queryFactory->delete($connection->getDatabasePlatform(), $tableDefinition, $bulkData),
3333
$bulkData->toSqlParameters($tableDefinition),
34-
$tableDefinition->dbalTypes($bulkData)
34+
$tableDefinition->dbalParameterTypes($bulkData)
3535
);
3636
}
3737

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

Lines changed: 153 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
* @param array<int, array<string, mixed>> $rows
2121
* @param array<Type> $types
2222
*/
23-
public function __construct(array $rows, private array $types = [])
23+
public function __construct(array $rows, private array $types = [], private SQLParametersStyle $parametersStyle = SQLParametersStyle::POSITIONAL)
2424
{
2525
if (0 === \count($rows)) {
2626
throw new RuntimeException('Bulk data cannot be empty');
@@ -58,6 +58,11 @@ public function count() : int
5858
return \count($this->rows);
5959
}
6060

61+
public function parametersStyle() : SQLParametersStyle
62+
{
63+
return $this->parametersStyle;
64+
}
65+
6166
/**
6267
* Example:.
6368
*
@@ -99,6 +104,52 @@ public function sqlRows() : array
99104
return $rows;
100105
}
101106

107+
public function toSqlCastedPlaceholders(TableDefinition $table) : string
108+
{
109+
return match ($this->parametersStyle) {
110+
SQLParametersStyle::NAMED => $this->toSqlNamedCastedPlaceholders($table),
111+
SQLParametersStyle::POSITIONAL => $this->toSqlPositionalCastedPlaceholders($table),
112+
};
113+
}
114+
115+
public function toSqlNamedCastedPlaceholders(TableDefinition $table) : string
116+
{
117+
return \implode(
118+
',',
119+
\array_map(
120+
/**
121+
* @param int $index
122+
* @param array<string, mixed> $row
123+
*
124+
* @return string
125+
*/
126+
function (int $index, array $row) use ($table) : string {
127+
$keys = [];
128+
129+
/**
130+
* @var mixed $value
131+
*/
132+
foreach ($row as $columnName => $value) {
133+
if (\array_key_exists($columnName, $this->types)) {
134+
$type = $this->types[$columnName];
135+
} else {
136+
$type = $table->dbalColumn($columnName)->getType();
137+
}
138+
139+
$keys[] = 'CAST(:' . $columnName . '_' . $index . ' as ' . $type->getSQLDeclaration([], $table->platform()) . ')';
140+
}
141+
142+
return \sprintf(
143+
'(%s)',
144+
\implode(',', $keys)
145+
);
146+
},
147+
\array_keys($this->rows),
148+
$this->rows,
149+
)
150+
);
151+
}
152+
102153
/**
103154
* Example:.
104155
*
@@ -109,7 +160,7 @@ public function sqlRows() : array
109160
*
110161
* @return array<string, mixed>
111162
*/
112-
public function toSqlParameters(TableDefinition $table) : array
163+
public function toSqlNamedParameters(TableDefinition $table) : array
113164
{
114165
$rows = [];
115166

@@ -135,7 +186,7 @@ public function toSqlParameters(TableDefinition $table) : array
135186
* @return string It returns a string for SQL bulk insert query, eg:
136187
* (:id_0, :name_0, :title_0), (:id_1, :name_1, :title_1), (:id_2, :name_2, :title_2)
137188
*/
138-
public function toSqlPlaceholders() : string
189+
public function toSqlNamedPlaceholders() : string
139190
{
140191
return \implode(
141192
',',
@@ -149,6 +200,105 @@ public function toSqlPlaceholders() : string
149200
);
150201
}
151202

203+
/**
204+
* @return array<int<0, max>|string, mixed>
205+
*/
206+
public function toSqlParameters(TableDefinition $table) : array
207+
{
208+
return match ($this->parametersStyle) {
209+
SQLParametersStyle::NAMED => $this->toSqlNamedParameters($table),
210+
SQLParametersStyle::POSITIONAL => $this->toSqlPositionalParameters($table),
211+
};
212+
}
213+
214+
public function toSqlPlaceholders() : string
215+
{
216+
return match ($this->parametersStyle) {
217+
SQLParametersStyle::NAMED => $this->toSqlNamedPlaceholders(),
218+
SQLParametersStyle::POSITIONAL => $this->toSqlPositionalPlaceholders(),
219+
};
220+
}
221+
222+
public function toSqlPositionalCastedPlaceholders(TableDefinition $table) : string
223+
{
224+
return \implode(
225+
',',
226+
\array_map(
227+
/**
228+
* @param array<string, mixed> $row
229+
*
230+
* @return string
231+
*/
232+
function (array $row) use ($table) : string {
233+
$keys = [];
234+
235+
/**
236+
* @var mixed $value
237+
*/
238+
foreach ($row as $columnName => $value) {
239+
if (\array_key_exists($columnName, $this->types)) {
240+
$type = $this->types[$columnName];
241+
} else {
242+
$dbColumn = $table->dbalColumn($columnName);
243+
$type = $dbColumn->getType();
244+
}
245+
246+
$keys[] = 'CAST(? as ' . $type->getSQLDeclaration([], $table->platform()) . ')';
247+
}
248+
249+
return \sprintf(
250+
'(%s)',
251+
\implode(',', $keys)
252+
);
253+
},
254+
$this->rows
255+
)
256+
);
257+
}
258+
259+
/**
260+
* Example:.
261+
*
262+
* [1, 'some name', 2, 'other name']
263+
*
264+
* @return array<int<0, max>, mixed>
265+
*/
266+
public function toSqlPositionalParameters(TableDefinition $table) : array
267+
{
268+
$parameters = [];
269+
270+
foreach ($this->rows as $row) {
271+
/**
272+
* @var mixed $entry
273+
*/
274+
foreach ($row as $column => $entry) {
275+
if (\array_key_exists($column, $this->types)) {
276+
$value = $this->types[$column]->convertToDatabaseValue($entry, $table->platform());
277+
} else {
278+
$value = $table->dbalColumn($column)->getType()->convertToDatabaseValue($entry, $table->platform());
279+
}
280+
281+
$parameters[] = $value;
282+
}
283+
}
284+
285+
return $parameters;
286+
}
287+
288+
/**
289+
* @return string It returns a string for SQL bulk insert query with positional parameters, eg:
290+
* (?,?,?), (?,?,?), (?,?,?)
291+
*/
292+
public function toSqlPositionalPlaceholders() : string
293+
{
294+
$columnCount = \count($this->columns->all());
295+
$rowCount = $this->count();
296+
297+
$rowPlaceholder = '(' . \str_repeat('?,', $columnCount - 1) . '?)';
298+
299+
return \str_repeat($rowPlaceholder . ',', $rowCount - 1) . $rowPlaceholder;
300+
}
301+
152302
/**
153303
* @return array<Type>
154304
*/

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function prepareDelete(TableDefinition $table, BulkData $bulkData) : stri
3939
'DELETE FROM %s WHERE (%s) IN (%s)',
4040
$table->name(),
4141
\implode(', ', \array_map(fn ($column) => $this->platform->quoteIdentifier($column), $columns)),
42-
$table->toSqlCastedPlaceholders($bulkData, $this->platform)
42+
$bulkData->toSqlCastedPlaceholders($table)
4343
);
4444
}
4545

@@ -134,7 +134,7 @@ public function prepareUpdate(TableDefinition $table, BulkData $bulkData, ?Updat
134134
\count($options->updateColumns)
135135
? $this->updatedSelectedColumns($options->updateColumns, $bulkData->columns()->without(...$options->primaryKeyColumns))
136136
: $this->updateAllColumns($bulkData->columns()->without(...$options->primaryKeyColumns)),
137-
$table->toSqlCastedPlaceholders($bulkData, $this->platform),
137+
$bulkData->toSqlCastedPlaceholders($table),
138138
\implode(',', \array_map(fn (string $column) : string => $this->platform->quoteIdentifier($column), $bulkData->columns()->all())),
139139
$this->updatedIndexColumns($options->primaryKeyColumns)
140140
);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Doctrine\Bulk;
6+
7+
enum SQLParametersStyle
8+
{
9+
case NAMED;
10+
case POSITIONAL;
11+
}

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

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,38 @@ public function dbalColumn(string $columnName) : Column
3636
return \current($dbColumnNames);
3737
}
3838

39+
/**
40+
* @param BulkData $bulkData
41+
*
42+
* @throws RuntimeException
43+
*
44+
* @return array<int<0, max>|string, string>
45+
*/
46+
public function dbalParameterTypes(BulkData $bulkData) : array
47+
{
48+
return match ($bulkData->parametersStyle()) {
49+
SQLParametersStyle::NAMED => $this->dbalTypes($bulkData),
50+
SQLParametersStyle::POSITIONAL => $this->dbalPositionalTypes($bulkData),
51+
};
52+
}
53+
54+
/**
55+
* @return array<int<0, max>, string>
56+
*/
57+
public function dbalPositionalTypes(BulkData $bulkData) : array
58+
{
59+
$types = [];
60+
61+
for ($i = 0; $i < $bulkData->count(); $i++) {
62+
foreach ($bulkData->columns()->all() as $columnName) {
63+
$dbColumn = $this->dbalColumn($columnName);
64+
$types[] = Type::getTypeRegistry()->lookupName($dbColumn->getType());
65+
}
66+
}
67+
68+
return $types;
69+
}
70+
3971
/**
4072
* @param BulkData $bulkData
4173
*
@@ -71,39 +103,6 @@ public function platform() : AbstractPlatform
71103
return $this->connection->getDatabasePlatform();
72104
}
73105

74-
public function toSqlCastedPlaceholders(BulkData $bulkData, AbstractPlatform $abstractPlatform) : string
75-
{
76-
return \implode(
77-
',',
78-
\array_map(
79-
/**
80-
* @param int $index
81-
* @param array<string, mixed> $row
82-
*
83-
* @return string
84-
*/
85-
function (int $index, array $row) use ($abstractPlatform) : string {
86-
$keys = [];
87-
88-
/**
89-
* @var mixed $value
90-
*/
91-
foreach ($row as $columnName => $value) {
92-
$dbColumn = $this->dbalColumn($columnName);
93-
$keys[] = 'CAST(:' . $columnName . '_' . $index . ' as ' . $dbColumn->getType()->getSQLDeclaration($dbColumn->toArray(), $abstractPlatform) . ')';
94-
}
95-
96-
return \sprintf(
97-
'(%s)',
98-
\implode(',', $keys)
99-
);
100-
},
101-
\array_keys($bulkData->rows()),
102-
$bulkData->rows(),
103-
)
104-
);
105-
}
106-
107106
/**
108107
* @return array<Column>
109108
*/

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,59 @@ public function test_delete_rows_with_single_column_condition() : void
157157
$remainingRows
158158
);
159159
}
160+
161+
public function test_delete_with_custom_types_using_casted_placeholders_works_with_postgresql() : void
162+
{
163+
$this->databaseContext->createTable(
164+
(new Table(
165+
$table = 'flow_doctrine_bulk_test',
166+
[
167+
new Column('id', Type::getType(Types::INTEGER), ['notnull' => true]),
168+
new Column('name', Type::getType(Types::STRING), ['notnull' => true, 'length' => 255]),
169+
new Column('category', Type::getType(Types::STRING), ['notnull' => true, 'length' => 100]),
170+
],
171+
))
172+
->setPrimaryKey(['id', 'name', 'category'])
173+
);
174+
175+
$this->databaseContext->connection()->executeStatement(
176+
"INSERT INTO {$table} (id, name, category) VALUES
177+
(1, 'Product One', 'Electronics'),
178+
(2, 'Product Two', 'Books'),
179+
(3, 'Product Three', 'Electronics'),
180+
(4, 'Product Four', 'Clothing')"
181+
);
182+
183+
self::assertEquals(4, $this->databaseContext->tableCount($table));
184+
185+
$customTypes = [
186+
'id' => Type::getType(Types::INTEGER),
187+
'name' => Type::getType(Types::STRING),
188+
'category' => Type::getType(Types::STRING),
189+
];
190+
191+
$bulkData = new BulkData([
192+
['id' => 1, 'name' => 'Product One', 'category' => 'Electronics'],
193+
['id' => 3, 'name' => 'Product Three', 'category' => 'Electronics'],
194+
], $customTypes);
195+
196+
Bulk::create()->delete(
197+
$this->databaseContext->connection(),
198+
$table,
199+
$bulkData
200+
);
201+
202+
self::assertEquals(2, $this->databaseContext->tableCount($table));
203+
self::assertEquals(1, $this->executedQueriesCount());
204+
205+
$remainingRows = $this->databaseContext->selectAll($table);
206+
self::assertCount(2, $remainingRows);
207+
self::assertEquals(
208+
[
209+
['id' => 2, 'name' => 'Product Two', 'category' => 'Books'],
210+
['id' => 4, 'name' => 'Product Four', 'category' => 'Clothing'],
211+
],
212+
$remainingRows
213+
);
214+
}
160215
}

0 commit comments

Comments
 (0)