From 7b58fd22bbd90b77dbabc892d28bc7500b5a5189 Mon Sep 17 00:00:00 2001 From: Joe Danis Date: Mon, 14 Sep 2020 10:16:29 -0400 Subject: [PATCH] Support ASCII parameter binding Closes #4263 --- docs/en/reference/types.rst | 10 +++ .../DBAL/Driver/Mysqli/MysqliStatement.php | 1 + .../DBAL/Driver/PDOSqlsrv/Statement.php | 19 +++-- lib/Doctrine/DBAL/Driver/PDOStatement.php | 1 + .../DBAL/Driver/SQLSrv/SQLSrvStatement.php | 9 +++ lib/Doctrine/DBAL/ParameterType.php | 5 ++ .../DBAL/Platforms/AbstractPlatform.php | 11 +++ .../DBAL/Platforms/SQLServerPlatform.php | 14 ++++ lib/Doctrine/DBAL/Types/AsciiStringType.php | 32 +++++++++ lib/Doctrine/DBAL/Types/Type.php | 1 + lib/Doctrine/DBAL/Types/Types.php | 1 + .../Functional/ParameterTypes/AsciiTest.php | 28 ++++++++ .../DBAL/Functional/Types/AsciiStringTest.php | 70 +++++++++++++++++++ .../Platforms/AbstractPlatformTestCase.php | 22 ++++++ .../DBAL/Platforms/OraclePlatformTest.php | 11 +++ .../Tests/DBAL/Types/AsciiStringTest.php | 43 ++++++++++++ 16 files changed, 273 insertions(+), 5 deletions(-) create mode 100644 lib/Doctrine/DBAL/Types/AsciiStringType.php create mode 100644 tests/Doctrine/Tests/DBAL/Functional/ParameterTypes/AsciiTest.php create mode 100644 tests/Doctrine/Tests/DBAL/Functional/Types/AsciiStringTest.php create mode 100644 tests/Doctrine/Tests/DBAL/Types/AsciiStringTest.php diff --git a/docs/en/reference/types.rst b/docs/en/reference/types.rst index f4916a69672..ee9214c20cf 100644 --- a/docs/en/reference/types.rst +++ b/docs/en/reference/types.rst @@ -157,6 +157,13 @@ or ``null`` if no data is present. This can lead to type inconsistencies when reverse engineering the type from the database. +ascii_string +++++++++++++ + +Similar to the ``string`` type but for binding non-unicode data. This type +should be used with database vendors where a binding type mismatch +can trigger an implicit cast and lead to performance problems. + text ++++ @@ -607,6 +614,9 @@ Please also notice the mapping specific footnotes for additional information. | | | | +----------------------------------------------------------+ | | | | | ``NCHAR(n)`` [4]_ | +-------------------+---------------+--------------------------+---------+----------------------------------------------------------+ +| **ascii_string** | ``string`` | **SQL Server** | | ``VARCHAR(n)`` | +| | | | | ``CHAR(n)`` | ++-------------------+---------------+--------------------------+---------+----------------------------------------------------------+ | **text** | ``string`` | **MySQL** | *all* | ``TINYTEXT`` [17]_ | | | | | +----------------------------------------------------------+ | | | | | ``TEXT`` [18]_ | diff --git a/lib/Doctrine/DBAL/Driver/Mysqli/MysqliStatement.php b/lib/Doctrine/DBAL/Driver/Mysqli/MysqliStatement.php index 0efd36b3293..c97f879cf8b 100644 --- a/lib/Doctrine/DBAL/Driver/Mysqli/MysqliStatement.php +++ b/lib/Doctrine/DBAL/Driver/Mysqli/MysqliStatement.php @@ -38,6 +38,7 @@ class MysqliStatement implements IteratorAggregate, StatementInterface, Result { /** @var string[] */ protected static $_paramTypeMap = [ + ParameterType::ASCII => 's', ParameterType::STRING => 's', ParameterType::BINARY => 's', ParameterType::BOOLEAN => 'i', diff --git a/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Statement.php b/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Statement.php index eef6d12c27f..5669ccc270e 100644 --- a/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Statement.php +++ b/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Statement.php @@ -17,11 +17,20 @@ class Statement extends PDO\Statement */ public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null, $driverOptions = null) { - if ( - ($type === ParameterType::LARGE_OBJECT || $type === ParameterType::BINARY) - && $driverOptions === null - ) { - $driverOptions = \PDO::SQLSRV_ENCODING_BINARY; + switch ($type) { + case ParameterType::LARGE_OBJECT: + case ParameterType::BINARY: + if ($driverOptions === null) { + $driverOptions = \PDO::SQLSRV_ENCODING_BINARY; + } + + break; + + case ParameterType::ASCII: + $type = ParameterType::STRING; + $length = 0; + $driverOptions = \PDO::SQLSRV_ENCODING_SYSTEM; + break; } return parent::bindParam($param, $variable, $type, $length, $driverOptions); diff --git a/lib/Doctrine/DBAL/Driver/PDOStatement.php b/lib/Doctrine/DBAL/Driver/PDOStatement.php index 9ef5bf23b43..4a244ab4617 100644 --- a/lib/Doctrine/DBAL/Driver/PDOStatement.php +++ b/lib/Doctrine/DBAL/Driver/PDOStatement.php @@ -30,6 +30,7 @@ class PDOStatement extends \PDOStatement implements StatementInterface, Result ParameterType::NULL => PDO::PARAM_NULL, ParameterType::INTEGER => PDO::PARAM_INT, ParameterType::STRING => PDO::PARAM_STR, + ParameterType::ASCII => PDO::PARAM_STR, ParameterType::BINARY => PDO::PARAM_LOB, ParameterType::LARGE_OBJECT => PDO::PARAM_LOB, ParameterType::BOOLEAN => PDO::PARAM_BOOL, diff --git a/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php b/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php index f2f54e9987d..af98476e1af 100644 --- a/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php +++ b/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php @@ -34,6 +34,7 @@ use function stripos; use const SQLSRV_ENC_BINARY; +use const SQLSRV_ENC_CHAR; use const SQLSRV_ERR_ERRORS; use const SQLSRV_FETCH_ASSOC; use const SQLSRV_FETCH_BOTH; @@ -306,6 +307,14 @@ private function prepare() ]; break; + case ParameterType::ASCII: + $params[$column - 1] = [ + &$variable, + SQLSRV_PARAM_IN, + SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR), + ]; + break; + default: $params[$column - 1] =& $variable; break; diff --git a/lib/Doctrine/DBAL/ParameterType.php b/lib/Doctrine/DBAL/ParameterType.php index 564adfce344..2c4c3ad18c2 100644 --- a/lib/Doctrine/DBAL/ParameterType.php +++ b/lib/Doctrine/DBAL/ParameterType.php @@ -49,6 +49,11 @@ final class ParameterType */ public const BINARY = 16; + /** + * Represents an ASCII string data type + */ + public const ASCII = 17; + /** * This class cannot be instantiated. * diff --git a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php index 61c6a81d742..0f25bd9bf92 100644 --- a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -245,6 +245,17 @@ private function initializeAllDoctrineTypeMappings() } } + /** + * Returns the SQL snippet used to declare a column that can + * store characters in the ASCII character set + * + * @param mixed[] $column + */ + public function getAsciiStringTypeDeclarationSQL(array $column): string + { + return $this->getVarcharTypeDeclarationSQL($column); + } + /** * Returns the SQL snippet used to declare a VARCHAR column type. * diff --git a/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php b/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php index fc551e0fe49..c1da7ab8e83 100644 --- a/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php @@ -1191,6 +1191,20 @@ public function getGuidTypeDeclarationSQL(array $column) return 'UNIQUEIDENTIFIER'; } + /** + * {@inheritDoc} + */ + public function getAsciiStringTypeDeclarationSQL(array $column): string + { + $length = $column['length'] ?? null; + + if (! isset($column['fixed'])) { + return sprintf('VARCHAR(%d)', $length); + } + + return sprintf('CHAR(%d)', $length); + } + /** * {@inheritDoc} */ diff --git a/lib/Doctrine/DBAL/Types/AsciiStringType.php b/lib/Doctrine/DBAL/Types/AsciiStringType.php new file mode 100644 index 00000000000..e7975748678 --- /dev/null +++ b/lib/Doctrine/DBAL/Types/AsciiStringType.php @@ -0,0 +1,32 @@ +getAsciiStringTypeDeclarationSQL($column); + } + + /** + * {@inheritdoc} + */ + public function getBindingType() + { + return ParameterType::ASCII; + } + + public function getName(): string + { + return Types::ASCII_STRING; + } +} diff --git a/lib/Doctrine/DBAL/Types/Type.php b/lib/Doctrine/DBAL/Types/Type.php index f75a4905673..5ff51c4c9bb 100644 --- a/lib/Doctrine/DBAL/Types/Type.php +++ b/lib/Doctrine/DBAL/Types/Type.php @@ -99,6 +99,7 @@ abstract class Type */ private const BUILTIN_TYPES_MAP = [ Types::ARRAY => ArrayType::class, + Types::ASCII_STRING => AsciiStringType::class, Types::BIGINT => BigIntType::class, Types::BINARY => BinaryType::class, Types::BLOB => BlobType::class, diff --git a/lib/Doctrine/DBAL/Types/Types.php b/lib/Doctrine/DBAL/Types/Types.php index 37a3d444bc0..3ca677942c5 100644 --- a/lib/Doctrine/DBAL/Types/Types.php +++ b/lib/Doctrine/DBAL/Types/Types.php @@ -10,6 +10,7 @@ final class Types { public const ARRAY = 'array'; + public const ASCII_STRING = 'ascii_string'; public const BIGINT = 'bigint'; public const BINARY = 'binary'; public const BLOB = 'blob'; diff --git a/tests/Doctrine/Tests/DBAL/Functional/ParameterTypes/AsciiTest.php b/tests/Doctrine/Tests/DBAL/Functional/ParameterTypes/AsciiTest.php new file mode 100644 index 00000000000..940152352d7 --- /dev/null +++ b/tests/Doctrine/Tests/DBAL/Functional/ParameterTypes/AsciiTest.php @@ -0,0 +1,28 @@ +connection->getDriver() instanceof AbstractSQLServerDriver) { + self::markTestSkipped('Driver does not support ascii string binding'); + } + + $statement = $this->connection->prepare('SELECT sql_variant_property(?, \'BaseType\')'); + + $statement->bindValue(1, 'test', ParameterType::ASCII); + $statement->execute(); + + $results = $statement->fetchOne(); + + self::assertEquals('varchar', $results); + } +} diff --git a/tests/Doctrine/Tests/DBAL/Functional/Types/AsciiStringTest.php b/tests/Doctrine/Tests/DBAL/Functional/Types/AsciiStringTest.php new file mode 100644 index 00000000000..d0559c7027a --- /dev/null +++ b/tests/Doctrine/Tests/DBAL/Functional/Types/AsciiStringTest.php @@ -0,0 +1,70 @@ +addColumn('id', 'ascii_string', [ + 'length' => 3, + 'fixed' => true, + ]); + + $table->addColumn('val', 'ascii_string', ['length' => 4]); + $table->setPrimaryKey(['id']); + + $sm = $this->connection->getSchemaManager(); + $sm->dropAndCreateTable($table); + } + + public function testInsertAndSelect(): void + { + $id1 = 'id1'; + $id2 = 'id2'; + + $value1 = 'val1'; + $value2 = 'val2'; + + $this->insert($id1, $value1); + $this->insert($id2, $value2); + + self::assertSame($value1, $this->select($id1)); + self::assertSame($value2, $this->select($id2)); + } + + private function insert(string $id, string $value): void + { + $result = $this->connection->insert('ascii_table', [ + 'id' => $id, + 'val' => $value, + ], [ + ParameterType::ASCII, + ParameterType::ASCII, + ]); + + self::assertSame(1, $result); + } + + private function select(string $id): string + { + $value = $this->connection->fetchOne( + 'SELECT val FROM ascii_table WHERE id = ?', + [$id], + [ParameterType::ASCII] + ); + + self::assertIsString($value); + + return $value; + } +} diff --git a/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php b/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php index b098f617ece..c63eb1a9e2f 100644 --- a/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php +++ b/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php @@ -1443,6 +1443,28 @@ public function testZeroOffsetWithoutLimitIsIgnored(): void $this->platform->modifyLimitQuery($query, null, 0) ); } + + /** + * @param array $column + * + * @dataProvider asciiStringSqlDeclarationDataProvider + */ + public function testAsciiSQLDeclaration(string $expectedSql, array $column): void + { + $declarationSql = $this->platform->getAsciiStringTypeDeclarationSQL($column); + self::assertEquals($expectedSql, $declarationSql); + } + + /** + * @return array}> + */ + public function asciiStringSqlDeclarationDataProvider(): array + { + return [ + ['VARCHAR(12)', ['length' => 12]], + ['CHAR(12)', ['length' => 12, 'fixed' => true]], + ]; + } } interface GetCreateTableSqlDispatchEventListener diff --git a/tests/Doctrine/Tests/DBAL/Platforms/OraclePlatformTest.php b/tests/Doctrine/Tests/DBAL/Platforms/OraclePlatformTest.php index 333a6e26bae..092c073551d 100644 --- a/tests/Doctrine/Tests/DBAL/Platforms/OraclePlatformTest.php +++ b/tests/Doctrine/Tests/DBAL/Platforms/OraclePlatformTest.php @@ -976,4 +976,15 @@ public function testQuotesDatabaseNameInListTableColumnsSQL(): void $this->platform->getListTableColumnsSQL('foo_table', "Foo'Bar\\") ); } + + /** + * @return array}> + */ + public function asciiStringSqlDeclarationDataProvider(): array + { + return [ + ['VARCHAR2(12)', ['length' => 12]], + ['CHAR(12)', ['length' => 12, 'fixed' => true]], + ]; + } } diff --git a/tests/Doctrine/Tests/DBAL/Types/AsciiStringTest.php b/tests/Doctrine/Tests/DBAL/Types/AsciiStringTest.php new file mode 100644 index 00000000000..0e4d5d299f7 --- /dev/null +++ b/tests/Doctrine/Tests/DBAL/Types/AsciiStringTest.php @@ -0,0 +1,43 @@ +type = new AsciiStringType(); + } + + public function testReturnCorrectBindingType(): void + { + self::assertEquals($this->type->getBindingType(), ParameterType::ASCII); + } + + public function testDelegateToPlatformForSqlDeclaration(): void + { + $columnDefinitions = [ + [['length' => 12, 'fixed' => true]], + [['length' => 14]], + ]; + + foreach ($columnDefinitions as $column) { + $platform = $this->createMock(AbstractPlatform::class); + $platform->expects(self::once()) + ->method('getAsciiStringTypeDeclarationSQL') + ->with($column); + + $this->type->getSQLDeclaration($column, $platform); + } + } +}