diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index b90ffc7b305..249a050542e 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -25,4 +25,4 @@ on: jobs: coding-standards: name: "Coding Standards" - uses: "doctrine/.github/.github/workflows/coding-standards.yml@3.0.0" + uses: "doctrine/.github/.github/workflows/coding-standards.yml@3.1.0" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 616ca0c795c..78b6cf628d4 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -56,7 +56,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" with: fetch-depth: 2 @@ -122,7 +122,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" with: fetch-depth: 2 @@ -178,7 +178,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" with: fetch-depth: 2 @@ -216,15 +216,16 @@ jobs: postgres-version: - "10" - "15" + - "16" extension: - "pgsql" - "pdo_pgsql" include: - php-version: "8.2" - postgres-version: "15" + postgres-version: "16" extension: "pgsql" - php-version: "8.3" - postgres-version: "15" + postgres-version: "16" extension: "pdo_pgsql" services: @@ -241,7 +242,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" with: fetch-depth: 2 @@ -310,7 +311,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" with: fetch-depth: 2 @@ -381,7 +382,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" with: fetch-depth: 2 @@ -451,7 +452,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" with: fetch-depth: 2 @@ -511,7 +512,7 @@ jobs: run: "docker exec ${{ job.services.ibm_db2.id }} su - db2inst1 -c 'db2 -t CONNECT TO doctrine; db2 -t CREATE USER TEMPORARY TABLESPACE doctrine_tbsp PAGESIZE 4 K;'" - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" with: fetch-depth: 2 @@ -557,7 +558,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -590,7 +591,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" with: fetch-depth: 2 diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml index c02383e0257..4c1e99c4c7c 100644 --- a/.github/workflows/release-on-milestone-closed.yml +++ b/.github/workflows/release-on-milestone-closed.yml @@ -12,7 +12,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: "Release" uses: "laminas/automatic-releases@1.24.0" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 409a24d2e2e..9b1999f3f88 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: "Checkout code" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -64,7 +64,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2555ebbd46..31b6eff1f10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,6 @@ +This repository has [guidelines specific to testing][testing guidelines], and Doctrine has [general contributing guidelines][contributor workflow], make -sure you follow them. +sure you follow both. [contributor workflow]: https://www.doctrine-project.org/contribute/index.html +[testing guidelines]: https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/testing.html diff --git a/composer.json b/composer.json index afc25e3a47f..58306ca4c45 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.2", - "phpstan/phpstan": "1.10.32", + "phpstan/phpstan": "1.10.34", "phpstan/phpstan-phpunit": "1.3.14", "phpstan/phpstan-strict-rules": "^1.5", "phpunit/phpunit": "10.2.2", diff --git a/docs/en/reference/configuration.rst b/docs/en/reference/configuration.rst index c159fd087b5..7d6bc9662a9 100644 --- a/docs/en/reference/configuration.rst +++ b/docs/en/reference/configuration.rst @@ -330,6 +330,7 @@ pdo_sqlsrv / sqlsrv - ``host`` (string): Hostname of the database to connect to. - ``port`` (integer): Port of the database to connect to. - ``dbname`` (string): Name of the database/schema to connect to. +- ``driverOptions`` (array): Any supported options found on `https://learn.microsoft.com/en-us/sql/connect/php/connection-options` ibm_db2 ^^^^^^^ diff --git a/docs/en/reference/data-retrieval-and-manipulation.rst b/docs/en/reference/data-retrieval-and-manipulation.rst index 32f85511526..6e011a32092 100644 --- a/docs/en/reference/data-retrieval-and-manipulation.rst +++ b/docs/en/reference/data-retrieval-and-manipulation.rst @@ -253,10 +253,12 @@ SQL injection possibilities if not handled carefully. Doctrine DBAL implements a very powerful parsing process that will make this kind of prepared statement possible natively in the binding type system. The parsing necessarily comes with a performance overhead, but only if you really use a list of parameters. -There are two special binding types that describe a list of integers or strings: +There are four special binding types that describe a list of integers, regular, ascii or binary strings: - ``\Doctrine\DBAL\ArrayParameterType::INTEGER`` - ``\Doctrine\DBAL\ArrayParameterType::STRING`` +- ``\Doctrine\DBAL\ArrayParameterType::ASCII`` +- ``\Doctrine\DBAL\ArrayParameterType::BINARY`` Using one of these constants as a type you can activate the SQLParser inside Doctrine that rewrites the SQL and flattens the specified values into the set of parameters. Consider our previous example: @@ -538,4 +540,4 @@ given data. update('user', ['username' => 'jwage'], ['id' => 1]); - // UPDATE user (username) VALUES (?) WHERE id = ? (jwage, 1) + // UPDATE user SET username = ? WHERE id = ? (jwage, 1) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4230630f7b7..0e2bf6c434c 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -100,6 +100,11 @@ parameters: # Ignore the possible false return value of db2_num_rows(). - '~^Method Doctrine\\DBAL\\Driver\\IBMDB2\\Connection\:\:exec\(\) should return int but returns int<0, max>\|false\.$~' - '~^Method Doctrine\\DBAL\\Driver\\IBMDB2\\Result\:\:rowCount\(\) should return int but returns int<0, max>\|false\.$~' + + # TODO + - + message: '~^Property Doctrine\\DBAL\\Platforms\\AbstractPlatform\:\:\$disableTypeComments is never read, only written\.$~' + path: src/Platforms/AbstractPlatform.php includes: - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon diff --git a/psalm.xml.dist b/psalm.xml.dist index b8299e81ca1..fcc412e3d24 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -173,6 +173,9 @@ + + + diff --git a/src/ArrayParameterType.php b/src/ArrayParameterType.php index 6e3877746d2..851d47d1871 100644 --- a/src/ArrayParameterType.php +++ b/src/ArrayParameterType.php @@ -21,6 +21,11 @@ enum ArrayParameterType */ case ASCII; + /** + * Represents an array of ascii strings to be expanded by Doctrine SQL parsing. + */ + case BINARY; + /** @internal */ public static function toElementParameterType(self $type): ParameterType { @@ -28,6 +33,7 @@ public static function toElementParameterType(self $type): ParameterType self::INTEGER => ParameterType::INTEGER, self::STRING => ParameterType::STRING, self::ASCII => ParameterType::ASCII, + self::BINARY => ParameterType::BINARY, }; } } diff --git a/src/Configuration.php b/src/Configuration.php index 826835b3a24..7c50ce83e30 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -33,6 +33,14 @@ class Configuration */ protected bool $autoCommit = true; + /** + * Whether type comments should be disabled to provide the same DB schema than + * will be obtained with DBAL 4.x. This is useful when relying only on the + * platform-aware schema comparison (which does not need those type comments) + * rather than the deprecated legacy tooling. + */ + private bool $disableTypeComments = false; + private ?SchemaManagerFactory $schemaManagerFactory = null; public function __construct() @@ -132,4 +140,17 @@ public function setSchemaManagerFactory(SchemaManagerFactory $schemaManagerFacto return $this; } + + public function getDisableTypeComments(): bool + { + return $this->disableTypeComments; + } + + /** @return $this */ + public function setDisableTypeComments(bool $disableTypeComments): self + { + $this->disableTypeComments = $disableTypeComments; + + return $this; + } } diff --git a/src/Connection.php b/src/Connection.php index bd788c86af4..52bdf9a4a62 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -188,6 +188,7 @@ public function getDatabasePlatform(): AbstractPlatform } $this->platform = $this->driver->getDatabasePlatform($versionProvider); + $this->platform->setDisableTypeComments($this->_config->getDisableTypeComments()); } return $this->platform; diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php index 8077dd7c9c6..0eb80c83adf 100644 --- a/src/Platforms/AbstractPlatform.php +++ b/src/Platforms/AbstractPlatform.php @@ -79,6 +79,14 @@ abstract class AbstractPlatform */ protected ?KeywordList $_keywords = null; + private bool $disableTypeComments = false; + + /** @internal */ + final public function setDisableTypeComments(bool $value): void + { + $this->disableTypeComments = $value; + } + /** * Returns the SQL snippet that declares a boolean column. * diff --git a/tests/Connection/ExpandArrayParametersTest.php b/tests/Connection/ExpandArrayParametersTest.php index 27e7c330c72..36868f6d051 100644 --- a/tests/Connection/ExpandArrayParametersTest.php +++ b/tests/Connection/ExpandArrayParametersTest.php @@ -14,6 +14,8 @@ use Doctrine\DBAL\Types\Type; use PHPUnit\Framework\TestCase; +use function hex2bin; + /** @psalm-import-type WrapperParameterTypeArray from Connection */ class ExpandArrayParametersTest extends TestCase { @@ -107,16 +109,24 @@ public static function dataExpandListParameters(): iterable [1 => ParameterType::STRING, 2 => ParameterType::STRING], ], 'Positional: explicit keys for array params and array types' => [ - 'SELECT * FROM Foo WHERE foo IN (?) AND bar IN (?) AND baz = ? AND bax IN (?)', - [1 => ['bar1', 'bar2'], 2 => true, 0 => [1, 2, 3], ['bax1', 'bax2']], + 'SELECT * FROM Foo WHERE foo IN (?) AND bar IN (?) AND baz = ? AND bax IN (?) AND bay IN (?)', + [ + 1 => ['bar1', 'bar2'], + 2 => true, + 0 => [1, 2, 3], + ['bax1', 'bax2'], + 4 => [hex2bin('DEADBEEF'), hex2bin('C0DEF00D')], + ], [ + 4 => ArrayParameterType::BINARY, 3 => ArrayParameterType::ASCII, 2 => ParameterType::BOOLEAN, 1 => ArrayParameterType::STRING, 0 => ArrayParameterType::INTEGER, ], - 'SELECT * FROM Foo WHERE foo IN (?, ?, ?) AND bar IN (?, ?) AND baz = ? AND bax IN (?, ?)', - [1, 2, 3, 'bar1', 'bar2', true, 'bax1', 'bax2'], + 'SELECT * FROM Foo WHERE foo IN (?, ?, ?) AND bar IN (?, ?) AND baz = ? AND bax IN (?, ?) ' . + 'AND bay IN (?, ?)', + [1, 2, 3, 'bar1', 'bar2', true, 'bax1', 'bax2', hex2bin('DEADBEEF'), hex2bin('C0DEF00D')], [ ParameterType::INTEGER, ParameterType::INTEGER, @@ -126,6 +136,8 @@ public static function dataExpandListParameters(): iterable ParameterType::BOOLEAN, ParameterType::ASCII, ParameterType::ASCII, + ParameterType::BINARY, + ParameterType::BINARY, ], ], 'Named: Very simple with param int' => [ @@ -323,6 +335,22 @@ public static function dataExpandListParameters(): iterable ['foo', 'bar', 'baz'], [1 => ParameterType::STRING, ParameterType::STRING], ], + 'Named: Binary array with explicit types' => [ + 'SELECT * FROM Foo WHERE foo IN (:foo) OR bar IN (:bar)', + [ + 'foo' => [hex2bin('DEADBEEF'), hex2bin('C0DEF00D')], + 'bar' => [hex2bin('DEADBEEF'), hex2bin('C0DEF00D')], + ], + ['foo' => ArrayParameterType::BINARY, 'bar' => ArrayParameterType::BINARY], + 'SELECT * FROM Foo WHERE foo IN (?, ?) OR bar IN (?, ?)', + [hex2bin('DEADBEEF'), hex2bin('C0DEF00D'), hex2bin('DEADBEEF'), hex2bin('C0DEF00D')], + [ + ParameterType::BINARY, + ParameterType::BINARY, + ParameterType::BINARY, + ParameterType::BINARY, + ], + ], ]; } diff --git a/tests/Functional/BinaryDataAccessTest.php b/tests/Functional/BinaryDataAccessTest.php new file mode 100644 index 00000000000..50be983860f --- /dev/null +++ b/tests/Functional/BinaryDataAccessTest.php @@ -0,0 +1,354 @@ +addColumn('test_int', 'integer'); + $table->addColumn('test_binary', 'binary', ['notnull' => false, 'length' => 4]); + $table->setPrimaryKey(['test_int']); + + $this->dropAndCreateTable($table); + + $this->connection->insert('binary_fetch_table', [ + 'test_int' => 1, + 'test_binary' => hex2bin('C0DEF00D'), + ], [ + 'test_binary' => ParameterType::BINARY, + ]); + } + + public function testPrepareWithBindValue(): void + { + $sql = 'SELECT test_int, test_binary FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $stmt = $this->connection->prepare($sql); + + $stmt->bindValue(1, 1); + $stmt->bindValue(2, hex2bin('C0DEF00D'), ParameterType::BINARY); + + $row = $stmt->executeQuery()->fetchAssociative(); + + self::assertIsArray($row); + $row = array_change_key_case($row, CASE_LOWER); + self::assertEquals(['test_int', 'test_binary'], array_keys($row)); + self::assertEquals(1, $row['test_int']); + + $binaryResult = $row['test_binary']; + if (is_resource($binaryResult)) { + $binaryResult = stream_get_contents($binaryResult); + } + + self::assertEquals(hex2bin('C0DEF00D'), $binaryResult); + } + + public function testPrepareWithFetchAllAssociative(): void + { + $paramInt = 1; + $paramBin = hex2bin('C0DEF00D'); + + $sql = 'SELECT test_int, test_binary FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $stmt = $this->connection->prepare($sql); + + $stmt->bindValue(1, $paramInt); + $stmt->bindValue(2, $paramBin, ParameterType::BINARY); + + $rows = $stmt->executeQuery()->fetchAllAssociative(); + $rows[0] = array_change_key_case($rows[0], CASE_LOWER); + + self::assertEquals(['test_int', 'test_binary'], array_keys($rows[0])); + self::assertEquals(1, $rows[0]['test_int']); + + $binaryResult = $rows[0]['test_binary']; + if (is_resource($binaryResult)) { + $binaryResult = stream_get_contents($binaryResult); + } + + self::assertEquals(hex2bin('C0DEF00D'), $binaryResult); + } + + public function testPrepareWithFetchOne(): void + { + $paramInt = 1; + $paramBin = hex2bin('C0DEF00D'); + + $sql = 'SELECT test_int FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $stmt = $this->connection->prepare($sql); + + $stmt->bindValue(1, $paramInt); + $stmt->bindValue(2, $paramBin, ParameterType::BINARY); + + $column = $stmt->executeQuery()->fetchOne(); + self::assertEquals(1, $column); + } + + public function testFetchAllAssociative(): void + { + $sql = 'SELECT test_int, test_binary FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $data = $this->connection->fetchAllAssociative($sql, [1, hex2bin('C0DEF00D')], [1 => ParameterType::BINARY]); + + self::assertCount(1, $data); + + $row = $data[0]; + self::assertCount(2, $row); + + $row = array_change_key_case($row, CASE_LOWER); + self::assertEquals(1, $row['test_int']); + + $binaryResult = $row['test_binary']; + if (is_resource($binaryResult)) { + $binaryResult = stream_get_contents($binaryResult); + } + + self::assertEquals(hex2bin('C0DEF00D'), $binaryResult); + } + + public function testFetchAllWithTypes(): void + { + $sql = 'SELECT test_int, test_binary FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $data = $this->connection->fetchAllAssociative( + $sql, + [1, hex2bin('C0DEF00D')], + [ParameterType::STRING, Types::BINARY], + ); + + self::assertCount(1, $data); + + $row = $data[0]; + self::assertCount(2, $row); + + $row = array_change_key_case($row, CASE_LOWER); + self::assertEquals(1, $row['test_int']); + + $binaryResult = $row['test_binary']; + if (is_resource($binaryResult)) { + $binaryResult = stream_get_contents($binaryResult); + } + + self::assertEquals(hex2bin('C0DEF00D'), $binaryResult); + } + + public function testFetchAssociative(): void + { + $sql = 'SELECT test_int, test_binary FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $row = $this->connection->fetchAssociative($sql, [1, hex2bin('C0DEF00D')], [1 => ParameterType::BINARY]); + + self::assertNotFalse($row); + + $row = array_change_key_case($row, CASE_LOWER); + + self::assertEquals(1, $row['test_int']); + + $binaryResult = $row['test_binary']; + if (is_resource($binaryResult)) { + $binaryResult = stream_get_contents($binaryResult); + } + + self::assertEquals(hex2bin('C0DEF00D'), $binaryResult); + } + + public function testFetchAssocWithTypes(): void + { + $sql = 'SELECT test_int, test_binary FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $row = $this->connection->fetchAssociative( + $sql, + [1, hex2bin('C0DEF00D')], + [ParameterType::STRING, Types::BINARY], + ); + + self::assertNotFalse($row); + + $row = array_change_key_case($row, CASE_LOWER); + + self::assertEquals(1, $row['test_int']); + + $binaryResult = $row['test_binary']; + if (is_resource($binaryResult)) { + $binaryResult = stream_get_contents($binaryResult); + } + + self::assertEquals(hex2bin('C0DEF00D'), $binaryResult); + } + + public function testFetchArray(): void + { + $sql = 'SELECT test_int, test_binary FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $row = $this->connection->fetchNumeric($sql, [1, hex2bin('C0DEF00D')], [1 => ParameterType::BINARY]); + self::assertNotFalse($row); + + self::assertEquals(1, $row[0]); + + $binaryResult = $row[1]; + if (is_resource($binaryResult)) { + $binaryResult = stream_get_contents($binaryResult); + } + + self::assertEquals(hex2bin('C0DEF00D'), $binaryResult); + } + + public function testFetchArrayWithTypes(): void + { + $sql = 'SELECT test_int, test_binary FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $row = $this->connection->fetchNumeric( + $sql, + [1, hex2bin('C0DEF00D')], + [ParameterType::STRING, Types::BINARY], + ); + + self::assertNotFalse($row); + + $row = array_change_key_case($row, CASE_LOWER); + + self::assertEquals(1, $row[0]); + + $binaryResult = $row[1]; + if (is_resource($binaryResult)) { + $binaryResult = stream_get_contents($binaryResult); + } + + self::assertEquals(hex2bin('C0DEF00D'), $binaryResult); + } + + public function testFetchColumn(): void + { + $sql = 'SELECT test_int FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $testInt = $this->connection->fetchOne($sql, [1, hex2bin('C0DEF00D')], [1 => ParameterType::BINARY]); + + self::assertEquals(1, $testInt); + + $sql = 'SELECT test_binary FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $testBinary = $this->connection->fetchOne($sql, [1, hex2bin('C0DEF00D')], [1 => ParameterType::BINARY]); + + if (is_resource($testBinary)) { + $testBinary = stream_get_contents($testBinary); + } + + self::assertEquals(hex2bin('C0DEF00D'), $testBinary); + } + + public function testFetchOneWithTypes(): void + { + $sql = 'SELECT test_binary FROM binary_fetch_table WHERE test_int = ? AND test_binary = ?'; + $column = $this->connection->fetchOne( + $sql, + [1, hex2bin('C0DEF00D')], + [ParameterType::STRING, Types::BINARY], + ); + + if (is_resource($column)) { + $column = stream_get_contents($column); + } + + self::assertIsString($column); + self::assertEquals(hex2bin('C0DEF00D'), $column); + } + + public function testNativeArrayListSupport(): void + { + $binaryValues = [ + hex2bin('A0AEFA'), + hex2bin('1F43BA'), + hex2bin('8C9D2A'), + hex2bin('72E8AA'), + hex2bin('5B6F9A'), + hex2bin('DAB24A'), + hex2bin('3E71CA'), + hex2bin('F0D6EA'), + hex2bin('6A8B5A'), + hex2bin('C582FA'), + ]; + + for ($i = 100; $i < 110; $i++) { + $this->connection->insert('binary_fetch_table', [ + 'test_int' => $i, + 'test_binary' => $binaryValues[$i - 100], + ], [ + 'test_binary' => ParameterType::BINARY, + ]); + } + + $result = $this->connection->executeQuery( + 'SELECT test_int FROM binary_fetch_table WHERE test_int IN (?)', + [[100, 101, 102, 103, 104]], + [ArrayParameterType::INTEGER], + ); + + $data = $result->fetchAllNumeric(); + self::assertCount(5, $data); + self::assertEquals([[100], [101], [102], [103], [104]], $data); + + $result = $this->connection->executeQuery( + 'SELECT test_int FROM binary_fetch_table WHERE test_binary IN (?)', + [ + [ + $binaryValues[0], + $binaryValues[1], + $binaryValues[2], + $binaryValues[3], + $binaryValues[4], + ], + ], + [ArrayParameterType::BINARY], + ); + + $data = $result->fetchAllNumeric(); + self::assertCount(5, $data); + self::assertEquals([[100], [101], [102], [103], [104]], $data); + + $result = $this->connection->executeQuery( + 'SELECT test_binary FROM binary_fetch_table WHERE test_binary IN (?)', + [ + [ + $binaryValues[0], + $binaryValues[1], + $binaryValues[2], + $binaryValues[3], + $binaryValues[4], + ], + ], + [ArrayParameterType::BINARY], + ); + + $data = $result->fetchFirstColumn(); + self::assertCount(5, $data); + + $data = array_map( + static fn ($binaryField) => is_resource($binaryField) + ? stream_get_contents($binaryField) + : $binaryField, + $data, + ); + + self::assertEquals([ + $binaryValues[0], + $binaryValues[1], + $binaryValues[2], + $binaryValues[3], + $binaryValues[4], + ], $data); + } +} diff --git a/tests/Query/QueryBuilderTest.php b/tests/Query/QueryBuilderTest.php index 0153f7f4820..723371ca805 100644 --- a/tests/Query/QueryBuilderTest.php +++ b/tests/Query/QueryBuilderTest.php @@ -16,6 +16,8 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use function hex2bin; + /** @psalm-import-type WrapperParameterTypeArray from Connection */ class QueryBuilderTest extends TestCase { @@ -814,12 +816,17 @@ public function testArrayParameters(): void $qb->andWhere('name IN (:names)'); $qb->setParameter('names', ['john', 'jane'], ArrayParameterType::STRING); + $qb->andWhere('hash IN (:hashes)'); + $qb->setParameter('hashes', [hex2bin('DEADBEEF'), hex2bin('C0DEF00D')], ArrayParameterType::BINARY); + self::assertSame(ArrayParameterType::INTEGER, $qb->getParameterType('ids')); self::assertSame(ArrayParameterType::STRING, $qb->getParameterType('names')); + self::assertSame(ArrayParameterType::BINARY, $qb->getParameterType('hashes')); self::assertSame([ 'ids' => ArrayParameterType::INTEGER, 'names' => ArrayParameterType::STRING, + 'hashes' => ArrayParameterType::BINARY, ], $qb->getParameterTypes()); }