diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/ArrayTypeTestCase.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/ArrayTypeTestCase.php new file mode 100644 index 00000000..9ecc1488 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/ArrayTypeTestCase.php @@ -0,0 +1,40 @@ +getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, []); + } + + #[Test] + public function can_handle_null_values(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, null); + } + + /** + * Data-driven test for array values. + * Subclasses should add #[DataProvider('provideValidTransformations')]. + */ + public function can_handle_array_values(string $testName, array $arrayValue): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, $arrayValue); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/BigIntArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/BigIntArrayTypeTest.php new file mode 100644 index 00000000..453624d5 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/BigIntArrayTypeTest.php @@ -0,0 +1,42 @@ +}> + */ + public static function provideValidTransformations(): array + { + return [ + 'simple bigint array' => ['simple bigint array', [9223372036854775807, 1, -9223372036854775807]], + 'bigint array with zeros' => ['bigint array with zeros', [0, 0, 0, 1, 0]], + 'bigint array with large numbers' => ['bigint array with large numbers', [1000000000000, 2000000000000, 3000000000000]], + 'bigint array with negative numbers' => ['bigint array with negative numbers', [-1000000000000, -2000000000000, -3000000000000]], + 'bigint array with PHP max and min integer constants' => ['bigint array with PHP max and min integer constants', [PHP_INT_MAX, PHP_INT_MIN, 0]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/BooleanArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/BooleanArrayTypeTest.php new file mode 100644 index 00000000..f1ec2141 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/BooleanArrayTypeTest.php @@ -0,0 +1,41 @@ +}> + */ + public static function provideValidTransformations(): array + { + return [ + 'simple boolean array' => ['simple boolean array', [true, false, true]], + 'boolean array with all true' => ['boolean array with all true', [true, true, true]], + 'boolean array with all false' => ['boolean array with all false', [false, false, false]], + 'boolean array mixed' => ['boolean array mixed', [true, false, true, false, true]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/CidrArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/CidrArrayTypeTest.php new file mode 100644 index 00000000..6e91bf65 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/CidrArrayTypeTest.php @@ -0,0 +1,63 @@ +}> + */ + public static function provideValidTransformations(): array + { + return [ + 'simple cidr array' => ['simple cidr array', ['192.168.1.0/24', '10.0.0.0/8']], + 'cidr array with IPv6' => ['cidr array with IPv6', ['2001:db8::/32', '2001:db8::/64']], + 'cidr array with mixed networks' => ['cidr array with mixed networks', [ + '192.168.1.0/24', + '172.16.0.0/16', + '10.0.0.0/8', + '2001:db8::/32', + ]], + 'cidr array with single hosts' => ['cidr array with single hosts', [ + '192.168.1.1/32', + '10.0.0.1/32', + '2001:db8::1/128', + ]], + 'empty cidr array' => ['empty cidr array', []], + ]; + } + + #[Test] + public function can_handle_invalid_networks(): void + { + $this->expectException(InvalidCidrArrayItemForPHPException::class); + + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, ['invalid-network', '192.168.1.0/24']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/CidrTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/CidrTypeTest.php new file mode 100644 index 00000000..51589f82 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/CidrTypeTest.php @@ -0,0 +1,58 @@ +getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, $testValue); + } + + /** + * @return array + */ + public static function provideValidTransformations(): array + { + return [ + 'IPv4 CIDR' => ['192.168.1.0/24'], + 'IPv4 CIDR /8' => ['10.0.0.0/8'], + 'IPv4 CIDR /16' => ['172.16.0.0/16'], + 'IPv6 CIDR' => ['2001:db8::/32'], + 'IPv6 CIDR /64' => ['2001:db8::/64'], + 'IPv6 CIDR /128' => ['2001:db8::1/128'], + ]; + } + + #[Test] + public function can_handle_invalid_networks(): void + { + $this->expectException(InvalidCidrForPHPException::class); + + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, 'invalid-network'); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/DBALTypesTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/DBALTypesTest.php deleted file mode 100644 index fd3ee9de..00000000 --- a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/DBALTypesTest.php +++ /dev/null @@ -1,219 +0,0 @@ -runTypeTest($typeName, $columnType, $testValue); - } - - /** - * @return array - */ - public static function provideScalarTypeTestCases(): array - { - return [ - 'inet' => ['inet', 'INET', '192.168.1.1'], - 'inet with CIDR' => ['inet', 'INET', '192.168.1.0/24'], - 'inet IPv6' => ['inet', 'INET', '2001:db8::1'], - 'cidr IPv4' => ['cidr', 'CIDR', '192.168.1.0/24'], - 'cidr IPv6' => ['cidr', 'CIDR', '2001:db8::/32'], - 'macaddr' => ['macaddr', 'MACADDR', '08:00:2b:01:02:03'], - ]; - } - - /** - * @param array $testValue - */ - #[DataProvider('provideArrayTypeTestCases')] - public function test_array_type(string $typeName, string $columnType, array $testValue): void - { - $this->runTypeTest($typeName, $columnType, $testValue); - } - - /** - * @return array}> - */ - public static function provideArrayTypeTestCases(): array - { - return [ - 'bool[]' => ['bool[]', 'BOOL[]', [true, false, true]], - 'smallint[]' => ['smallint[]', 'SMALLINT[]', [32767, 0, -32768]], - 'integer[]' => ['integer[]', 'INTEGER[]', [1, 2, 3, 4, 5]], - 'bigint[]' => ['bigint[]', 'BIGINT[]', [9223372036854775807, 1, -9223372036854775807]], - 'text[]' => ['text[]', 'TEXT[]', ['foo', 'bar', 'baz']], - 'text[] with special chars' => ['text[]', 'TEXT[]', ['foo"bar', 'baz\qux', 'with,comma']], - 'real[]' => ['real[]', 'REAL[]', [1.5, 2.5, 3.5]], - 'double precision[]' => ['double precision[]', 'DOUBLE PRECISION[]', [1.123456789, 2.123456789, 3.123456789]], - 'cidr[]' => ['cidr[]', 'CIDR[]', ['192.168.1.0/24', '10.0.0.0/8', '172.16.0.0/16']], - 'inet[]' => ['inet[]', 'INET[]', ['192.168.1.1', '10.0.0.1', '172.16.0.1']], - 'macaddr[]' => ['macaddr[]', 'MACADDR[]', ['08:00:2b:01:02:03', '00:0c:29:aa:bb:cc']], - 'point[]' => ['point[]', 'POINT[]', [ - new PointValueObject(1.23, 4.56), - new PointValueObject(-10.5, -20.75), - new PointValueObject(0.0, 0.0), - ]], - ]; - } - - /** - * @param array $testValue - */ - #[DataProvider('provideJsonTypeTestCases')] - public function test_json_type(string $typeName, string $columnType, array $testValue): void - { - $this->runTypeTest($typeName, $columnType, $testValue); - } - - /** - * @return array}> - */ - public static function provideJsonTypeTestCases(): array - { - return [ - 'jsonb simple' => ['jsonb', 'JSONB', ['foo' => 'bar', 'baz' => 123]], - 'jsonb complex' => [ - 'jsonb', - 'JSONB', - [ - 'string' => 'value', - 'number' => 42, - 'boolean' => true, - 'null' => null, - 'array' => [1, 2, 3], - 'object' => ['nested' => 'value'], - ], - ], - ]; - } - - #[DataProvider('providePointTypeTestCases')] - public function test_point_type(string $typeName, string $columnType, PointValueObject $pointValueObject): void - { - $this->runTypeTest($typeName, $columnType, $pointValueObject); - } - - /** - * @return array - */ - public static function providePointTypeTestCases(): array - { - return [ - 'point with positive coordinates' => ['point', 'POINT', new PointValueObject(1.23, 4.56)], - 'point with negative coordinates' => ['point', 'POINT', new PointValueObject(-10.5, -20.75)], - 'point with zero coordinates' => ['point', 'POINT', new PointValueObject(0.0, 0.0)], - 'point with max precision' => ['point', 'POINT', new PointValueObject(123.456789, -98.765432)], - ]; - } - - /** - * @param DateRangeValueObject|Int4RangeValueObject|Int8RangeValueObject|NumRangeValueObject|TsRangeValueObject|TstzRangeValueObject $rangeValueObject - */ - #[DataProvider('provideRangeTypeTestCases')] - public function test_range_type(string $typeName, string $columnType, RangeValueObject $rangeValueObject): void - { - $this->runTypeTest($typeName, $columnType, $rangeValueObject); - } - - /** - * @return array - */ - public static function provideRangeTypeTestCases(): array - { - return [ - 'numrange simple' => ['numrange', 'NUMRANGE', new NumRangeValueObject(1.5, 10.7)], - 'numrange infinite' => ['numrange', 'NUMRANGE', new NumRangeValueObject(null, 1000, false, false)], - 'numrange empty' => ['numrange', 'NUMRANGE', NumRangeValueObject::empty()], - 'int4range simple' => ['int4range', 'INT4RANGE', new Int4RangeValueObject(1, 1000)], - 'int4range infinite' => ['int4range', 'INT4RANGE', new Int4RangeValueObject(null, 1000, false, false)], - 'int8range simple' => ['int8range', 'INT8RANGE', new Int8RangeValueObject(PHP_INT_MIN, PHP_INT_MAX)], - 'daterange simple' => ['daterange', 'DATERANGE', new DateRangeValueObject(new \DateTimeImmutable('2023-01-01'), new \DateTimeImmutable('2023-12-31'))], - 'tsrange simple' => ['tsrange', 'TSRANGE', new TsRangeValueObject(new \DateTimeImmutable('2023-01-01 10:00:00'), new \DateTimeImmutable('2023-01-01 18:00:00'))], - 'tstzrange simple' => ['tstzrange', 'TSTZRANGE', new TstzRangeValueObject(new \DateTimeImmutable('2023-01-01 10:00:00+00:00'), new \DateTimeImmutable('2023-01-01 18:00:00+00:00'))], - ]; - } - - /** - * Generic test method that handles all types of tests. - */ - private function runTypeTest(string $typeName, string $columnType, mixed $testValue): void - { - $tableName = 'test_'.\str_replace(['[', ']', ' '], ['', '', '_'], $typeName); - $columnName = 'test_column'; - - try { - $this->createTestTableForDataType($tableName, $columnName, $columnType); - - // Insert test value - $queryBuilder = $this->connection->createQueryBuilder(); - $queryBuilder - ->insert('test.'.$tableName) - ->values([$columnName => ':value']) - ->setParameter('value', $testValue, $typeName); - - $queryBuilder->executeStatement(); - - // Query the value back - $queryBuilder = $this->connection->createQueryBuilder(); - $queryBuilder - ->select($columnName) - ->from('test.'.$tableName) - ->where('id = 1'); - - $result = $queryBuilder->executeQuery(); - $row = $result->fetchAssociative(); - \assert(\is_array($row) && \array_key_exists($columnName, $row)); - - // Get the value with the correct type - $platform = $this->connection->getDatabasePlatform(); - $retrievedValue = Type::getType($typeName)->convertToPHPValue($row[$columnName], $platform); - - $this->assertDatabaseRoundtripEquals($testValue, $retrievedValue, $typeName); - } finally { - $this->dropTestTableIfItExists($tableName); - } - } - - private function assertDatabaseRoundtripEquals(mixed $expected, mixed $actual, string $typeName): void - { - match (true) { - $expected instanceof PointValueObject => $this->assertPointEquals($expected, $actual, $typeName), - $expected instanceof RangeValueObject => $this->assertRangeEquals($expected, $actual, $typeName), - \is_array($expected) => $this->assertEquals($expected, $actual, 'Failed asserting that array values are equal for type '.$typeName), - default => $this->assertSame($expected, $actual, 'Failed asserting that values are identical for type '.$typeName) - }; - } - - private function assertPointEquals(PointValueObject $pointValueObject, mixed $actual, string $typeName): void - { - $this->assertInstanceOf(PointValueObject::class, $actual, 'Failed asserting that value is a Point object for type '.$typeName); - $this->assertEquals($pointValueObject->getX(), $actual->getX(), 'Failed asserting that X coordinates are equal for type '.$typeName); - $this->assertEquals($pointValueObject->getY(), $actual->getY(), 'Failed asserting that Y coordinates are equal for type '.$typeName); - } - - /** - * @param RangeValueObject<\DateTimeInterface|float|int> $rangeValueObject - */ - private function assertRangeEquals(RangeValueObject $rangeValueObject, mixed $actual, string $typeName): void - { - $this->assertInstanceOf(RangeValueObject::class, $actual, 'Failed asserting that value is a Range object for type '.$typeName); - $this->assertEquals($rangeValueObject->__toString(), $actual->__toString(), 'Failed asserting that range string representations are equal for type '.$typeName); - $this->assertEquals($rangeValueObject->isEmpty(), $actual->isEmpty(), 'Failed asserting that range empty states are equal for type '.$typeName); - } -} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/DateRangeTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/DateRangeTypeTest.php new file mode 100644 index 00000000..6bfc7eba --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/DateRangeTypeTest.php @@ -0,0 +1,101 @@ + + */ + public static function provideValidTransformations(): array + { + return [ + 'simple daterange' => ['simple daterange', new DateRangeValueObject( + new \DateTimeImmutable('2023-01-02'), + new \DateTimeImmutable('2023-12-31'), + true, + false + )], + 'daterange with inclusive bounds' => ['daterange with inclusive bounds', new DateRangeValueObject( + new \DateTimeImmutable('2023-01-01'), + new \DateTimeImmutable('2024-01-01'), + true, + false + )], + 'daterange with single day' => ['daterange with single day', new DateRangeValueObject( + new \DateTimeImmutable('2023-06-15'), + new \DateTimeImmutable('2023-06-16'), + true, + false + )], + 'daterange with leap year' => ['daterange with leap year', new DateRangeValueObject( + new \DateTimeImmutable('2024-02-02'), + new \DateTimeImmutable('2024-02-29'), + true, + false + )], + ]; + } + + #[Test] + public function can_handle_infinite_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $dateRange = new DateRangeValueObject(null, null, false, false); + $this->runTypeTest($typeName, $columnType, $dateRange); + } + + #[Test] + public function can_handle_empty_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + // lower > upper shall result in an empty range + $dateRange = new DateRangeValueObject( + new \DateTimeImmutable('2023-12-31'), + new \DateTimeImmutable('2023-01-01'), + false, + false + ); + $this->runTypeTest($typeName, $columnType, $dateRange); + } + + /** + * @return array}> [name, dql, expectedIds] + */ + public static function provideOperatorScenarios(): array + { + return [ + 'contains daterange' => ['contains daterange', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE CONTAINS(r.dateRange, \'[2023-02-01,2023-11-01)\') = TRUE', [1]], + 'is contained by daterange' => ['is contained by daterange', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE IS_CONTAINED_BY(\'[2023-02-01,2023-11-01)\', r.dateRange) = TRUE', [1]], + 'overlaps daterange' => ['overlaps daterange', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE OVERLAPS(r.dateRange, \'[2023-11-15,2024-01-15)\') = TRUE', [1, 2, 3]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/DoublePrecisionArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/DoublePrecisionArrayTypeTest.php new file mode 100644 index 00000000..8cd4828f --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/DoublePrecisionArrayTypeTest.php @@ -0,0 +1,48 @@ +}> + */ + public static function provideValidTransformations(): array + { + return [ + 'simple double precision array' => ['simple double precision array', [1.123456789, 2.123456789, 3.123456789]], + 'double precision array with negative values' => ['double precision array with negative values', [-1.5, -2.5, -3.5]], + 'double precision array with high precision' => ['double precision array with high precision', [ + 1.1234567890123, + 2.9876543210988, + 3.1415926535898, + ]], + 'double precision array with integers' => ['double precision array with integers', [1.0, 2.0, 3.0]], + 'double precision array with zero' => ['double precision array with zero', [0.0, 1.5, -1.5]], + 'empty double precision array' => ['empty double precision array', []], + 'double precision array with large numbers' => ['double precision array with large numbers', [1234567.123456, -9876543.987654]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/InetArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/InetArrayTypeTest.php new file mode 100644 index 00000000..9bf36aae --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/InetArrayTypeTest.php @@ -0,0 +1,62 @@ +}> + */ + public static function provideValidTransformations(): array + { + return [ + 'simple inet array' => ['simple inet array', ['192.168.1.1', '10.0.0.1']], + 'inet array with IPv6' => ['inet array with IPv6', ['2001:db8::1', '::1']], + 'inet array with mixed addresses' => ['inet array with mixed addresses', [ + '192.168.1.1', + '172.16.0.1', + '10.0.0.1', + '2001:db8::1', + ]], + 'inet array with localhost' => ['inet array with localhost', [ + '127.0.0.1', + '::1', + ]], + 'empty inet array' => ['empty inet array', []], + ]; + } + + #[Test] + public function can_handle_invalid_addresses(): void + { + $this->expectException(InvalidInetArrayItemForPHPException::class); + + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, ['invalid-address', '192.168.1.1']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/InetTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/InetTypeTest.php new file mode 100644 index 00000000..8567d896 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/InetTypeTest.php @@ -0,0 +1,69 @@ +getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, $testValue); + } + + /** + * @return array + */ + public static function provideValidTransformations(): array + { + return [ + 'IPv4 address' => ['192.168.1.1'], + 'IPv4 with CIDR' => ['192.168.1.0/24'], + 'IPv6 address' => ['2001:db8::1'], + 'IPv6 with CIDR' => ['2001:db8::/32'], + 'localhost IPv4' => ['127.0.0.1'], + 'localhost IPv6' => ['::1'], + ]; + } + + #[Test] + public function can_handle_invalid_addresses(): void + { + $this->expectException(InvalidInetForPHPException::class); + + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, 'invalid-address'); + } + + #[Test] + public function can_handle_empty_string(): void + { + $this->expectException(InvalidInetForPHPException::class); + + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, ''); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/Int4RangeTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/Int4RangeTypeTest.php new file mode 100644 index 00000000..d6705ffa --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/Int4RangeTypeTest.php @@ -0,0 +1,76 @@ + + */ + public static function provideValidTransformations(): array + { + return [ + 'simple int4range' => ['simple int4range', new Int4RangeValueObject(1, 10, true, false)], + 'int4range with inclusive bounds' => ['int4range with inclusive bounds', new Int4RangeValueObject(5, 15, true, false)], + 'int4range with negative values' => ['int4range with negative values', new Int4RangeValueObject(-100, 100, true, false)], + 'int4range with max values' => ['int4range with max values', new Int4RangeValueObject(-2147483648, 2147483647, true, false)], + ]; + } + + #[Test] + public function can_handle_infinite_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $int4Range = new Int4RangeValueObject(null, null, false, false); + $this->runTypeTest($typeName, $columnType, $int4Range); + } + + #[Test] + public function can_handle_empty_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + // lower > upper shall result in an empty range + $int4Range = new Int4RangeValueObject(10, 5, false, false); + $this->runTypeTest($typeName, $columnType, $int4Range); + } + + /** + * @return array}> [name, dql, expectedIds] + */ + public static function provideOperatorScenarios(): array + { + return [ + 'contains int4range' => ['contains int4range', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE CONTAINS(r.int4Range, \'[3,7)\') = TRUE', [1]], + 'is contained by int4range' => ['is contained by int4range', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE IS_CONTAINED_BY(\'[3,7)\', r.int4Range) = TRUE', [1]], + 'overlaps int4range' => ['overlaps int4range', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE OVERLAPS(r.int4Range, \'[8,12)\') = TRUE', [1, 2]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/Int8RangeTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/Int8RangeTypeTest.php new file mode 100644 index 00000000..2624b6a0 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/Int8RangeTypeTest.php @@ -0,0 +1,76 @@ + + */ + public static function provideValidTransformations(): array + { + return [ + 'simple int8range' => ['simple int8range', new Int8RangeValueObject(1, 1000, true, false)], + 'int8range with inclusive bounds' => ['int8range with inclusive bounds', new Int8RangeValueObject(5, 16, true, false)], + 'int8range with negative values' => ['int8range with negative values', new Int8RangeValueObject(-999999, 1000000, true, false)], + 'int8range with max values' => ['int8range with max values', new Int8RangeValueObject(PHP_INT_MIN + 1, PHP_INT_MAX, true, false)], + ]; + } + + #[Test] + public function can_handle_infinite_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $int8Range = new Int8RangeValueObject(null, null, false, false); + $this->runTypeTest($typeName, $columnType, $int8Range); + } + + #[Test] + public function can_handle_empty_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + // lower > upper shall result in an empty range + $int8Range = new Int8RangeValueObject(10, 5, false, false); + $this->runTypeTest($typeName, $columnType, $int8Range); + } + + /** + * @return array}> [name, dql, expectedIds] + */ + public static function provideOperatorScenarios(): array + { + return [ + 'contains int8range' => ['contains int8range', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE CONTAINS(r.int8Range, \'[150,800)\') = TRUE', [1]], + 'is contained by int8range' => ['is contained by int8range', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE IS_CONTAINED_BY(\'[200,800)\', r.int8Range) = TRUE', [1]], + 'overlaps int8range' => ['overlaps int8range', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE OVERLAPS(r.int8Range, \'[800,1200)\') = TRUE', [1, 2]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/IntegerArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/IntegerArrayTypeTest.php new file mode 100644 index 00000000..3ed2545a --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/IntegerArrayTypeTest.php @@ -0,0 +1,37 @@ + ['simple integer array', [1, 2, 3, 4, 5]], + 'integer array with negatives' => ['integer array with negatives', [-1, 0, 1, -100, 100]], + 'integer array with max values' => ['integer array with max values', [2147483647, -2147483648, 0]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/JsonbTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/JsonbTypeTest.php new file mode 100644 index 00000000..f7e50063 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/JsonbTypeTest.php @@ -0,0 +1,75 @@ +getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, null); + } + + #[Test] + public function can_handle_empty_arrays(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, []); + } + + #[DataProvider('provideValidTransformations')] + #[Test] + public function can_handle_json_values(string $testName, array $json): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, $json); + } + + /** + * @return array + */ + public static function provideValidTransformations(): array + { + return [ + 'simple object' => ['simple object', ['foo' => 'bar', 'baz' => 123]], + 'nested structures' => ['nested structures', [ + 'user' => ['id' => 1, 'name' => 'John'], + 'meta' => ['active' => true, 'roles' => ['admin', 'user']], + ]], + 'mixed types' => ['mixed types', [ + 'string' => 'value', + 'number' => 42, + 'boolean' => false, + 'null' => null, + 'array' => [1, 2, 3], + 'object' => ['a' => 1], + ]], + 'special characters' => ['special characters', [ + 'message' => 'Hello "World" with \'quotes\'', + 'path' => '/path/with/slashes', + ]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/MacaddrArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/MacaddrArrayTypeTest.php new file mode 100644 index 00000000..6c179083 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/MacaddrArrayTypeTest.php @@ -0,0 +1,60 @@ +}> + */ + public static function provideValidTransformations(): array + { + return [ + 'simple macaddr array' => ['simple macaddr array', ['08:00:2b:01:02:03', '00:0c:29:aa:bb:cc']], + 'macaddr array with zeros' => ['macaddr array with zeros', ['00:00:00:00:00:00', 'ff:ff:ff:ff:ff:ff']], + 'macaddr array with mixed case' => ['macaddr array with mixed case', [ + '08:00:2b:01:02:03', + '00:0c:29:aa:bb:cc', + ]], + 'macaddr array with single digits' => ['macaddr array with single digits', [ + '01:02:03:04:05:06', + '0a:0b:0c:0d:0e:0f', + ]], + 'empty macaddr array' => ['empty macaddr array', []], + ]; + } + + #[Test] + public function can_handle_invalid_addresses(): void + { + $this->expectException(InvalidMacaddrArrayItemForPHPException::class); + + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, ['invalid-mac', '08:00:2b:01:02:03']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/MacaddrTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/MacaddrTypeTest.php new file mode 100644 index 00000000..cdf392c2 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/MacaddrTypeTest.php @@ -0,0 +1,57 @@ +getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, $testValue); + } + + /** + * @return array + */ + public static function provideValidTransformations(): array + { + return [ + 'standard MAC address' => ['08:00:2b:01:02:03'], + 'MAC with zeros' => ['00:00:00:00:00:00'], + 'MAC with FF' => ['ff:ff:ff:ff:ff:ff'], + 'mixed case MAC' => ['08:00:2b:01:02:03'], + 'MAC with single digits' => ['01:02:03:04:05:06'], + ]; + } + + #[Test] + public function can_handle_invalid_addresses(): void + { + $this->expectException(InvalidMacaddrForPHPException::class); + + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, 'invalid-mac'); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/NumRangeTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/NumRangeTypeTest.php new file mode 100644 index 00000000..7329427f --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/NumRangeTypeTest.php @@ -0,0 +1,74 @@ + + */ + public static function provideValidTransformations(): array + { + return [ + 'simple numrange' => ['simple numrange', new NumRangeValueObject(1.5, 10.7, false, false)], + 'numrange with inclusive bounds' => ['numrange with inclusive bounds', new NumRangeValueObject(5.5, 15.7, true, true)], + 'numrange with negative values' => ['numrange with negative values', new NumRangeValueObject(-100.5, 100.7, false, false)], + 'numrange with high precision' => ['numrange with high precision', new NumRangeValueObject(1.123456789, 10.987654321, false, false)], + ]; + } + + #[Test] + public function can_handle_infinite_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $numericRange = new NumRangeValueObject(null, null, false, false); + $this->runTypeTest($typeName, $columnType, $numericRange); + } + + #[Test] + public function can_handle_empty_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + // lower > upper shall result in an empty range + $numericRange = new NumRangeValueObject(10.5, 5.7, false, false); + $this->runTypeTest($typeName, $columnType, $numericRange); + } + + /** + * @return array}> [name, dql, expectedIds] + */ + public static function provideOperatorScenarios(): array + { + return [ + 'contains numrange' => ['contains numrange', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE CONTAINS(r.numRange, \'[2.5,8.5)\') = TRUE', [1]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/PointArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/PointArrayTypeTest.php new file mode 100644 index 00000000..b0230ffb --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/PointArrayTypeTest.php @@ -0,0 +1,87 @@ +}> + */ + public static function provideValidTransformations(): array + { + return [ + 'simple point array' => ['simple point array', [ + new PointValueObject(1.23, 4.56), + new PointValueObject(-10.5, -20.75), + ]], + 'point array with zero coordinates' => ['point array with zero coordinates', [ + new PointValueObject(0.0, 0.0), + new PointValueObject(100.0, 200.0), + ]], + 'point array with high precision' => ['point array with high precision', [ + new PointValueObject(123.456789, -987.654321), + new PointValueObject(0.123456, 0.987654), + ]], + 'point array with integer coordinates' => ['point array with integer coordinates', [ + new PointValueObject(100, 200), + new PointValueObject(-50, -100), + ]], + 'empty point array' => ['empty point array', []], + ]; + } + + /** + * Override to handle Point array-specific coordinate precision comparison. + */ + protected function assertTypeValueEquals(mixed $expected, mixed $actual, string $typeName): void + { + if (!\is_array($expected) || !\is_array($actual)) { + throw new \InvalidArgumentException('PointArrayTypeTest expects arrays of Point value objects.'); + } + + if (!$this->isPointArray($expected) || !$this->isPointArray($actual)) { + throw new \InvalidArgumentException('PointArrayTypeTest expects arrays containing only Point value objects.'); + } + + $this->assertPointArrayEquals($expected, $actual, $typeName); + } + + /** + * Assert that two point arrays are equal with coordinate precision. + */ + protected function assertPointArrayEquals(array $expected, array $actual, string $typeName): void + { + $this->assertCount(\count($expected), $actual, \sprintf('Point array count mismatch for type %s', $typeName)); + + foreach ($expected as $index => $expectedPoint) { + if ($expectedPoint instanceof PointValueObject && $actual[$index] instanceof PointValueObject) { + $this->assertPointEquals($expectedPoint, $actual[$index], $typeName); + } + } + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/PointAssertionTrait.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/PointAssertionTrait.php new file mode 100644 index 00000000..a21f5820 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/PointAssertionTrait.php @@ -0,0 +1,47 @@ +assertEqualsWithDelta( + $expected->getX(), + $actual->getX(), + 0.000001, + 'X coordinate mismatch for type '.$typeName + ); + + $this->assertEqualsWithDelta( + $expected->getY(), + $actual->getY(), + 0.000001, + 'Y coordinate mismatch for type '.$typeName + ); + } + + protected function isPointArray(array $array): bool + { + foreach ($array as $item) { + if (!$item instanceof PointValueObject) { + return false; + } + } + + return true; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/PointTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/PointTypeTest.php new file mode 100644 index 00000000..48d0d134 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/PointTypeTest.php @@ -0,0 +1,69 @@ +getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, null); + } + + /** + * Override to handle Point-specific coordinate precision comparison. + */ + protected function assertTypeValueEquals(mixed $expected, mixed $actual, string $typeName): void + { + if (!$expected instanceof PointValueObject || !$actual instanceof PointValueObject) { + throw new \InvalidArgumentException('PointTypeTest expects Point value objects.'); + } + + $this->assertPointEquals($expected, $actual, $typeName); + } + + #[DataProvider('provideValidTransformations')] + #[Test] + public function can_handle_point_values(string $testName, PointValueObject $pointValueObject): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, $pointValueObject); + } + + /** + * @return array + */ + public static function provideValidTransformations(): array + { + return [ + 'simple point' => ['simple point', new PointValueObject(1.23, 4.56)], + 'zero coordinates' => ['zero coordinates', new PointValueObject(0.0, 0.0)], + 'negative coordinates' => ['negative coordinates', new PointValueObject(-10.5, -20.75)], + 'high precision' => ['high precision', new PointValueObject(123.456789, -987.654321)], + 'integer coordinates' => ['integer coordinates', new PointValueObject(100, 200)], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RangeOperatorsTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RangeOperatorsTest.php deleted file mode 100644 index e3b67ded..00000000 --- a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RangeOperatorsTest.php +++ /dev/null @@ -1,176 +0,0 @@ - Contains::class, - 'IS_CONTAINED_BY' => IsContainedBy::class, - 'OVERLAPS' => Overlaps::class, - ]; - } - - public function test_range_contains_operator_int4range(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE CONTAINS(r.int4Range, \'[3,7)\') = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); - $this->assertSame(1, $result[0]['id']); - } - - public function test_range_contains_operator_numrange(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE CONTAINS(r.numRange, \'[2.5,8.5)\') = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); - $this->assertSame(1, $result[0]['id']); - } - - public function test_range_contains_operator_int8range(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE CONTAINS(r.int8Range, \'[150,800)\') = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); - $this->assertSame(1, $result[0]['id']); - } - - public function test_range_contains_operator_daterange(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE CONTAINS(r.dateRange, \'[2023-02-01,2023-11-01)\') = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); - $this->assertSame(1, $result[0]['id']); - } - - public function test_range_is_contained_by_int4range(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE IS_CONTAINED_BY(\'[3,7)\', r.int4Range) = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); - $this->assertSame(1, $result[0]['id']); - } - - public function test_range_is_contained_by_int8range(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE IS_CONTAINED_BY(\'[200,800)\', r.int8Range) = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); - $this->assertSame(1, $result[0]['id']); - } - - public function test_range_is_contained_by_daterange(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE IS_CONTAINED_BY(\'[2023-02-01,2023-11-01)\', r.dateRange) = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); - $this->assertSame(1, $result[0]['id']); - } - - public function test_range_overlaps_operator_int4range(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE OVERLAPS(r.int4Range, \'[8,12)\') = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(2, $result); // Should match records 1 and 2 - $this->assertContains(['id' => 1], $result); - $this->assertContains(['id' => 2], $result); - } - - public function test_range_overlaps_operator_int8range(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE OVERLAPS(r.int8Range, \'[800,1200)\') = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(2, $result); // Should match records 1 and 2 - $this->assertContains(['id' => 1], $result); - $this->assertContains(['id' => 2], $result); - } - - public function test_range_overlaps_operator_daterange(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE OVERLAPS(r.dateRange, \'[2023-11-15,2024-01-15)\') = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(3, $result); // Should match all 3 records as they all overlap with the test range - $this->assertContains(['id' => 1], $result); - $this->assertContains(['id' => 2], $result); - $this->assertContains(['id' => 3], $result); - } - - public function test_datetime_is_contained_by_tsrange(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE IS_CONTAINED_BY(\'[2023-01-01 12:00:00,2023-01-01 16:00:00)\', r.tsRange) = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); - $this->assertSame(1, $result[0]['id']); - } - - public function test_datetime_is_contained_by_tstzrange(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE IS_CONTAINED_BY(\'[2023-01-01 12:00:00+00,2023-01-01 16:00:00+00)\', r.tstzRange) = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); - $this->assertSame(1, $result[0]['id']); - } - - public function test_datetime_overlaps_tsrange(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE OVERLAPS(r.tsRange, \'[2023-01-01 16:00:00,2023-01-01 20:00:00)\') = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); // Should match record 1 where tsrange overlaps with the test range - $this->assertSame(1, $result[0]['id']); - } - - public function test_datetime_overlaps_tstzrange(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE OVERLAPS(r.tstzRange, \'[2023-01-01 16:00:00+00,2023-01-01 20:00:00+00)\') = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); // Should match record 1 where tstzrange overlaps with the test range - $this->assertSame(1, $result[0]['id']); - } - - public function test_complex_range_query(): void - { - $dql = 'SELECT r.id FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsRanges r - WHERE CONTAINS(r.int4Range, \'[3,7)\') = TRUE - AND CONTAINS(r.numRange, \'[2.5,8.5)\') = TRUE'; - - $result = $this->executeDqlQuery($dql); - $this->assertCount(1, $result); - $this->assertSame(1, $result[0]['id']); - } -} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RangeTestCase.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RangeTestCase.php deleted file mode 100644 index f21122f8..00000000 --- a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RangeTestCase.php +++ /dev/null @@ -1,59 +0,0 @@ -createTestTableForRangeFixture(); - $this->insertTestDataForRangeFixture(); - } - - protected function createTestTableForRangeFixture(): void - { - $tableName = 'containsranges'; - - $this->createTestSchema(); - $this->dropTestTableIfItExists($tableName); - - $fullTableName = \sprintf('%s.%s', self::DATABASE_SCHEMA, $tableName); - $sql = \sprintf(' - CREATE TABLE %s ( - id SERIAL PRIMARY KEY, - int4range INT4RANGE, - int8range INT8RANGE, - numrange NUMRANGE, - daterange DATERANGE, - tsrange TSRANGE, - tstzrange TSTZRANGE - ) - ', $fullTableName); - - $this->connection->executeStatement($sql); - } - - protected function insertTestDataForRangeFixture(): void - { - $sql = \sprintf(' - INSERT INTO %s.containsranges ( - int4range, - int8range, - numrange, - daterange, - tsrange, - tstzrange - ) VALUES - (\'[1,10)\', \'[100,1000)\', \'[1.5,10.7)\', \'[2023-01-01,2023-12-31)\', \'[2023-01-01 10:00:00,2023-01-01 18:00:00)\', \'[2023-01-01 10:00:00+00,2023-01-01 18:00:00+00)\'), - (\'[5,15)\', \'[500,1500)\', \'[5.5,15.7)\', \'[2023-06-01,2023-12-31)\', \'[2023-06-01 10:00:00,2023-06-01 18:00:00)\', \'[2023-06-01 10:00:00+00,2023-06-01 18:00:00+00)\'), - (\'[20,30)\', \'[2000,3000)\', \'[20.5,30.7)\', \'[2023-12-01,2023-12-31)\', \'[2023-12-01 10:00:00,2023-12-01 18:00:00)\', \'[2023-12-01 10:00:00+00,2023-12-01 18:00:00+00)\') - ', self::DATABASE_SCHEMA); - $this->connection->executeStatement($sql); - } -} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RangeTypeTestCase.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RangeTypeTestCase.php new file mode 100644 index 00000000..b02e9ed8 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RangeTypeTestCase.php @@ -0,0 +1,164 @@ +registerRangeOperatorFunctions(); + $this->createRangeOperatorsTable(); + $this->insertRangeOperatorsRows(); + } + + protected function registerRangeOperatorFunctions(): void + { + $this->configuration->addCustomStringFunction('CONTAINS', Contains::class); + $this->configuration->addCustomStringFunction('IS_CONTAINED_BY', IsContainedBy::class); + $this->configuration->addCustomStringFunction('OVERLAPS', Overlaps::class); + } + + private function createRangeOperatorsTable(): void + { + $tableName = 'containsranges'; + + // Ensure a clean slate + $this->dropTestTableIfItExists($tableName); + + $fullTableName = \sprintf('%s.%s', self::DATABASE_SCHEMA, $tableName); + $sql = \sprintf(' + CREATE TABLE %s ( + id SERIAL PRIMARY KEY, + int4range INT4RANGE, + int8range INT8RANGE, + numrange NUMRANGE, + daterange DATERANGE, + tsrange TSRANGE, + tstzrange TSTZRANGE + ) + ', $fullTableName); + + $this->connection->executeStatement($sql); + } + + private function insertRangeOperatorsRows(): void + { + $sql = \sprintf(' + INSERT INTO %s.containsranges ( + int4range, + int8range, + numrange, + daterange, + tsrange, + tstzrange + ) VALUES + (\'[1,10)\', \'[100,1000)\', \'[1.5,10.7)\', \'[2023-01-01,2023-12-31)\', \'[2023-01-01 10:00:00,2023-01-01 18:00:00)\', \'[2023-01-01 10:00:00+00,2023-01-01 18:00:00+00)\'), + (\'[5,15)\', \'[500,1500)\', \'[5.5,15.7)\', \'[2023-06-01,2023-12-31)\', \'[2023-06-01 10:00:00,2023-06-01 18:00:00)\', \'[2023-06-01 10:00:00+00,2023-06-01 18:00:00+00)\'), + (\'[20,30)\', \'[2000,3000)\', \'[20.5,30.7)\', \'[2023-12-01,2023-12-31)\', \'[2023-12-01 10:00:00,2023-12-01 18:00:00)\', \'[2023-12-01 10:00:00+00,2023-12-01 18:00:00+00)\') + ', self::DATABASE_SCHEMA); + + $this->connection->executeStatement($sql); + } + + /** + * @param array> $result + */ + protected function assertIds(array $expectedIds, array $result): void + { + $this->assertCount(\count($expectedIds), $result); + + $actualIds = []; + foreach ($result as $row) { + $this->assertIsArray($row); + $this->assertArrayHasKey('id', $row, 'Result row is expected to contain an id key'); + $this->assertIsInt($row['id']); + $actualIds[] = $row['id']; + } + + foreach ($expectedIds as $expectedId) { + $this->assertContains($expectedId, $actualIds, 'Expected id not found in result set'); + } + } + + /** + * Override to handle Range-specific value object comparison. + */ + protected function assertTypeValueEquals(mixed $expected, mixed $actual, string $typeName): void + { + if (!$expected instanceof RangeValueObject || !$actual instanceof RangeValueObject) { + throw new \InvalidArgumentException('assertTypeValueEquals in RangeTypeTestCase expects RangeValueObject arguments.'); + } + + $this->assertRangeEquals($expected, $actual, $typeName); + } + + /** + * Assert that two range value objects are equal. + * + * @param RangeValueObject<\DateTimeInterface|float|int> $expected + * @param RangeValueObject<\DateTimeInterface|float|int> $actual + */ + protected function assertRangeEquals(RangeValueObject $expected, RangeValueObject $actual, string $typeName): void + { + $this->assertEquals( + $expected->__toString(), + $actual->__toString(), + 'Range string representation mismatch for type '.$typeName + ); + + $this->assertEquals( + $expected->isEmpty(), + $actual->isEmpty(), + 'Range empty state mismatch for type '.$typeName + ); + } + + #[Test] + public function can_handle_null_values(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, null); + } + + /** + * Data-driven test for Range value objects. + * Subclasses should add #[DataProvider('provideValidTransformations')]. + * + * @param RangeValueObject<\DateTimeInterface|float|int> $rangeValueObject + */ + public function can_handle_range_values(string $testName, RangeValueObject $rangeValueObject): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, $rangeValueObject); + } + + /** + * @param array $expectedIds + */ + #[DataProvider('provideOperatorScenarios')] + #[Test] + public function can_evaluate_operator_scenarios(string $name, string $dql, array $expectedIds): void + { + $result = $this->executeDqlQuery($dql); + $this->assertIds($expectedIds, $result); + } + + /** + * @return array}> [name, dql, expectedIds] + */ + abstract public static function provideOperatorScenarios(): array; +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RealArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RealArrayTypeTest.php new file mode 100644 index 00000000..4d932226 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/RealArrayTypeTest.php @@ -0,0 +1,48 @@ +}> + */ + public static function provideValidTransformations(): array + { + return [ + 'simple real array' => ['simple real array', [1.5, 2.5, 3.5]], + 'real array with negative values' => ['real array with negative values', [-1.5, -2.5, -3.5]], + 'real array with high precision' => ['real array with high precision', [ + 1.123457, + 2.987654, + 3.141593, + ]], + 'real array with integers' => ['real array with integers', [1.0, 2.0, 3.0]], + 'real array with zero' => ['real array with zero', [0.0, 1.5, -1.5]], + 'empty real array' => ['empty real array', []], + 'real array with large numbers' => ['real array with large numbers', [3.402823e+6, -3.402823e+6]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/ScalarTypeTestCase.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/ScalarTypeTestCase.php new file mode 100644 index 00000000..f2e80d56 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/ScalarTypeTestCase.php @@ -0,0 +1,19 @@ +getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $this->runTypeTest($typeName, $columnType, null); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/SmallIntArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/SmallIntArrayTypeTest.php new file mode 100644 index 00000000..a190cb3b --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/SmallIntArrayTypeTest.php @@ -0,0 +1,42 @@ +}> + */ + public static function provideValidTransformations(): array + { + return [ + 'smallint array with positive values' => ['smallint array with positive values', [1, 2, 3, 4, 5]], + 'smallint array with negative values' => ['smallint array with negative values', [-100, -50, 0, 50, 100]], + 'smallint array with max values' => ['smallint array with max values', [-32768, 0, 32767]], + 'smallint array with zeros' => ['smallint array with zeros', [0, 0, 0]], + 'empty smallint array' => ['empty smallint array', []], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TestCase.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TestCase.php index 8defdac9..158dd006 100644 --- a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TestCase.php +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TestCase.php @@ -4,10 +4,74 @@ namespace Tests\Integration\MartinGeorgiev\Doctrine\DBAL\Types; +use Doctrine\DBAL\Types\Type; +use PHPUnit\Framework\Attributes\Test; use Tests\Integration\MartinGeorgiev\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { + /** + * The DBAL type name to test (e.g., 'inet', 'text[]', etc.). + */ + abstract protected function getTypeName(): string; + + /** + * The PostgreSQL column type name. + */ + abstract protected function getPostgresTypeName(): string; + + protected function assertTypeValueEquals(mixed $expected, mixed $actual, string $typeName): void + { + match (true) { + \is_array($expected) && \is_array($actual) => $this->assertEquals($expected, $actual, \sprintf('Array type %s round-trip failed', $typeName)), + default => $this->assertEquals($expected, $actual, \sprintf('Type %s round-trip failed', $typeName)) + }; + } + + protected function runTypeTest(string $typeName, string $columnType, mixed $testValue): void + { + $tableName = 'test_type_'.\strtolower(\str_replace([' ', '[]', '()'], ['_', '_array', ''], $columnType)); + $columnName = 'test_column'; + + try { + $this->createTestTableForDataType($tableName, $columnName, $columnType); + + // Insert test value using proper type conversion + $queryBuilder = $this->connection->createQueryBuilder(); + $queryBuilder + ->insert(self::DATABASE_SCHEMA.'.'.$tableName) + ->values([$columnName => ':value']) + ->setParameter('value', $testValue, $typeName); + + $queryBuilder->executeStatement(); + + // Query the value back + $queryBuilder = $this->connection->createQueryBuilder(); + $queryBuilder + ->select($columnName) + ->from(self::DATABASE_SCHEMA.'.'.$tableName) + ->where('id = 1'); + + $result = $queryBuilder->executeQuery(); + $row = $result->fetchAssociative(); + \assert(\is_array($row) && \array_key_exists($columnName, $row)); + + // Get the value with the correct type + $platform = $this->connection->getDatabasePlatform(); + $retrievedValue = Type::getType($typeName)->convertToPHPValue($row[$columnName], $platform); + + if ($testValue === null) { + $this->assertNull($retrievedValue); + + return; + } + + $this->assertTypeValueEquals($testValue, $retrievedValue, $typeName); + } finally { + $this->dropTestTableIfItExists($tableName); + } + } + protected function createTestTableForDataType(string $tableName, string $columnName, string $columnType): void { $this->dropTestTableIfItExists($tableName); @@ -22,4 +86,14 @@ protected function createTestTableForDataType(string $tableName, string $columnN $this->connection->executeStatement($sql); } + + #[Test] + public function type_will_be_registered(): void + { + $typeName = $this->getTypeName(); + + $this->assertTrue(Type::hasType($typeName), \sprintf('Type %s should be registered', $typeName)); + + Type::getType($typeName); + } } diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTypeTest.php new file mode 100644 index 00000000..0b7a96d0 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTypeTest.php @@ -0,0 +1,40 @@ + ['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']], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TsRangeTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TsRangeTypeTest.php new file mode 100644 index 00000000..0985b8f6 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TsRangeTypeTest.php @@ -0,0 +1,100 @@ + + */ + public static function provideValidTransformations(): array + { + return [ + 'simple tsrange' => ['simple tsrange', new TsRangeValueObject( + new \DateTimeImmutable('2023-01-01 10:00:00'), + new \DateTimeImmutable('2023-01-01 18:00:00'), + false, + false + )], + 'tsrange with inclusive bounds' => ['tsrange with inclusive bounds', new TsRangeValueObject( + new \DateTimeImmutable('2023-01-01 09:00:00'), + new \DateTimeImmutable('2023-01-01 17:00:00'), + true, + true + )], + 'tsrange with same day' => ['tsrange with same day', new TsRangeValueObject( + new \DateTimeImmutable('2023-06-15 08:00:00'), + new \DateTimeImmutable('2023-06-15 16:00:00'), + false, + false + )], + 'tsrange with midnight' => ['tsrange with midnight', new TsRangeValueObject( + new \DateTimeImmutable('2023-01-01 00:00:00'), + new \DateTimeImmutable('2023-01-01 23:59:59'), + false, + false + )], + ]; + } + + #[Test] + public function can_handle_infinite_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $tsRange = new TsRangeValueObject(null, null, false, false); + $this->runTypeTest($typeName, $columnType, $tsRange); + } + + #[Test] + public function can_handle_empty_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + // lower > upper shall result in an empty range + $tsRange = new TsRangeValueObject( + new \DateTimeImmutable('2023-01-01 18:00:00'), + new \DateTimeImmutable('2023-01-01 10:00:00'), + false, + false + ); + $this->runTypeTest($typeName, $columnType, $tsRange); + } + + /** + * @return array}> [name, dql, expectedIds] + */ + public static function provideOperatorScenarios(): array + { + return [ + 'is contained by tsrange' => ['is contained by tsrange', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE IS_CONTAINED_BY(\'[2023-01-01 12:00:00,2023-01-01 16:00:00)\', r.tsRange) = TRUE', [1]], + 'overlaps tsrange' => ['overlaps tsrange', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE OVERLAPS(r.tsRange, \'[2023-01-01 16:00:00,2023-01-01 20:00:00)\') = TRUE', [1]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TstzRangeTypeTest.php b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TstzRangeTypeTest.php new file mode 100644 index 00000000..4ec2cb49 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/TstzRangeTypeTest.php @@ -0,0 +1,94 @@ + + */ + public static function provideValidTransformations(): array + { + return [ + 'simple tstzrange' => ['simple tstzrange', new TstzRangeValueObject( + new \DateTimeImmutable('2023-01-01 10:00:00+00:00'), + new \DateTimeImmutable('2023-01-01 18:00:00+00:00'), + false, + false + )], + 'tstzrange with inclusive bounds' => ['tstzrange with inclusive bounds', new TstzRangeValueObject( + new \DateTimeImmutable('2023-01-01 09:00:00+00:00'), + new \DateTimeImmutable('2023-01-01 17:00:00+00:00'), + true, + true + )], + 'tstzrange with UTC' => ['tstzrange with UTC', new TstzRangeValueObject( + new \DateTimeImmutable('2023-06-15 08:00:00+00:00'), + new \DateTimeImmutable('2023-06-15 16:00:00+00:00'), + false, + false + )], + ]; + } + + #[Test] + public function can_handle_infinite_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + $tstzRange = new TstzRangeValueObject(null, null, false, false); + $this->runTypeTest($typeName, $columnType, $tstzRange); + } + + #[Test] + public function can_handle_empty_ranges(): void + { + $typeName = $this->getTypeName(); + $columnType = $this->getPostgresTypeName(); + + // lower > upper shall result in an empty range + $tstzRange = new TstzRangeValueObject( + new \DateTimeImmutable('2023-01-01 18:00:00+00:00'), + new \DateTimeImmutable('2023-01-01 10:00:00+00:00'), + false, + false + ); + $this->runTypeTest($typeName, $columnType, $tstzRange); + } + + /** + * @return array}> [name, dql, expectedIds] + */ + public static function provideOperatorScenarios(): array + { + return [ + 'is contained by tstzrange' => ['is contained by tstzrange', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE IS_CONTAINED_BY(\'[2023-01-01 12:00:00+00,2023-01-01 16:00:00+00)\', r.tstzRange) = TRUE', [1]], + 'overlaps tstzrange' => ['overlaps tstzrange', 'SELECT r.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsRanges r WHERE OVERLAPS(r.tstzRange, \'[2023-01-01 16:00:00+00,2023-01-01 20:00:00+00)\') = TRUE', [1]], + ]; + } +} diff --git a/tests/Integration/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformerTest.php b/tests/Integration/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformerTest.php index 64c65a41..54bb2434 100644 --- a/tests/Integration/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformerTest.php +++ b/tests/Integration/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformerTest.php @@ -7,6 +7,7 @@ use MartinGeorgiev\Utils\Exception\InvalidArrayFormatException; use MartinGeorgiev\Utils\PostgresArrayToPHPArrayTransformer; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use Tests\Integration\MartinGeorgiev\TestCase; class PostgresArrayToPHPArrayTransformerTest extends TestCase @@ -23,7 +24,8 @@ protected function setUp(): void * @param array{description: string, input: array} $testCase */ #[DataProvider('provideArrayTestCases')] - public function test_array_round_trip(array $testCase): void + #[Test] + public function array_round_trip(array $testCase): void { $id = $this->insertArray($testCase['input']); @@ -56,7 +58,8 @@ public static function provideArrayTestCases(): array } #[DataProvider('provideInvalidArrayFormats')] - public function test_invalid_array_formats_throw_exceptions(array $testCase): void + #[Test] + public function invalid_array_formats_throw_exceptions(array $testCase): void { $this->expectException(InvalidArrayFormatException::class); PostgresArrayToPHPArrayTransformer::transformPostgresArrayToPHPArray($testCase['input']); // @phpstan-ignore-line