From 0851bf5321d373bd0c8e6673a0bcae4ebeb97825 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sat, 21 Dec 2024 19:04:07 +0100 Subject: [PATCH 01/17] fix prepare statement sqlite --- system/Database/SQLite3/PreparedQuery.php | 2 ++ .../Database/Live/PreparedQueryTest.php | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php index 21dc4c2fdeff..b86bd9981f49 100644 --- a/system/Database/SQLite3/PreparedQuery.php +++ b/system/Database/SQLite3/PreparedQuery.php @@ -75,6 +75,8 @@ public function _execute(array $data): bool $bindType = SQLITE3_INTEGER; } elseif (is_float($item)) { $bindType = SQLITE3_FLOAT; + } elseif (is_string($item) && mb_detect_encoding($item, 'UTF-8', true) === false) { + $bindType = SQLITE3_BLOB; } else { $bindType = SQLITE3_TEXT; } diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index fd3b6cedb403..0e5dd579e931 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -269,4 +269,23 @@ public function testDeallocatePreparedQueryThenTryToClose(): void $this->query->close(); } + + public function testInsertBinaryData(): void + { + if ($this->db->DBDriver === 'Postgre' || $this->db->DBDriver === 'SQLSRV') { + $this->markTestSkipped('Blob not supported for Postgre and SQLSRV.'); + } + + $this->query = $this->db->prepare(static fn ($db) => $db->table('type_test')->insert([ + 'type_blob' => 'binary', + ])); + + $fileContent = file_get_contents(TESTPATH . '_support/Images/EXIFsamples/landscape_0.jpg'); + $this->assertTrue($this->query->execute($fileContent)); + + $id = $this->db->insertId(); + $file = $this->db->table('type_test')->where('id', $id)->get()->getRow(); + + $this->assertSame(strlen($fileContent), strlen($file->type_blob)); + } } From d9d32b7aef685cc209b56587fdca11afdc83ac25 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sat, 21 Dec 2024 19:04:57 +0100 Subject: [PATCH 02/17] fix prepare statement postgre --- system/Database/Postgre/Forge.php | 4 ++++ system/Database/Postgre/PreparedQuery.php | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php index b516172737ae..704f3e576047 100644 --- a/system/Database/Postgre/Forge.php +++ b/system/Database/Postgre/Forge.php @@ -173,6 +173,10 @@ protected function _attributeType(array &$attributes) $attributes['TYPE'] = 'TIMESTAMP'; break; + case 'BLOB': + $attributes['TYPE'] = 'BYTEA'; + break; + default: break; } diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php index 33a6c8044c7d..430fd70cff88 100644 --- a/system/Database/Postgre/PreparedQuery.php +++ b/system/Database/Postgre/PreparedQuery.php @@ -87,6 +87,12 @@ public function _execute(array $data): bool throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); } + foreach ($data as &$item) { + if (is_string($item) && mb_detect_encoding($item, 'UTF-8', true) === false) { + $item = pg_escape_bytea($this->db->connID, $item); + } + } + $this->result = pg_execute($this->db->connID, $this->name, $data); return (bool) $this->result; From 7307ff09f3280f3f01a399551d29111a4603e306 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sat, 21 Dec 2024 19:05:22 +0100 Subject: [PATCH 03/17] fix prepare statement sqlsrv --- system/Database/SQLSRV/PreparedQuery.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/Database/SQLSRV/PreparedQuery.php b/system/Database/SQLSRV/PreparedQuery.php index f3a4a14c2bc6..3013e31c185b 100644 --- a/system/Database/SQLSRV/PreparedQuery.php +++ b/system/Database/SQLSRV/PreparedQuery.php @@ -59,7 +59,7 @@ public function _prepare(string $sql, array $options = []): PreparedQuery // Prepare parameters for the query $queryString = $this->getQueryString(); - $parameters = $this->parameterize($queryString); + $parameters = $this->parameterize($queryString, $options); // Prepare the query $this->statement = sqlsrv_prepare($this->db->connID, $sql, $parameters); @@ -121,7 +121,7 @@ protected function _close(): bool /** * Handle parameters. */ - protected function parameterize(string $queryString): array + protected function parameterize(string $queryString, array $options): array { $numberOfVariables = substr_count($queryString, '?'); @@ -129,7 +129,7 @@ protected function parameterize(string $queryString): array for ($c = 0; $c < $numberOfVariables; $c++) { $this->parameters[$c] = null; - $params[] = &$this->parameters[$c]; + $params[] = [&$this->parameters[$c], SQLSRV_PARAM_IN, $options[$c] ?? SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)]; } return $params; From ae7912a023f0d83f310bbf5458b3d1a1196e9e4a Mon Sep 17 00:00:00 2001 From: michalsn Date: Sat, 21 Dec 2024 19:05:44 +0100 Subject: [PATCH 04/17] fix prepare statement oci8 --- system/Database/OCI8/PreparedQuery.php | 28 +++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php index feffc3be50b5..97825c535634 100644 --- a/system/Database/OCI8/PreparedQuery.php +++ b/system/Database/OCI8/PreparedQuery.php @@ -73,11 +73,33 @@ public function _execute(array $data): bool throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); } - foreach (array_keys($data) as $key) { - oci_bind_by_name($this->statement, ':' . $key, $data[$key]); + $blobs = []; + + foreach ($data as $key => $item) { + if (is_string($item) && mb_detect_encoding($item, 'UTF-8', true) === false) { + $blobs[$key] = oci_new_descriptor($this->db->connID, OCI_D_LOB); + oci_bind_by_name($this->statement, ':' . $key, $blobs[$key], -1, OCI_B_BLOB); + } else { + oci_bind_by_name($this->statement, ':' . $key, $item); + } } - $result = oci_execute($this->statement, $this->db->commitMode); + $result = oci_execute($this->statement, $blobs === [] ? $this->db->commitMode : OCI_NO_AUTO_COMMIT); + + if ($blobs !== []) { + foreach ($blobs as $key => $blob) { + if (! $blob->save($data[$key])) { + oci_rollback($this->db->connID); + return false; + } + } + + oci_commit($this->db->connID); + + foreach ($blobs as $blob) { + $blob->free(); + } + } if ($result && $this->lastInsertTableName !== '') { $this->db->lastInsertedTableName = $this->lastInsertTableName; From 0f0e630c3d3193ffa6cb888932689c4e24b76eec Mon Sep 17 00:00:00 2001 From: michalsn Date: Sat, 21 Dec 2024 19:07:10 +0100 Subject: [PATCH 05/17] tests --- .../20160428212500_Create_test_tables.php | 5 ++-- .../Database/Live/PreparedQueryTest.php | 24 ++++++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php index cf567ace96cf..3753fb49ffa7 100644 --- a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php +++ b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php @@ -91,7 +91,7 @@ public function up(): void } if ($this->db->DBDriver === 'SQLSRV') { - unset($dataTypeFields['type_timestamp']); + unset($dataTypeFields['type_timestamp'], $dataTypeFields['type_blob']); $dataTypeFields['type_text'] = ['type' => 'NVARCHAR(max)', 'null' => true]; } @@ -99,8 +99,7 @@ public function up(): void unset( $dataTypeFields['type_set'], $dataTypeFields['type_mediumtext'], - $dataTypeFields['type_double'], - $dataTypeFields['type_blob'] + $dataTypeFields['type_double'] ); } diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index 0e5dd579e931..d2205f06d3a5 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -272,20 +272,32 @@ public function testDeallocatePreparedQueryThenTryToClose(): void public function testInsertBinaryData(): void { - if ($this->db->DBDriver === 'Postgre' || $this->db->DBDriver === 'SQLSRV') { - $this->markTestSkipped('Blob not supported for Postgre and SQLSRV.'); + $params = []; + if ($this->db->DBDriver === 'SQLSRV') { + $params = [0 => SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)]; } $this->query = $this->db->prepare(static fn ($db) => $db->table('type_test')->insert([ 'type_blob' => 'binary', - ])); + ]), $params); $fileContent = file_get_contents(TESTPATH . '_support/Images/EXIFsamples/landscape_0.jpg'); $this->assertTrue($this->query->execute($fileContent)); - $id = $this->db->insertId(); - $file = $this->db->table('type_test')->where('id', $id)->get()->getRow(); + $id = $this->db->insertId(); + $builder = $this->db->table('type_test'); + + if ($this->db->DBDriver === 'Postgre') { + $file = $builder->select("ENCODE(type_blob, 'base64') AS type_blob")->where('id', $id)->get()->getRow(); + $file = base64_decode($file->type_blob, true); + } elseif ($this->db->DBDriver === 'OCI8') { + $file = $builder->select('type_blob')->where('id', $id)->get()->getRow(); + $file = $file->type_blob->load(); + } else { + $file = $builder->select('type_blob')->where('id', $id)->get()->getRow(); + $file = $file->type_blob; + } - $this->assertSame(strlen($fileContent), strlen($file->type_blob)); + $this->assertSame(strlen($fileContent), strlen($file)); } } From 4fca7e6b9338cd811177427de29f71f8b7b3c1ae Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 22 Dec 2024 14:39:39 +0100 Subject: [PATCH 06/17] abstract isBinary() method --- system/Database/BasePreparedQuery.php | 8 ++++++++ system/Database/Postgre/PreparedQuery.php | 2 +- system/Database/SQLite3/PreparedQuery.php | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index 8c4f252cfb7e..fd84535bc2d5 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -259,4 +259,12 @@ public function getErrorMessage(): string { return $this->errorString; } + + /** + * Whether the input contain binary data. + */ + protected function isBinary($input): bool + { + return mb_detect_encoding($input, 'UTF-8', true) === false; + } } diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php index 430fd70cff88..c55d5d8c5402 100644 --- a/system/Database/Postgre/PreparedQuery.php +++ b/system/Database/Postgre/PreparedQuery.php @@ -88,7 +88,7 @@ public function _execute(array $data): bool } foreach ($data as &$item) { - if (is_string($item) && mb_detect_encoding($item, 'UTF-8', true) === false) { + if (is_string($item) && $this->isBinary($item)) { $item = pg_escape_bytea($this->db->connID, $item); } } diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php index b86bd9981f49..0e15b5d61f3b 100644 --- a/system/Database/SQLite3/PreparedQuery.php +++ b/system/Database/SQLite3/PreparedQuery.php @@ -75,7 +75,7 @@ public function _execute(array $data): bool $bindType = SQLITE3_INTEGER; } elseif (is_float($item)) { $bindType = SQLITE3_FLOAT; - } elseif (is_string($item) && mb_detect_encoding($item, 'UTF-8', true) === false) { + } elseif (is_string($item) && $this->isBinary($item)) { $bindType = SQLITE3_BLOB; } else { $bindType = SQLITE3_TEXT; From 59774344db1caf5181e0911020ff5b0fcf830d82 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 22 Dec 2024 14:40:13 +0100 Subject: [PATCH 07/17] fix prepare statement mysqli --- system/Database/MySQLi/PreparedQuery.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index e9a6c6d10e30..34eedc25ff15 100644 --- a/system/Database/MySQLi/PreparedQuery.php +++ b/system/Database/MySQLi/PreparedQuery.php @@ -66,15 +66,19 @@ public function _execute(array $data): bool throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); } - // First off -bind the parameters - $bindTypes = ''; + // First off - bind the parameters + $bindTypes = ''; + $binaryData = []; // Determine the type string - foreach ($data as $item) { + foreach ($data as $key => $item) { if (is_int($item)) { $bindTypes .= 'i'; } elseif (is_numeric($item)) { $bindTypes .= 'd'; + } elseif (is_string($item) && $this->isBinary($item)) { + $bindTypes .= 'b'; + $binaryData[$key] = $item; } else { $bindTypes .= 's'; } @@ -83,6 +87,11 @@ public function _execute(array $data): bool // Bind it $this->statement->bind_param($bindTypes, ...$data); + // Stream binary data + foreach ($binaryData as $key => $value) { + $this->statement->send_long_data($key, $value); + } + try { return $this->statement->execute(); } catch (mysqli_sql_exception $e) { From 11212599c68bcc7fe26bac8155010a71876335dc Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 22 Dec 2024 14:57:17 +0100 Subject: [PATCH 08/17] fix prepare statement oci8 --- system/Database/OCI8/PreparedQuery.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php index 97825c535634..0ce4ab1b277a 100644 --- a/system/Database/OCI8/PreparedQuery.php +++ b/system/Database/OCI8/PreparedQuery.php @@ -73,30 +73,34 @@ public function _execute(array $data): bool throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); } - $blobs = []; + $binaryData = []; foreach ($data as $key => $item) { - if (is_string($item) && mb_detect_encoding($item, 'UTF-8', true) === false) { - $blobs[$key] = oci_new_descriptor($this->db->connID, OCI_D_LOB); - oci_bind_by_name($this->statement, ':' . $key, $blobs[$key], -1, OCI_B_BLOB); + if (is_string($item) && $this->isBinary($item)) { + $binaryData[$key] = oci_new_descriptor($this->db->connID, OCI_D_LOB); + oci_bind_by_name($this->statement, ':' . $key, $binaryData[$key], -1, OCI_B_BLOB); } else { oci_bind_by_name($this->statement, ':' . $key, $item); } } - $result = oci_execute($this->statement, $blobs === [] ? $this->db->commitMode : OCI_NO_AUTO_COMMIT); + $result = oci_execute( + $this->statement, + $binaryData === [] ? $this->db->commitMode : OCI_NO_AUTO_COMMIT + ); - if ($blobs !== []) { - foreach ($blobs as $key => $blob) { + if ($binaryData !== []) { + foreach ($binaryData as $key => $blob) { if (! $blob->save($data[$key])) { oci_rollback($this->db->connID); + return false; } } oci_commit($this->db->connID); - foreach ($blobs as $blob) { + foreach ($binaryData as $blob) { $blob->free(); } } From 55a27d10502def6c0c20b08132a8463ff3ceedc3 Mon Sep 17 00:00:00 2001 From: michalsn Date: Sun, 22 Dec 2024 14:57:37 +0100 Subject: [PATCH 09/17] sqlsrv blob support --- system/Database/SQLSRV/Forge.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index d121560c9d47..fff528d3f12f 100644 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -397,6 +397,11 @@ protected function _attributeType(array &$attributes) $attributes['TYPE'] = 'BIT'; break; + case 'BLOB': + $attributes['TYPE'] = 'VARBINARY'; + $attributes['CONSTRAINT'] ??= 'MAX'; + break; + default: break; } From eeadef2b59d64523a3629f25abf63a52dc05a615 Mon Sep 17 00:00:00 2001 From: michalsn Date: Mon, 23 Dec 2024 09:46:17 +0100 Subject: [PATCH 10/17] fix tests --- system/Database/BasePreparedQuery.php | 2 +- system/Database/SQLSRV/Connection.php | 6 +++++- system/Database/SQLSRV/Forge.php | 2 +- .../Migrations/20160428212500_Create_test_tables.php | 2 +- .../Database/Live/AbstractGetFieldDataTestCase.php | 10 ++++------ .../Database/Live/Postgre/GetFieldDataTestCase.php | 7 +++++++ .../Database/Live/SQLSRV/GetFieldDataTestCase.php | 7 +++++++ 7 files changed, 26 insertions(+), 10 deletions(-) diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index fd84535bc2d5..6ba2f6eb5a43 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -263,7 +263,7 @@ public function getErrorMessage(): string /** * Whether the input contain binary data. */ - protected function isBinary($input): bool + protected function isBinary(string $input): bool { return mb_detect_encoding($input, 'UTF-8', true) === false; } diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 7d60ad1cabd1..8ded4cd02098 100644 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -368,7 +368,11 @@ protected function _fieldData(string $table): array $retVal[$i]->max_length = $query[$i]->CHARACTER_MAXIMUM_LENGTH > 0 ? $query[$i]->CHARACTER_MAXIMUM_LENGTH - : $query[$i]->NUMERIC_PRECISION; + : ( + $query[$i]->CHARACTER_MAXIMUM_LENGTH === -1 + ? 'max' + : $query[$i]->NUMERIC_PRECISION + ); $retVal[$i]->nullable = $query[$i]->IS_NULLABLE !== 'NO'; $retVal[$i]->default = $query[$i]->COLUMN_DEFAULT; diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index fff528d3f12f..df3d55c779c4 100644 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -398,7 +398,7 @@ protected function _attributeType(array &$attributes) break; case 'BLOB': - $attributes['TYPE'] = 'VARBINARY'; + $attributes['TYPE'] = 'VARBINARY'; $attributes['CONSTRAINT'] ??= 'MAX'; break; diff --git a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php index 3753fb49ffa7..434644bef280 100644 --- a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php +++ b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php @@ -91,7 +91,7 @@ public function up(): void } if ($this->db->DBDriver === 'SQLSRV') { - unset($dataTypeFields['type_timestamp'], $dataTypeFields['type_blob']); + unset($dataTypeFields['type_timestamp']); $dataTypeFields['type_text'] = ['type' => 'NVARCHAR(max)', 'null' => true]; } diff --git a/tests/system/Database/Live/AbstractGetFieldDataTestCase.php b/tests/system/Database/Live/AbstractGetFieldDataTestCase.php index 60413acc784f..011564992922 100644 --- a/tests/system/Database/Live/AbstractGetFieldDataTestCase.php +++ b/tests/system/Database/Live/AbstractGetFieldDataTestCase.php @@ -104,8 +104,8 @@ protected function createTableForType(): void $this->forge->dropTable($this->table, true); // missing types: - // TINYINT,MEDIUMINT,BIT,YEAR,BINARY,VARBINARY,TINYTEXT,LONGTEXT, - // JSON,Spatial data types + // TINYINT,MEDIUMINT,BIT,YEAR,BINARY,VARBINARY (BLOB more or less handles these two), + // TINYTEXT,LONGTEXT,JSON,Spatial data types // `id` must be INTEGER else SQLite3 error on not null for autoincrement field. $fields = [ 'id' => ['type' => 'INTEGER', 'constraint' => 20, 'auto_increment' => true], @@ -138,8 +138,7 @@ protected function createTableForType(): void $fields['type_enum'], $fields['type_set'], $fields['type_mediumtext'], - $fields['type_double'], - $fields['type_blob'] + $fields['type_double'] ); } @@ -147,8 +146,7 @@ protected function createTableForType(): void unset( $fields['type_set'], $fields['type_mediumtext'], - $fields['type_double'], - $fields['type_blob'] + $fields['type_double'] ); } diff --git a/tests/system/Database/Live/Postgre/GetFieldDataTestCase.php b/tests/system/Database/Live/Postgre/GetFieldDataTestCase.php index 42e823397926..0adfc036c4c3 100644 --- a/tests/system/Database/Live/Postgre/GetFieldDataTestCase.php +++ b/tests/system/Database/Live/Postgre/GetFieldDataTestCase.php @@ -212,6 +212,13 @@ public function testGetFieldDataType(): void 'default' => null, ], 15 => (object) [ + 'name' => 'type_blob', + 'type' => 'bytea', + 'max_length' => null, + 'nullable' => true, + 'default' => null, + ], + 16 => (object) [ 'name' => 'type_boolean', 'type' => 'boolean', 'max_length' => null, diff --git a/tests/system/Database/Live/SQLSRV/GetFieldDataTestCase.php b/tests/system/Database/Live/SQLSRV/GetFieldDataTestCase.php index bf00175c4fab..47aa8d108972 100644 --- a/tests/system/Database/Live/SQLSRV/GetFieldDataTestCase.php +++ b/tests/system/Database/Live/SQLSRV/GetFieldDataTestCase.php @@ -219,6 +219,13 @@ public function testGetFieldDataType(): void 'default' => null, ], 16 => (object) [ + 'name' => 'type_blob', + 'type' => 'varbinary', + 'max_length' => 'max', + 'nullable' => true, + 'default' => null, + ], + 17 => (object) [ 'name' => 'type_boolean', 'type' => 'bit', 'max_length' => null, From ee4e2e4ac3990e0048ba0e8a765516fec3026a46 Mon Sep 17 00:00:00 2001 From: michalsn Date: Mon, 23 Dec 2024 09:48:30 +0100 Subject: [PATCH 11/17] add changelog --- user_guide_src/source/changelogs/v4.5.6.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.5.6.rst b/user_guide_src/source/changelogs/v4.5.6.rst index 569c5a542ecd..bf751a686135 100644 --- a/user_guide_src/source/changelogs/v4.5.6.rst +++ b/user_guide_src/source/changelogs/v4.5.6.rst @@ -41,7 +41,8 @@ Bugs Fixed - **Validation:** Fixed a bug where complex language strings were not properly handled. - **CURLRequest:** Added support for handling proxy responses using HTTP versions other than 1.1. - **Database:** Fixed a bug that caused ``Postgre\Connection::reconnect()`` method to throw an error when the connection had not yet been established. -- **Model** Fixed a bug that caused the ``Model::getIdValue()`` method to not correctly recognize the primary key in the ``Entity`` object if a data mapping for the primary key was used. +- **Model:** Fixed a bug that caused the ``Model::getIdValue()`` method to not correctly recognize the primary key in the ``Entity`` object if a data mapping for the primary key was used. +- **Database:** Fixed a bug in prepared statement to correctly handle binary data. See the repo's `CHANGELOG.md `_ From e344fb811f73ea5caf44110129359f47b1cd9e6f Mon Sep 17 00:00:00 2001 From: michalsn Date: Mon, 23 Dec 2024 12:10:21 +0100 Subject: [PATCH 12/17] fix rector --- system/Database/SQLSRV/PreparedQuery.php | 2 ++ tests/system/Database/Live/PreparedQueryTest.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/system/Database/SQLSRV/PreparedQuery.php b/system/Database/SQLSRV/PreparedQuery.php index 3013e31c185b..5e9c28913ad2 100644 --- a/system/Database/SQLSRV/PreparedQuery.php +++ b/system/Database/SQLSRV/PreparedQuery.php @@ -120,6 +120,8 @@ protected function _close(): bool /** * Handle parameters. + * + * @param array $options */ protected function parameterize(string $queryString, array $options): array { diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index d2205f06d3a5..5513b2ee6bd2 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -284,7 +284,7 @@ public function testInsertBinaryData(): void $fileContent = file_get_contents(TESTPATH . '_support/Images/EXIFsamples/landscape_0.jpg'); $this->assertTrue($this->query->execute($fileContent)); - $id = $this->db->insertId(); + $id = $this->db->insertID(); $builder = $this->db->table('type_test'); if ($this->db->DBDriver === 'Postgre') { From 24c242235d2f50426f61a7c41d6a2291dba0d855 Mon Sep 17 00:00:00 2001 From: michalsn Date: Mon, 23 Dec 2024 13:58:57 +0100 Subject: [PATCH 13/17] make sqlsrv happy --- system/Database/SQLSRV/PreparedQuery.php | 6 +++++- tests/system/Database/Live/PreparedQueryTest.php | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/system/Database/SQLSRV/PreparedQuery.php b/system/Database/SQLSRV/PreparedQuery.php index 5e9c28913ad2..425555927c60 100644 --- a/system/Database/SQLSRV/PreparedQuery.php +++ b/system/Database/SQLSRV/PreparedQuery.php @@ -131,7 +131,11 @@ protected function parameterize(string $queryString, array $options): array for ($c = 0; $c < $numberOfVariables; $c++) { $this->parameters[$c] = null; - $params[] = [&$this->parameters[$c], SQLSRV_PARAM_IN, $options[$c] ?? SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)]; + if (isset($options[$c])) { + $params[] = [&$this->parameters[$c], SQLSRV_PARAM_IN, $options[$c]]; + } else { + $params[] = &$this->parameters[$c]; + } } return $params; diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index 5513b2ee6bd2..80e0addbca82 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -284,7 +284,11 @@ public function testInsertBinaryData(): void $fileContent = file_get_contents(TESTPATH . '_support/Images/EXIFsamples/landscape_0.jpg'); $this->assertTrue($this->query->execute($fileContent)); - $id = $this->db->insertID(); + $id = $this->db->DBDriver === 'SQLSRV' + // It seems like INSERT for a prepared statement is run in the + // separate execution context even though it's part of the same session + ? (int) ($this->db->query('SELECT @@IDENTITY AS insert_id')->getRow()->insert_id ?? 0) + : $this->db->insertID(); $builder = $this->db->table('type_test'); if ($this->db->DBDriver === 'Postgre') { From ab919366a3e2d3ccb1f768b07ff19d93283a5b67 Mon Sep 17 00:00:00 2001 From: michalsn Date: Mon, 23 Dec 2024 17:58:55 +0100 Subject: [PATCH 14/17] make oci8 happy - hopefully --- system/Database/OCI8/PreparedQuery.php | 34 +++++++------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php index 0ce4ab1b277a..cc436e272fad 100644 --- a/system/Database/OCI8/PreparedQuery.php +++ b/system/Database/OCI8/PreparedQuery.php @@ -73,36 +73,20 @@ public function _execute(array $data): bool throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); } - $binaryData = []; - - foreach ($data as $key => $item) { - if (is_string($item) && $this->isBinary($item)) { - $binaryData[$key] = oci_new_descriptor($this->db->connID, OCI_D_LOB); - oci_bind_by_name($this->statement, ':' . $key, $binaryData[$key], -1, OCI_B_BLOB); + foreach (array_keys($data) as $key) { + if (is_string($data[$key]) && $this->isBinary($data[$key])) { + $binaryData = oci_new_descriptor($this->db->connID, OCI_D_LOB); + $binaryData->writeTemporary($data[$key], OCI_TEMP_BLOB); + oci_bind_by_name($this->statement, ':' . $key, $binaryData, -1, OCI_B_BLOB); } else { - oci_bind_by_name($this->statement, ':' . $key, $item); + oci_bind_by_name($this->statement, ':' . $key, $data[$key]); } } - $result = oci_execute( - $this->statement, - $binaryData === [] ? $this->db->commitMode : OCI_NO_AUTO_COMMIT - ); - - if ($binaryData !== []) { - foreach ($binaryData as $key => $blob) { - if (! $blob->save($data[$key])) { - oci_rollback($this->db->connID); - - return false; - } - } - - oci_commit($this->db->connID); + $result = oci_execute($this->statement, $this->db->commitMode); - foreach ($binaryData as $blob) { - $blob->free(); - } + if (isset($binaryData)) { + $binaryData->free(); } if ($result && $this->lastInsertTableName !== '') { From 96158e8392474338719f5504e707f03b7f2d83a3 Mon Sep 17 00:00:00 2001 From: michalsn Date: Mon, 23 Dec 2024 18:12:24 +0100 Subject: [PATCH 15/17] add a note about options for prepared statement --- user_guide_src/source/database/queries.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/database/queries.rst b/user_guide_src/source/database/queries.rst index b4cc0f978ff9..4a07b4ba8295 100644 --- a/user_guide_src/source/database/queries.rst +++ b/user_guide_src/source/database/queries.rst @@ -246,6 +246,8 @@ array through in the second parameter: .. literalinclude:: queries/018.php +.. note:: Currently, the only database that actually uses the array of option is SQLSRV. + Executing the Query =================== From 11fc75e449bc336f6723698527c95c41632ec9c1 Mon Sep 17 00:00:00 2001 From: michalsn Date: Tue, 24 Dec 2024 08:21:49 +0100 Subject: [PATCH 16/17] ignore PreparedQueryTest.php file --- .php-cs-fixer.tests.php | 1 + 1 file changed, 1 insertion(+) diff --git a/.php-cs-fixer.tests.php b/.php-cs-fixer.tests.php index 28a7124909e5..bf37256da58a 100644 --- a/.php-cs-fixer.tests.php +++ b/.php-cs-fixer.tests.php @@ -26,6 +26,7 @@ '_support/View/Cells/multiplier.php', '_support/View/Cells/colors.php', '_support/View/Cells/addition.php', + 'system/Database/Live/PreparedQueryTest.php', ]) ->notName('#Foobar.php$#'); From 8e7d17dff3977c78a11bfd42e6650cb194c432c6 Mon Sep 17 00:00:00 2001 From: michalsn Date: Thu, 26 Dec 2024 10:10:56 +0100 Subject: [PATCH 17/17] apply code suggestion for oci8 --- system/Database/OCI8/PreparedQuery.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php index cc436e272fad..e1577642c3a9 100644 --- a/system/Database/OCI8/PreparedQuery.php +++ b/system/Database/OCI8/PreparedQuery.php @@ -16,6 +16,7 @@ use BadMethodCallException; use CodeIgniter\Database\BasePreparedQuery; use CodeIgniter\Database\Exceptions\DatabaseException; +use OCILob; /** * Prepared query for OCI8 @@ -73,6 +74,8 @@ public function _execute(array $data): bool throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); } + $binaryData = null; + foreach (array_keys($data) as $key) { if (is_string($data[$key]) && $this->isBinary($data[$key])) { $binaryData = oci_new_descriptor($this->db->connID, OCI_D_LOB); @@ -85,7 +88,7 @@ public function _execute(array $data): bool $result = oci_execute($this->statement, $this->db->commitMode); - if (isset($binaryData)) { + if ($binaryData instanceof OCILob) { $binaryData->free(); }