Skip to content

Commit d2f0be2

Browse files
committed
fix: cast OCI8 batch subquery values so updateBatch() and deleteBatch() accept mixed PHP types
1 parent 4bc33aa commit d2f0be2

4 files changed

Lines changed: 62 additions & 19 deletions

File tree

‎system/Database/OCI8/Builder.php‎

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -282,14 +282,24 @@ protected function _updateBatch(string $table, array $keys, array $values): stri
282282
if (isset($this->QBOptions['setQueryAsData'])) {
283283
$data = $this->QBOptions['setQueryAsData'];
284284
} else {
285+
$updateFields = $this->QBOptions['updateFields'] ?? [];
286+
285287
$data = implode(
286288
" UNION ALL\n",
287289
array_map(
288-
static fn ($value): string => 'SELECT ' . implode(', ', array_map(
289-
static fn ($key, $index): string => $index . ' ' . $key,
290+
fn (array $value): string => sprintf('SELECT %s FROM DUAL', implode(', ', array_map(
291+
function (string $key, float|int|string $index) use ($table, $updateFields): string {
292+
$column = array_search($key, $updateFields, true);
293+
294+
return sprintf(
295+
'%s %s',
296+
$this->cast((string) $index, $this->getFieldType($table, is_string($column) ? $column : $key)),
297+
$key,
298+
);
299+
},
290300
$keys,
291301
$value,
292-
)) . ' FROM DUAL',
302+
))),
293303
$values,
294304
),
295305
) . "\n";
@@ -466,14 +476,24 @@ protected function _deleteBatch(string $table, array $keys, array $values): stri
466476
if (isset($this->QBOptions['setQueryAsData'])) {
467477
$data = $this->QBOptions['setQueryAsData'];
468478
} else {
479+
$constraints = $this->QBOptions['constraints'] ?? [];
480+
469481
$data = implode(
470482
" FROM DUAL UNION ALL\n",
471483
array_map(
472-
static fn ($value): string => 'SELECT ' . implode(', ', array_map(
473-
static fn ($key, $index): string => $index . ' ' . $key,
484+
fn (array $value): string => sprintf('SELECT %s', implode(', ', array_map(
485+
function (string $key, float|int|string $index) use ($table, $constraints): string {
486+
$column = array_search($key, $constraints, true);
487+
488+
return sprintf(
489+
'%s %s',
490+
$this->cast((string) $index, $this->getFieldType($table, is_string($column) ? $column : $key)),
491+
$key,
492+
);
493+
},
474494
$keys,
475495
$value,
476-
)),
496+
))),
477497
$values,
478498
),
479499
) . " FROM DUAL\n";
@@ -489,4 +509,39 @@ protected function fieldsFromQuery(string $sql): array
489509
{
490510
return $this->db->query('SELECT * FROM (' . $sql . ') "_u_" WHERE ROWNUM = 1')->getFieldNames();
491511
}
512+
513+
private function cast(string $expression, ?string $type): string
514+
{
515+
return $type === null ? $expression : sprintf('CAST(%s AS %s)', $expression, $type);
516+
}
517+
518+
/**
519+
* Returns the type to cast a literal to for the field, or null when the field is unknown or a LOB.
520+
*
521+
* @param string $table Protected table name.
522+
* @param string $fieldName Field name. May be protected.
523+
*/
524+
private function getFieldType(string $table, string $fieldName): ?string
525+
{
526+
$fieldName = trim($fieldName, $this->db->escapeChar);
527+
528+
if (! isset($this->QBOptions['fieldTypes'][$table])) {
529+
$this->QBOptions['fieldTypes'][$table] = [];
530+
531+
foreach ($this->db->getFieldData(str_replace($this->db->escapeChar, '', $table)) as $field) {
532+
$this->QBOptions['fieldTypes'][$table][$field->name] = $this->castType($field->type, $field->max_length);
533+
}
534+
}
535+
536+
return $this->QBOptions['fieldTypes'][$table][$fieldName] ?? null;
537+
}
538+
539+
private function castType(string $type, int|string|null $maxLength): ?string
540+
{
541+
return match ($type) {
542+
'CHAR', 'NCHAR', 'VARCHAR2', 'NVARCHAR2', 'RAW' => sprintf('%s(%d)', $type, $maxLength),
543+
'CLOB', 'NCLOB', 'BLOB', 'BFILE', 'LONG', 'LONG RAW' => null,
544+
default => $type,
545+
};
546+
}
492547
}

‎tests/system/Database/Live/DeleteTest.php‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,6 @@ public function testDeleteBatchWithMixedConstraintValueTypesInTextColumn(): void
125125
$this->markTestSkipped('SQL Server cannot compare `text` columns with `=`.');
126126
}
127127

128-
if ($this->db->DBDriver === 'OCI8') {
129-
$this->markTestSkipped('TODO: the OCI8 builder does not cast mixed `UNION ALL` values yet. Remove this skip once it does.');
130-
}
131-
132128
$table = 'type_test';
133129

134130
$builder = $this->db->table($table);

‎tests/system/Database/Live/UpdateTest.php‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,6 @@ public function testUpdateBatchWithMixedValueTypesInTextColumn(): void
284284
$this->markTestSkipped('SQL Server cannot compare `text` columns with `=`.');
285285
}
286286

287-
if ($this->db->DBDriver === 'OCI8') {
288-
$this->markTestSkipped('TODO: the OCI8 builder does not cast mixed `UNION ALL` values yet. Remove this skip once it does.');
289-
}
290-
291287
$table = 'type_test';
292288

293289
$builder = $this->db->table($table);
@@ -323,10 +319,6 @@ public function testUpdateBatchWithMappedUpdateFieldsAndMixedValueTypes(): void
323319
$this->markTestSkipped('SQL Server cannot compare `text` columns with `=`.');
324320
}
325321

326-
if ($this->db->DBDriver === 'OCI8') {
327-
$this->markTestSkipped('TODO: the OCI8 builder does not cast mixed `UNION ALL` values yet. Remove this skip once it does.');
328-
}
329-
330322
$table = 'type_test';
331323

332324
$builder = $this->db->table($table);

‎user_guide_src/source/changelogs/v4.7.5.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Bugs Fixed
5454
- **Cookie:** Fixed a bug where ``Cookie`` instances allowed invalid characters in path, domain, and prefix attributes rejected by ``setcookie()`` and ``setrawcookie()``.
5555
- **Database:** Fixed a bug where rebuilding a SQLite3 table (e.g., ``Forge::dropColumn()``, ``Forge::modifyColumn()``, ``Forge::dropForeignKey()`` and ``Forge::dropPrimaryKey()``) corrupted the table names referenced by its foreign keys when ``DBPrefix`` was set.
5656
- **Database:** Fixed a bug where Postgre query failures were silently ignored when ``DBDebug`` was enabled and PHP warnings were disabled. A ``DatabaseException`` is now thrown.
57-
- **Database:** Fixed a bug where ``updateBatch()`` and ``deleteBatch()`` failed on PostgreSQL when the rows supplied different PHP types (e.g., a string and an integer) for the same column, because the ``UNION ALL`` subquery values were not cast to the column type.
57+
- **Database:** Fixed a bug where ``updateBatch()`` and ``deleteBatch()`` failed on PostgreSQL and Oracle when the rows supplied different PHP types (e.g., a string and an integer) for the same column, because the ``UNION ALL`` subquery values were not cast to the column type.
5858
- **Debug:** Fixed a bug where ``Timer::start()`` treated ``0.0`` as an empty value and substituted the current time.
5959
- **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``.
6060
- **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them.

0 commit comments

Comments
 (0)