diff --git a/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md b/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md index 8eb697ef..467a4e4b 100644 --- a/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md +++ b/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md @@ -102,6 +102,8 @@ Complete documentation for PostgreSQL ltree (label tree) operations and hierarch - `CRC32` - CRC-32 checksum computation - `CRC32C` - CRC-32C checksum computation - `REVERSE_BYTES` - Reverse byte order for bytea values +- `UUID_EXTRACT_TIMESTAMP` - Extract timestamp from UUID v1 or v7 +- `UUID_EXTRACT_VERSION` - Extract version number from UUID - `UUIDV4` - Explicit UUID version 4 generation - `UUIDV7` - Generate timestamp-ordered UUIDs (version 7) for better database performance diff --git a/docs/MATHEMATICAL-FUNCTIONS.md b/docs/MATHEMATICAL-FUNCTIONS.md index db7a70b3..750395a5 100644 --- a/docs/MATHEMATICAL-FUNCTIONS.md +++ b/docs/MATHEMATICAL-FUNCTIONS.md @@ -36,6 +36,8 @@ This document covers PostgreSQL mathematical, utility, and miscellaneous functio | to_char | TO_CHAR | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToChar` | | to_number | TO_NUMBER | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToNumber` | +**Note**: `TO_NUMBER` supports Roman numeral conversion via the `RN` pattern (PostgreSQL 18+). + ## Utility and Miscellaneous Functions | PostgreSQL functions | Register for DQL as | Implemented by | @@ -46,6 +48,8 @@ This document covers PostgreSQL mathematical, utility, and miscellaneous functio | reverse (bytea) | REVERSE_BYTES | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ReverseBytes` | | row | ROW | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Row` | | row_to_json | ROW_TO_JSON | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RowToJson` | +| uuid_extract_timestamp | UUID_EXTRACT_TIMESTAMP | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\UuidExtractTimestamp` | +| uuid_extract_version | UUID_EXTRACT_VERSION | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\UuidExtractVersion` | | uuidv4 | UUIDV4 | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Uuidv4` | | uuidv7 | UUIDV7 | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Uuidv7` | | xmlagg | XML_AGG | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\XmlAgg` | diff --git a/src/MartinGeorgiev/Doctrine/DBAL/Types/BooleanArray.php b/src/MartinGeorgiev/Doctrine/DBAL/Types/BooleanArray.php index b9cf57cd..6895f1ce 100644 --- a/src/MartinGeorgiev/Doctrine/DBAL/Types/BooleanArray.php +++ b/src/MartinGeorgiev/Doctrine/DBAL/Types/BooleanArray.php @@ -41,6 +41,6 @@ public function convertToPHPValue($postgresArray, AbstractPlatform $platform): ? return null; } - return \array_map(static fn ($value): ?bool => $platform->convertFromBoolean($value), $phpArray); + return \array_map($platform->convertFromBoolean(...), $phpArray); } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumber.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumber.php index d528c819..754452c8 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumber.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumber.php @@ -7,7 +7,9 @@ /** * Implementation of PostgreSQL to_number(). * - * @see https://www.postgresql.org/docs/17/functions-formatting.html + * Supports Roman numeral conversion via RN pattern (PostgreSQL 18+). + * + * @see https://www.postgresql.org/docs/18/functions-formatting.html * @since 3.3.0 */ class ToNumber extends BaseFunction diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractTimestamp.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractTimestamp.php new file mode 100644 index 00000000..1322ba61 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractTimestamp.php @@ -0,0 +1,22 @@ + + */ +class UuidExtractTimestamp extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('uuid_extract_timestamp(%s)'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractVersion.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractVersion.php new file mode 100644 index 00000000..77e9ae3e --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractVersion.php @@ -0,0 +1,22 @@ + + */ +class UuidExtractVersion extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('uuid_extract_version(%s)'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Utils/PHPArrayToPostgresValueTransformer.php b/src/MartinGeorgiev/Utils/PHPArrayToPostgresValueTransformer.php index 705e8dd3..caa540e5 100644 --- a/src/MartinGeorgiev/Utils/PHPArrayToPostgresValueTransformer.php +++ b/src/MartinGeorgiev/Utils/PHPArrayToPostgresValueTransformer.php @@ -36,7 +36,7 @@ public static function transformToPostgresTextArray(array $phpArray): string /** @var array */ $processed = \array_map( - static fn (mixed $value): string => self::formatValue($value), + self::formatValue(...), $phpArray ); diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumberTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumberTest.php index 49fcd9c4..899a1760 100644 --- a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumberTest.php +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumberTest.php @@ -26,6 +26,26 @@ public function tonumber(): void $this->assertSame('-12454.8', $result[0]['result']); } + #[Test] + public function tonumber_converts_roman_numerals(): void + { + $this->requirePostgresVersion(180000, 'Roman numeral support in to_number'); + + $dql = "SELECT to_number('MCMXCIV', 'RN') as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsTexts t WHERE t.id = 1"; + $result = $this->executeDqlQuery($dql); + $this->assertSame('1994', $result[0]['result']); + } + + #[Test] + public function tonumber_converts_lowercase_roman_numerals(): void + { + $this->requirePostgresVersion(180000, 'Roman numeral support in to_number'); + + $dql = "SELECT to_number('xlii', 'rn') as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsTexts t WHERE t.id = 1"; + $result = $this->executeDqlQuery($dql); + $this->assertSame('42', $result[0]['result']); + } + #[Test] public function tonumber_throws_with_invalid_format(): void { diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractTimestampTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractTimestampTest.php new file mode 100644 index 00000000..7d183930 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractTimestampTest.php @@ -0,0 +1,66 @@ +requirePostgresVersion(170000, 'uuid_extract_timestamp function'); + } + + protected function getStringFunctions(): array + { + return [ + 'UUID_EXTRACT_TIMESTAMP' => UuidExtractTimestamp::class, + ]; + } + + #[Test] + public function can_extract_timestamp_from_uuid_v1(): void + { + $dql = "SELECT UUID_EXTRACT_TIMESTAMP('a0eebc99-9c0b-11d1-b465-00c04fd430c8') as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics t + WHERE t.id = 1"; + + $result = $this->executeDqlQuery($dql); + $timestamp = $result[0]['result']; + \assert(\is_string($timestamp)); + + $this->assertStringStartsWith('1998-02-02 20:23:12.90287', $timestamp); + } + + #[Test] + public function can_extract_timestamp_from_uuid_v7(): void + { + $this->requirePostgresVersion(180000, 'uuid_extract_timestamp function for UUID v7'); + + $dql = "SELECT UUID_EXTRACT_TIMESTAMP('018e7e39-9f42-7000-8000-000000000000') as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics t + WHERE t.id = 1"; + + $result = $this->executeDqlQuery($dql); + $timestamp = $result[0]['result']; + \assert(\is_string($timestamp)); + + $this->assertStringStartsWith('2024-03-27 04:44:49.346', $timestamp); + } + + #[Test] + public function returns_null_for_non_timestamped_uuid(): void + { + $dql = "SELECT UUID_EXTRACT_TIMESTAMP('550e8400-e29b-41d4-a716-446655440000') as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics t + WHERE t.id = 1"; + + $result = $this->executeDqlQuery($dql); + + $this->assertNull($result[0]['result']); + } +} diff --git a/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractVersionTest.php b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractVersionTest.php new file mode 100644 index 00000000..b604ca02 --- /dev/null +++ b/tests/Integration/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractVersionTest.php @@ -0,0 +1,60 @@ +requirePostgresVersion(170000, 'uuid_extract_version function'); + } + + protected function getStringFunctions(): array + { + return [ + 'UUID_EXTRACT_VERSION' => UuidExtractVersion::class, + ]; + } + + #[Test] + public function can_extract_version_from_uuid_v1(): void + { + $dql = "SELECT UUID_EXTRACT_VERSION('a0eebc99-9c0b-11d1-b465-00c04fd430c8') as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics t + WHERE t.id = 1"; + + $result = $this->executeDqlQuery($dql); + + $this->assertEquals(1, $result[0]['result']); + } + + #[Test] + public function can_extract_version_from_uuid_v4(): void + { + $dql = "SELECT UUID_EXTRACT_VERSION('550e8400-e29b-41d4-a716-446655440000') as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics t + WHERE t.id = 1"; + + $result = $this->executeDqlQuery($dql); + + $this->assertEquals(4, $result[0]['result']); + } + + #[Test] + public function can_extract_version_from_uuid_v7(): void + { + $dql = "SELECT UUID_EXTRACT_VERSION('018e7e39-9f42-7000-8000-000000000000') as result + FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsNumerics t + WHERE t.id = 1"; + + $result = $this->executeDqlQuery($dql); + + $this->assertEquals(7, $result[0]['result']); + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumberTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumberTest.php index 1e7ab224..72ce568e 100644 --- a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumberTest.php +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToNumberTest.php @@ -22,6 +22,7 @@ protected function getExpectedSqlStatements(): array { return [ 'converts text to number using format pattern' => "SELECT to_number(c0_.text1, '99G999D9S') AS sclr_0 FROM ContainsTexts c0_", + 'converts roman numerals to number' => "SELECT to_number(c0_.text1, 'RN') AS sclr_0 FROM ContainsTexts c0_", ]; } @@ -29,6 +30,7 @@ protected function getDqlStatements(): array { return [ 'converts text to number using format pattern' => \sprintf("SELECT TO_NUMBER(e.text1, '99G999D9S') FROM %s e", ContainsTexts::class), + 'converts roman numerals to number' => \sprintf("SELECT TO_NUMBER(e.text1, 'RN') FROM %s e", ContainsTexts::class), ]; } diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractTimestampTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractTimestampTest.php new file mode 100644 index 00000000..c723a43b --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractTimestampTest.php @@ -0,0 +1,32 @@ + UuidExtractTimestamp::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'extracts timestamp from uuid' => 'SELECT uuid_extract_timestamp(c0_.text1) AS sclr_0 FROM ContainsTexts c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'extracts timestamp from uuid' => \sprintf('SELECT UUID_EXTRACT_TIMESTAMP(e.text1) FROM %s e', ContainsTexts::class), + ]; + } +} diff --git a/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractVersionTest.php b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractVersionTest.php new file mode 100644 index 00000000..ee93fc2d --- /dev/null +++ b/tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UuidExtractVersionTest.php @@ -0,0 +1,32 @@ + UuidExtractVersion::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'extracts version from uuid' => 'SELECT uuid_extract_version(c0_.text1) AS sclr_0 FROM ContainsTexts c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'extracts version from uuid' => \sprintf('SELECT UUID_EXTRACT_VERSION(e.text1) FROM %s e', ContainsTexts::class), + ]; + } +}