Skip to content

Commit c7cfba4

Browse files
authored
Merge pull request #8439 from kenjis/fix-postgre-updateBatch-2
fix: [Postgre] QueryBuilder::updateBatch() does not work (No API change)
2 parents 3d8b04b + baca36e commit c7cfba4

File tree

7 files changed

+311
-43
lines changed

7 files changed

+311
-43
lines changed

phpstan-baseline.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,11 +1341,6 @@
13411341
'count' => 7,
13421342
'path' => __DIR__ . '/system/Database/Postgre/Builder.php',
13431343
];
1344-
$ignoreErrors[] = [
1345-
'message' => '#^Only booleans are allowed in a negated boolean, array\\<int\\|string, array\\<int, int\\|string\\>\\|string\\> given\\.$#',
1346-
'count' => 1,
1347-
'path' => __DIR__ . '/system/Database/Postgre/Builder.php',
1348-
];
13491344
$ignoreErrors[] = [
13501345
'message' => '#^Return type \\(CodeIgniter\\\\Database\\\\BaseBuilder\\) of method CodeIgniter\\\\Database\\\\Postgre\\\\Builder\\:\\:join\\(\\) should be covariant with return type \\(\\$this\\(CodeIgniter\\\\Database\\\\BaseBuilder\\)\\) of method CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:join\\(\\)$#',
13511346
'count' => 1,

system/Database/BaseBuilder.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,11 @@ class BaseBuilder
169169
* constraints?: array,
170170
* setQueryAsData?: string,
171171
* sql?: string,
172-
* alias?: string
172+
* alias?: string,
173+
* fieldTypes?: array<string, array<string, string>>
173174
* }
175+
*
176+
* fieldTypes: [ProtectedTableName => [FieldName => Type]]
174177
*/
175178
protected $QBOptions;
176179

@@ -1758,6 +1761,8 @@ public function getWhere($where = null, ?int $limit = null, ?int $offset = 0, bo
17581761
/**
17591762
* Compiles batch insert/update/upsert strings and runs the queries
17601763
*
1764+
* @param '_deleteBatch'|'_insertBatch'|'_updateBatch'|'_upsertBatch' $renderMethod
1765+
*
17611766
* @return false|int|string[] Number of rows inserted or FALSE on failure, SQL array when testMode
17621767
*
17631768
* @throws DatabaseException

system/Database/Postgre/Builder.php

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public function replace(?array $set = null)
146146
$this->set($set);
147147
}
148148

149-
if (! $this->QBSet) {
149+
if ($this->QBSet === []) {
150150
if ($this->db->DBDebug) {
151151
throw new DatabaseException('You must use the "set" method to update an entry.');
152152
}
@@ -312,6 +312,132 @@ public function join(string $table, $cond, string $type = '', ?bool $escape = nu
312312
return parent::join($table, $cond, $type, $escape);
313313
}
314314

315+
/**
316+
* Generates a platform-specific batch update string from the supplied data
317+
*
318+
* @used-by batchExecute
319+
*
320+
* @param string $table Protected table name
321+
* @param list<string> $keys QBKeys
322+
* @param list<list<int|string>> $values QBSet
323+
*/
324+
protected function _updateBatch(string $table, array $keys, array $values): string
325+
{
326+
$sql = $this->QBOptions['sql'] ?? '';
327+
328+
// if this is the first iteration of batch then we need to build skeleton sql
329+
if ($sql === '') {
330+
$constraints = $this->QBOptions['constraints'] ?? [];
331+
332+
if ($constraints === []) {
333+
if ($this->db->DBDebug) {
334+
throw new DatabaseException('You must specify a constraint to match on for batch updates.'); // @codeCoverageIgnore
335+
}
336+
337+
return ''; // @codeCoverageIgnore
338+
}
339+
340+
$updateFields = $this->QBOptions['updateFields'] ??
341+
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
342+
[];
343+
344+
$alias = $this->QBOptions['alias'] ?? '_u';
345+
346+
$sql = 'UPDATE ' . $this->compileIgnore('update') . $table . "\n";
347+
348+
$sql .= "SET\n";
349+
350+
$that = $this;
351+
$sql .= implode(
352+
",\n",
353+
array_map(
354+
static fn ($key, $value) => $key . ($value instanceof RawSql ?
355+
' = ' . $value :
356+
' = ' . $that->cast($alias . '.' . $value, $that->getFieldType($table, $key))),
357+
array_keys($updateFields),
358+
$updateFields
359+
)
360+
) . "\n";
361+
362+
$sql .= "FROM (\n{:_table_:}";
363+
364+
$sql .= ') ' . $alias . "\n";
365+
366+
$sql .= 'WHERE ' . implode(
367+
' AND ',
368+
array_map(
369+
static function ($key, $value) use ($table, $alias, $that) {
370+
if ($value instanceof RawSql && is_string($key)) {
371+
return $table . '.' . $key . ' = ' . $value;
372+
}
373+
374+
if ($value instanceof RawSql) {
375+
return $value;
376+
}
377+
378+
return $table . '.' . $value . ' = '
379+
. $that->cast($alias . '.' . $value, $that->getFieldType($table, $value));
380+
},
381+
array_keys($constraints),
382+
$constraints
383+
)
384+
);
385+
386+
$this->QBOptions['sql'] = $sql;
387+
}
388+
389+
if (isset($this->QBOptions['setQueryAsData'])) {
390+
$data = $this->QBOptions['setQueryAsData'];
391+
} else {
392+
$data = implode(
393+
" UNION ALL\n",
394+
array_map(
395+
static fn ($value) => 'SELECT ' . implode(', ', array_map(
396+
static fn ($key, $index) => $index . ' ' . $key,
397+
$keys,
398+
$value
399+
)),
400+
$values
401+
)
402+
) . "\n";
403+
}
404+
405+
return str_replace('{:_table_:}', $data, $sql);
406+
}
407+
408+
/**
409+
* Returns cast expression.
410+
*
411+
* @TODO move this to BaseBuilder in 4.5.0
412+
*
413+
* @param float|int|string $expression
414+
*/
415+
private function cast($expression, ?string $type): string
416+
{
417+
return ($type === null) ? $expression : 'CAST(' . $expression . ' AS ' . strtoupper($type) . ')';
418+
}
419+
420+
/**
421+
* Returns the filed type from database meta data.
422+
*
423+
* @param string $table Protected table name.
424+
* @param string $fieldName Field name. May be protected.
425+
*/
426+
private function getFieldType(string $table, string $fieldName): ?string
427+
{
428+
$fieldName = trim($fieldName, $this->db->escapeChar);
429+
430+
if (! isset($this->QBOptions['fieldTypes'][$table])) {
431+
$this->QBOptions['fieldTypes'][$table] = [];
432+
433+
foreach ($this->db->getFieldData($table) as $field) {
434+
$this->QBOptions['fieldTypes'][$table][$field->name] = $field->type;
435+
}
436+
}
437+
438+
return $this->QBOptions['fieldTypes'][$table][$fieldName] ?? null;
439+
}
440+
315441
/**
316442
* Generates a platform-specific upsertBatch string from the supplied data
317443
*

tests/_support/Database/Migrations/20160428212500_Create_test_tables.php

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,23 @@ public function up(): void
4646
])->addKey('id', true)->createTable('misc', true);
4747

4848
// Database Type test table
49-
// missing types :
50-
// TINYINT,MEDIUMINT,BIT,YEAR,BINARY , VARBINARY, TINYTEXT,LONGTEXT,YEAR,JSON,Spatial data types
51-
// id must be interger else SQLite3 error on not null for autoinc field
49+
// missing types:
50+
// TINYINT,MEDIUMINT,BIT,YEAR,BINARY,VARBINARY,TINYTEXT,LONGTEXT,
51+
// JSON,Spatial data types
52+
// `id` must be INTEGER else SQLite3 error on not null for autoincrement field.
5253
$data_type_fields = [
53-
'id' => ['type' => 'INTEGER', 'constraint' => 20, 'auto_increment' => true],
54-
'type_varchar' => ['type' => 'VARCHAR', 'constraint' => 40, 'null' => true],
55-
'type_char' => ['type' => 'CHAR', 'constraint' => 10, 'null' => true],
56-
'type_text' => ['type' => 'TEXT', 'null' => true],
57-
'type_smallint' => ['type' => 'SMALLINT', 'null' => true],
58-
'type_integer' => ['type' => 'INTEGER', 'null' => true],
54+
'id' => ['type' => 'INTEGER', 'constraint' => 20, 'auto_increment' => true],
55+
'type_varchar' => ['type' => 'VARCHAR', 'constraint' => 40, 'null' => true],
56+
'type_char' => ['type' => 'CHAR', 'constraint' => 10, 'null' => true],
57+
// TEXT should not be used on SQLSRV. It is deprecated.
58+
'type_text' => ['type' => 'TEXT', 'null' => true],
59+
'type_smallint' => ['type' => 'SMALLINT', 'null' => true],
60+
'type_integer' => ['type' => 'INTEGER', 'null' => true],
61+
// FLOAT should not be used on MySQL.
62+
// CREATE TABLE t (f FLOAT, d DOUBLE);
63+
// INSERT INTO t VALUES(99.9, 99.9);
64+
// SELECT * FROM t WHERE f=99.9; // Empty set
65+
// SELECT * FROM t WHERE d=99.9; // 1 row
5966
'type_float' => ['type' => 'FLOAT', 'null' => true],
6067
'type_numeric' => ['type' => 'NUMERIC', 'constraint' => '18,2', 'null' => true],
6168
'type_date' => ['type' => 'DATE', 'null' => true],

tests/system/Database/Live/UpdateTest.php

Lines changed: 149 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -111,29 +111,159 @@ public function testUpdateWithWhereAndLimit(): void
111111
}
112112
}
113113

114-
public function testUpdateBatch(): void
114+
/**
115+
* @dataProvider provideUpdateBatch
116+
*/
117+
public function testUpdateBatch(string $constraints, array $data, array $expected): void
115118
{
116-
$data = [
117-
[
118-
'name' => 'Derek Jones',
119-
'country' => 'Greece',
119+
$table = 'type_test';
120+
121+
// Prepares test data.
122+
$builder = $this->db->table($table);
123+
$builder->truncate();
124+
125+
for ($i = 1; $i < 4; $i++) {
126+
$builder->insert([
127+
'type_varchar' => 'test' . $i,
128+
'type_char' => 'char',
129+
'type_text' => 'text',
130+
'type_smallint' => 32767,
131+
'type_integer' => 2_147_483_647,
132+
'type_bigint' => 9_223_372_036_854_775_807,
133+
'type_float' => 10.1,
134+
'type_numeric' => 123.23,
135+
'type_date' => '2023-12-0' . $i,
136+
'type_datetime' => '2023-12-21 12:00:00',
137+
]);
138+
}
139+
140+
$this->db->table($table)->updateBatch($data, $constraints);
141+
142+
if ($this->db->DBDriver === 'SQLSRV') {
143+
// We cannot compare `text` and `varchar` with `=`. It causes the error:
144+
// [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]The data types text and varchar are incompatible in the equal to operator.
145+
// And data type `text`, `ntext`, `image` are deprecated in SQL Server 2016
146+
// See https://github.com/codeigniter4/CodeIgniter4/pull/8439#issuecomment-1902535909
147+
unset($expected[0]['type_text'], $expected[1]['type_text']);
148+
}
149+
150+
$this->seeInDatabase($table, $expected[0]);
151+
$this->seeInDatabase($table, $expected[1]);
152+
}
153+
154+
public static function provideUpdateBatch(): iterable
155+
{
156+
yield from [
157+
'constraints varchar' => [
158+
'type_varchar',
159+
[
160+
[
161+
'type_varchar' => 'test1', // Key
162+
'type_text' => 'updated',
163+
'type_smallint' => 9999,
164+
'type_integer' => 9_999_999,
165+
'type_bigint' => 9_999_999,
166+
'type_float' => 99.9,
167+
'type_numeric' => 999999.99,
168+
'type_date' => '2024-01-01',
169+
'type_datetime' => '2024-01-01 09:00:00',
170+
],
171+
[
172+
'type_varchar' => 'test2', // Key
173+
'type_text' => 'updated',
174+
'type_smallint' => 9999,
175+
'type_integer' => 9_999_999,
176+
'type_bigint' => 9_999_999,
177+
'type_float' => 99.9,
178+
'type_numeric' => 999999.99,
179+
'type_date' => '2024-01-01',
180+
'type_datetime' => '2024-01-01 09:00:00',
181+
],
182+
],
183+
[
184+
[
185+
'type_varchar' => 'test1',
186+
'type_text' => 'updated',
187+
'type_smallint' => 9999,
188+
'type_integer' => 9_999_999,
189+
'type_bigint' => 9_999_999,
190+
'type_numeric' => 999999.99,
191+
'type_date' => '2024-01-01',
192+
'type_datetime' => '2024-01-01 09:00:00',
193+
],
194+
[
195+
'type_varchar' => 'test2',
196+
'type_text' => 'updated',
197+
'type_smallint' => 9999,
198+
'type_integer' => 9_999_999,
199+
'type_bigint' => 9_999_999,
200+
'type_numeric' => 999999.99,
201+
'type_date' => '2024-01-01',
202+
'type_datetime' => '2024-01-01 09:00:00',
203+
],
204+
],
120205
],
121-
[
122-
'name' => 'Ahmadinejad',
123-
'country' => 'Greece',
206+
'constraints date' => [
207+
'type_date',
208+
[
209+
[
210+
'type_text' => 'updated',
211+
'type_bigint' => 9_999_999,
212+
'type_date' => '2023-12-01', // Key
213+
'type_datetime' => '2024-01-01 09:00:00',
214+
],
215+
[
216+
'type_text' => 'updated',
217+
'type_bigint' => 9_999_999,
218+
'type_date' => '2023-12-02', // Key
219+
'type_datetime' => '2024-01-01 09:00:00',
220+
],
221+
],
222+
[
223+
[
224+
'type_varchar' => 'test1',
225+
'type_text' => 'updated',
226+
'type_bigint' => 9_999_999,
227+
'type_date' => '2023-12-01',
228+
'type_datetime' => '2024-01-01 09:00:00',
229+
],
230+
[
231+
'type_varchar' => 'test2',
232+
'type_text' => 'updated',
233+
'type_bigint' => 9_999_999,
234+
'type_date' => '2023-12-02',
235+
'type_datetime' => '2024-01-01 09:00:00',
236+
],
237+
],
238+
],
239+
'int as string' => [
240+
'type_varchar',
241+
[
242+
[
243+
'type_varchar' => 'test1', // Key
244+
'type_integer' => '9999999', // PHP string
245+
'type_bigint' => '2448114396435166946', // PHP string
246+
],
247+
[
248+
'type_varchar' => 'test2', // Key
249+
'type_integer' => '9999999', // PHP string
250+
'type_bigint' => '2448114396435166946', // PHP string
251+
],
252+
],
253+
[
254+
[
255+
'type_varchar' => 'test1',
256+
'type_integer' => 9_999_999,
257+
'type_bigint' => 2_448_114_396_435_166_946,
258+
],
259+
[
260+
'type_varchar' => 'test2',
261+
'type_integer' => 9_999_999,
262+
'type_bigint' => 2_448_114_396_435_166_946,
263+
],
264+
],
124265
],
125266
];
126-
127-
$this->db->table('user')->updateBatch($data, 'name');
128-
129-
$this->seeInDatabase('user', [
130-
'name' => 'Derek Jones',
131-
'country' => 'Greece',
132-
]);
133-
$this->seeInDatabase('user', [
134-
'name' => 'Ahmadinejad',
135-
'country' => 'Greece',
136-
]);
137267
}
138268

139269
public function testUpdateWithWhereSameColumn(): void

user_guide_src/source/changelogs/v4.4.5.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ Deprecations
3030
Bugs Fixed
3131
**********
3232

33+
- **QueryBuilder:** Fixed a bug that the ``updateBatch()`` method does not work
34+
due to type errors on PostgreSQL.
35+
3336
See the repo's
3437
`CHANGELOG.md <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_
3538
for a complete list of bugs fixed.

0 commit comments

Comments
 (0)