diff --git a/docs/en/reference/types.rst b/docs/en/reference/types.rst index c504002c300..2262b7cc9c7 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 ++++ @@ -583,6 +590,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`` [16]_ | | | | | +----------------------------------------------------------+ | | | | | ``TEXT`` [17]_ | diff --git a/src/Driver/Mysqli/Statement.php b/src/Driver/Mysqli/Statement.php index 657a4e56e3c..b3a5ca87465 100644 --- a/src/Driver/Mysqli/Statement.php +++ b/src/Driver/Mysqli/Statement.php @@ -28,6 +28,7 @@ final class Statement implements StatementInterface { /** @var string[] */ protected static $_paramTypeMap = [ + ParameterType::ASCII => 's', ParameterType::STRING => 's', ParameterType::BINARY => 's', ParameterType::BOOLEAN => 'i', diff --git a/src/Driver/PDO/SQLSrv/Statement.php b/src/Driver/PDO/SQLSrv/Statement.php index 12d956bb950..8b8e551fcdb 100644 --- a/src/Driver/PDO/SQLSrv/Statement.php +++ b/src/Driver/PDO/SQLSrv/Statement.php @@ -28,11 +28,20 @@ public function __construct(PDOStatement $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 = PDO::PARAM_STR; + $length = 0; + $driverOptions = PDO::SQLSRV_ENCODING_SYSTEM; + break; } return $this->statement->bindParam($param, $variable, $type, $length, $driverOptions); diff --git a/src/Driver/PDO/Statement.php b/src/Driver/PDO/Statement.php index 550984c706a..7107fa2dff5 100644 --- a/src/Driver/PDO/Statement.php +++ b/src/Driver/PDO/Statement.php @@ -20,6 +20,7 @@ final class Statement implements StatementInterface 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/src/Driver/SQLSrv/Statement.php b/src/Driver/SQLSrv/Statement.php index fc482ab42f0..fe380fa11cd 100644 --- a/src/Driver/SQLSrv/Statement.php +++ b/src/Driver/SQLSrv/Statement.php @@ -182,6 +182,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/src/ParameterType.php b/src/ParameterType.php index 7834c88afff..77917e8703b 100644 --- a/src/ParameterType.php +++ b/src/ParameterType.php @@ -41,6 +41,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/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php index f807bf5d898..0b91397b951 100644 --- a/src/Platforms/AbstractPlatform.php +++ b/src/Platforms/AbstractPlatform.php @@ -185,6 +185,19 @@ private function initializeAllDoctrineTypeMappings() } } + /** + * Returns the SQL snippet used to declare a column that can + * store characters in the ASCII character set + * + * @param mixed[] $column + * + * @return string + */ + public function getAsciiStringTypeDeclarationSQL(array $column) + { + return $this->getVarcharTypeDeclarationSQL($column); + } + /** * Returns the SQL snippet used to declare a VARCHAR column type. * diff --git a/src/Platforms/SQLServer2012Platform.php b/src/Platforms/SQLServer2012Platform.php index 6b33ca6b63a..1a98e301efe 100644 --- a/src/Platforms/SQLServer2012Platform.php +++ b/src/Platforms/SQLServer2012Platform.php @@ -1247,6 +1247,20 @@ public function getDateTimeTzTypeDeclarationSQL(array $column) return 'DATETIMEOFFSET(6)'; } + /** + * {@inheritDoc} + */ + public function getAsciiStringTypeDeclarationSQL(array $column) + { + $length = $column['length'] ?? null; + + if (! isset($column['fixed'])) { + return sprintf('VARCHAR(%d)', $length); + } + + return sprintf('CHAR(%d)', $length); + } + /** * {@inheritDoc} */ diff --git a/src/Types/AsciiStringType.php b/src/Types/AsciiStringType.php new file mode 100644 index 00000000000..981761a11e2 --- /dev/null +++ b/src/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/src/Types/Type.php b/src/Types/Type.php index 48f95ac7b81..bdf25817ffc 100644 --- a/src/Types/Type.php +++ b/src/Types/Type.php @@ -21,6 +21,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/src/Types/Types.php b/src/Types/Types.php index 771a01780ba..56bf3f51cf0 100644 --- a/src/Types/Types.php +++ b/src/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/Functional/ParameterTypes/AsciiTest.php b/tests/Functional/ParameterTypes/AsciiTest.php new file mode 100644 index 00000000000..70c56540173 --- /dev/null +++ b/tests/Functional/ParameterTypes/AsciiTest.php @@ -0,0 +1,26 @@ +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); + $results = $statement->execute()->fetchOne(); + + self::assertEquals('varchar', $results); + } +} diff --git a/tests/Functional/Types/AsciiStringTest.php b/tests/Functional/Types/AsciiStringTest.php new file mode 100644 index 00000000000..832541c50b3 --- /dev/null +++ b/tests/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/Platforms/AbstractPlatformTestCase.php b/tests/Platforms/AbstractPlatformTestCase.php index 45d74b79307..403b3f806df 100644 --- a/tests/Platforms/AbstractPlatformTestCase.php +++ b/tests/Platforms/AbstractPlatformTestCase.php @@ -1438,6 +1438,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/Platforms/OraclePlatformTest.php b/tests/Platforms/OraclePlatformTest.php index d92478d5ee7..5c06ddafc0c 100644 --- a/tests/Platforms/OraclePlatformTest.php +++ b/tests/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/Types/AsciiStringTest.php b/tests/Types/AsciiStringTest.php new file mode 100644 index 00000000000..7cc48c083c6 --- /dev/null +++ b/tests/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); + } + } +}