From 3bd0aa95231230c79817ecb8131aeeb6ea53fbd8 Mon Sep 17 00:00:00 2001 From: Martin Georgiev Date: Sun, 7 Sep 2025 01:02:33 +0300 Subject: [PATCH] fix(#424): address unintentional loss of string data type when retrieving PHP array item values from a stored `TextArray` value --- .../Doctrine/DBAL/Types/TextArray.php | 27 ++++++++- .../Doctrine/DBAL/Types/TextArrayTypeTest.php | 58 +++++++++++++++++-- .../Doctrine/DBAL/Types/TextArrayTest.php | 57 ++++++++++++++++++ ...PostgresArrayToPHPArrayTransformerTest.php | 4 ++ 4 files changed, 137 insertions(+), 9 deletions(-) diff --git a/src/MartinGeorgiev/Doctrine/DBAL/Types/TextArray.php b/src/MartinGeorgiev/Doctrine/DBAL/Types/TextArray.php index 8c091558..2152f5bf 100644 --- a/src/MartinGeorgiev/Doctrine/DBAL/Types/TextArray.php +++ b/src/MartinGeorgiev/Doctrine/DBAL/Types/TextArray.php @@ -62,10 +62,31 @@ public function convertToPHPValue($value, AbstractPlatform $platform): ?array protected function transformFromPostgresTextArray(string $postgresValue): array { - if ($postgresValue === '{}') { - return []; + $values = PostgresArrayToPHPArrayTransformer::transformPostgresArrayToPHPArray($postgresValue); + + // No matter what the original PHP array items' data types were, + // once they are stored in PostgreSQL, all of them will become strings. + // Therefore, we need to ensure all items in the returned PHP array are strings. + foreach ($values as $key => $value) { + if (\is_string($value)) { + continue; + } + + if (\is_bool($value)) { + $values[$key] = $value ? 'true' : 'false'; + + continue; + } + + if ($value === null) { + $values[$key] = 'null'; + + continue; + } + + $values[$key] = (string) $value; // @phpstan-ignore-line } - return PostgresArrayToPHPArrayTransformer::transformPostgresArrayToPHPArray($postgresValue); + return $values; } } diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTypeTest.php index 0b7a96d0..81dbf8f1 100644 --- a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTypeTest.php +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTypeTest.php @@ -20,6 +20,7 @@ protected function getPostgresTypeName(): string } #[DataProvider('provideValidTransformations')] + #[DataProvider('provideGithubIssue424TestCases')] #[Test] public function can_handle_array_values(string $testName, array $arrayValue): void { @@ -29,12 +30,57 @@ public function can_handle_array_values(string $testName, array $arrayValue): vo public static function provideValidTransformations(): array { return [ - 'simple text array' => ['simple text array', ['foo', 'bar', 'baz']], - 'text array with special chars' => ['text array with special chars', ['foo"bar', 'baz\qux', 'with,comma']], - 'text array with empty strings' => ['text array with empty strings', ['', 'not empty', '']], - 'text array with unicode' => ['text array with unicode', ['café', 'naïve', 'résumé']], - 'text array with numbers as strings' => ['text array with numbers as strings', ['123', '456', '789']], - 'text array with null elements' => ['text array with null elements', ['foo', null, 'baz']], + 'simple text array' => [ + 'simple text array', + ['foo', 'bar', 'baz'], + ], + 'text array with special chars' => [ + 'text array with special chars', + ['foo"bar', 'baz\qux', 'with,comma'], + ], + 'text array with empty strings' => [ + 'text array with empty strings', + ['', 'not empty', ''], + ], + 'text array with unicode' => [ + 'text array with unicode', + ['café', 'naïve', 'résumé'], + ], + 'text array with numbers as strings' => [ + 'text array with numbers as strings', + ['123', '456', '789'], + ], + 'text array with null element as string' => [ + 'text array with null elements', + ['foo', 'null', 'baz'], + ], + ]; + } + + /** + * This test scenarios specifically verify the scenarios from GitHub issue #424 + * where PostgreSQL optimizes {"1","test"} to {1,test} and we have to ensure + * that TextArray correctly preserves string types when converted back for PHP. + */ + public static function provideGithubIssue424TestCases(): array + { + return [ + 'numeric values' => [ + 'Numeric values should be preserved as strings', + ['1', 'test'], + ], + 'mixed values' => [ + 'Mixed numeric values should be preserved as strings', + ['1', '2.5', '3.14', 'test', 'true', ''], + ], + 'boolean values' => [ + 'Boolean values should be converted to strings', + ['1', '', '1', ''], + ], + 'null values' => [ + 'Null values should be converted to strings', + ['', 'null', 'NULL'], + ], ]; } } diff --git a/tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTest.php b/tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTest.php index 1fadc2da..f969587c 100644 --- a/tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTest.php +++ b/tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTest.php @@ -122,4 +122,61 @@ public function can_handle_backslashes_correctly(): void $this->assertEquals($expectedPhpValue, $this->fixture->convertToPHPValue($postgresValue, $this->platform)); } + + #[DataProvider('provideGithubIssue424TestCases')] + #[Test] + public function can_preserve_string_types_retrieved_from_database_for_github_issue_424(string $testName, string $postgresValue, array $expectedResult): void + { + $result = $this->fixture->convertToPHPValue($postgresValue, $this->platform); + + $this->assertSame($expectedResult, $result, $testName); + + // Verify all values are strings + foreach ($result as $value) { + $this->assertIsString($value, $testName.' - all values should be strings'); + } + } + + /** + * @return list + */ + public static function provideGithubIssue424TestCases(): array + { + return [ + [ + 'testName' => 'Numeric values should be preserved as strings', + 'postgresValue' => '{1,test}', + 'expectedResult' => ['1', 'test'], + ], + [ + 'testName' => 'Mixed values should be preserved as strings', + 'postgresValue' => '{1,2.5,3.14,test,true,false}', + 'expectedResult' => ['1', '2.5', '3.14', 'test', 'true', 'false'], + ], + [ + 'testName' => 'Boolean-like values should be converted to strings', + 'postgresValue' => '{true,false}', + 'expectedResult' => ['true', 'false'], + ], + [ + 'testName' => 'Quoted boolean-like values should remain as strings', + 'postgresValue' => '{"true","false","t","f"}', + 'expectedResult' => ['true', 'false', 't', 'f'], + ], + [ + 'testName' => 'Mixed quoted/unquoted values should all be strings', + 'postgresValue' => '{1,"2",true,"false",3.14,"test"}', + 'expectedResult' => ['1', '2', 'true', 'false', '3.14', 'test'], + ], + [ + 'testName' => 'Null values should be converted to strings', + 'postgresValue' => '{"",null,"null","NULL"}', + 'expectedResult' => ['', 'null', 'null', 'NULL'], + ], + ]; + } } diff --git a/tests/Unit/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformerTest.php b/tests/Unit/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformerTest.php index 9a0b8cb1..8a7daa95 100644 --- a/tests/Unit/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformerTest.php +++ b/tests/Unit/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformerTest.php @@ -166,6 +166,10 @@ public static function provideValidTransformations(): array 'phpValue' => [' foo '], 'postgresValue' => '{" foo "}', ], + 'github #424 regression: numeric strings should be preserved as strings when unquoted' => [ + 'phpValue' => ['1', 'test', 'true'], + 'postgresValue' => '{1,test,true}', + ], ]; }