-
-
Notifications
You must be signed in to change notification settings - Fork 56
feat: add support for arrays of REAL and DOUBLE PRECISION
#307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
3cbad4f
feat: add support for arrays of `REAL` and `DOUBLE PRECISION`
martin-georgiev 49634db
cs-fixer ;)
martin-georgiev c13c75e
Address some of AI's code-review comments and tidy up loose ends. Sti…
martin-georgiev 5a668bf
fix some of the AI generated tests
martin-georgiev f6b52ac
add test scenario for PHP floats that end with a decimal and have no …
martin-georgiev 59c63a3
detailed exception handling
martin-georgiev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,13 @@ | ||
| # Available types | ||
|
|
||
| | PostgreSQL type | Implemented by | | ||
| |---|---| | ||
| | _bool | `MartinGeorgiev\Doctrine\DBAL\Types\BooleanArray` | | ||
| | _int2 | `MartinGeorgiev\Doctrine\DBAL\Types\SmallIntArray` | | ||
| | _int4 | `MartinGeorgiev\Doctrine\DBAL\Types\IntegerArray` | | ||
| | _int8 | `MartinGeorgiev\Doctrine\DBAL\Types\BigIntArray` | | ||
| | _text | `MartinGeorgiev\Doctrine\DBAL\Types\TextArray` | | ||
| | _jsonb | `MartinGeorgiev\Doctrine\DBAL\Types\JsonbArray` | | ||
| | jsonb | `MartinGeorgiev\Doctrine\DBAL\Types\Jsonb` | | ||
| | PostgreSQL type in practical use | PostgreSQL internal system catalogue name | Implemented by | | ||
| |---|---|---| | ||
| | bool[] | _bool | `MartinGeorgiev\Doctrine\DBAL\Types\BooleanArray` | | ||
| | smallint[] | _int2 | `MartinGeorgiev\Doctrine\DBAL\Types\SmallIntArray` | | ||
| | integer[] | _int4 | `MartinGeorgiev\Doctrine\DBAL\Types\IntegerArray` | | ||
| | bigint[] | _int8 | `MartinGeorgiev\Doctrine\DBAL\Types\BigIntArray` | | ||
| | real[] | _float4 | `MartinGeorgiev\Doctrine\DBAL\Types\RealArray` | | ||
| | double precision[] | _float8 | `MartinGeorgiev\Doctrine\DBAL\Types\DoublePrecisionArray` | | ||
| | text[] | _text | `MartinGeorgiev\Doctrine\DBAL\Types\TextArray` | | ||
| | jsonb[] | _jsonb | `MartinGeorgiev\Doctrine\DBAL\Types\JsonbArray` | | ||
| | jsonb | jsonb | `MartinGeorgiev\Doctrine\DBAL\Types\Jsonb` | |
115 changes: 115 additions & 0 deletions
115
src/MartinGeorgiev/Doctrine/DBAL/Types/BaseFloatArray.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace MartinGeorgiev\Doctrine\DBAL\Types; | ||
|
|
||
| use MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidFloatValueException; | ||
|
|
||
| /** | ||
| * @since 3.0 | ||
| * | ||
| * @author Martin Georgiev <martin.georgiev@gmail.com> | ||
| */ | ||
| abstract class BaseFloatArray extends BaseArray | ||
| { | ||
| private const FLOAT_REGEX = '/^-?\d*\.?\d+(?:[eE][-+]?\d+)?$/'; | ||
|
|
||
| abstract protected function getMinValue(): string; | ||
|
|
||
| abstract protected function getMaxValue(): string; | ||
|
|
||
| abstract protected function getMaxPrecision(): int; | ||
|
|
||
| abstract protected function getMinAbsoluteValue(): string; | ||
|
|
||
| public function isValidArrayItemForDatabase(mixed $item): bool | ||
| { | ||
| $isNotANumber = !\is_float($item) && !\is_int($item) && !\is_string($item); | ||
| if ($isNotANumber) { | ||
| return false; | ||
| } | ||
|
|
||
| $stringValue = (string) $item; | ||
| if (!\preg_match(self::FLOAT_REGEX, $stringValue)) { | ||
| return false; | ||
| } | ||
|
|
||
| $floatValue = (float) $stringValue; | ||
|
|
||
| // For scientific notation, convert to standard decimal form before checking precision | ||
| if (\str_contains($stringValue, 'e') || \str_contains($stringValue, 'E')) { | ||
| $standardForm = \sprintf('%.'.($this->getMaxPrecision() + 1).'f', $floatValue); | ||
| $parts = \explode('.', $standardForm); | ||
| if (isset($parts[1]) && \strlen($parts[1]) > $this->getMaxPrecision()) { | ||
| return false; | ||
| } | ||
| } elseif (\str_contains($stringValue, '.')) { | ||
| $parts = \explode('.', $stringValue); | ||
| if (\strlen($parts[1]) > $this->getMaxPrecision()) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| $isBelowMinValue = $floatValue < (float) $this->getMinValue(); | ||
| if ($isBelowMinValue) { | ||
| return false; | ||
| } | ||
|
|
||
| $isAboveMaxValue = $floatValue > (float) $this->getMaxValue(); | ||
| if ($isAboveMaxValue) { | ||
| return false; | ||
| } | ||
|
|
||
| // Check if value is too close to zero | ||
| $absoluteValue = \abs($floatValue); | ||
| $isTooCloseToZero = $absoluteValue > 0 && $absoluteValue < (float) $this->getMinAbsoluteValue(); | ||
|
|
||
| return !$isTooCloseToZero; | ||
| } | ||
|
|
||
| public function transformArrayItemForPHP(mixed $item): ?float | ||
| { | ||
| if ($item === null) { | ||
| return null; | ||
| } | ||
|
|
||
| $isNotANumberCandidate = !\is_float($item) && !\is_int($item) && !\is_string($item); | ||
| if ($isNotANumberCandidate) { | ||
| throw InvalidFloatValueException::forValueThatIsNotAValidPHPFloat($item); | ||
| } | ||
|
|
||
| $stringValue = (string) $item; | ||
| if (!\preg_match(self::FLOAT_REGEX, $stringValue)) { | ||
| throw InvalidFloatValueException::forValueThatIsNotAValidPHPFloat($item); | ||
| } | ||
|
|
||
| $floatValue = (float) $stringValue; | ||
|
|
||
| // Check if value is too close to zero | ||
| $absValue = \abs($floatValue); | ||
| if ($absValue > 0 && $absValue < (float) $this->getMinAbsoluteValue()) { | ||
| throw InvalidFloatValueException::forValueThatIsTooCloseToZero($item, static::TYPE_NAME); | ||
| } | ||
|
|
||
| if ($floatValue < (float) $this->getMinValue() || $floatValue > (float) $this->getMaxValue()) { | ||
| throw InvalidFloatValueException::forValueThatIsNotAValidPHPFloat($item); | ||
| } | ||
|
|
||
| // Scientific notation is valid for input as long as the resulting number | ||
| // when converted to decimal doesn't exceed precision limits | ||
| if (\str_contains($stringValue, 'e') || \str_contains($stringValue, 'E')) { | ||
| return $floatValue; | ||
| } | ||
|
|
||
| // For regular decimal notation, check precision | ||
| if (\str_contains($stringValue, '.')) { | ||
| $parts = \explode('.', $stringValue); | ||
| if (\strlen($parts[1]) > $this->getMaxPrecision()) { | ||
| throw InvalidFloatValueException::forValueThatExceedsMaximumPrecision($item, $this->getMaxPrecision(), static::TYPE_NAME); | ||
| } | ||
| } | ||
|
|
||
| return $floatValue; | ||
| } | ||
| } |
38 changes: 38 additions & 0 deletions
38
src/MartinGeorgiev/Doctrine/DBAL/Types/DoublePrecisionArray.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace MartinGeorgiev\Doctrine\DBAL\Types; | ||
|
|
||
| /** | ||
| * Implementation of PostgreSQL DOUBLE PRECISION[] data type. | ||
| * | ||
| * @see https://www.postgresql.org/docs/current/datatype-numeric.html | ||
| * @since 3.0 | ||
| * | ||
| * @author Martin Georgiev <martin.georgiev@gmail.com> | ||
| */ | ||
| class DoublePrecisionArray extends BaseFloatArray | ||
| { | ||
| protected const TYPE_NAME = 'double precision[]'; | ||
|
|
||
| protected function getMinValue(): string | ||
| { | ||
| return '-1.7976931348623157E+308'; | ||
| } | ||
|
|
||
| protected function getMaxValue(): string | ||
| { | ||
| return '1.7976931348623157E+308'; | ||
| } | ||
|
|
||
| protected function getMaxPrecision(): int | ||
| { | ||
| return 15; | ||
| } | ||
|
|
||
| protected function getMinAbsoluteValue(): string | ||
| { | ||
| return '2.2250738585072014E-308'; | ||
| } | ||
| } |
30 changes: 30 additions & 0 deletions
30
src/MartinGeorgiev/Doctrine/DBAL/Types/Exceptions/InvalidFloatValueException.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace MartinGeorgiev\Doctrine\DBAL\Types\Exceptions; | ||
|
|
||
| use Doctrine\DBAL\Types\ConversionException; | ||
|
|
||
| /** | ||
| * @since 3.0 | ||
| * | ||
| * @author Martin Georgiev <martin.georgiev@gmail.com> | ||
| */ | ||
| class InvalidFloatValueException extends ConversionException | ||
| { | ||
| public static function forValueThatIsNotAValidPHPFloat(mixed $value): self | ||
| { | ||
| return new self(\sprintf('Given value of %s content cannot be transformed to valid PHP float.', \var_export($value, true))); | ||
| } | ||
|
|
||
| public static function forValueThatIsTooCloseToZero(mixed $value, string $type): self | ||
| { | ||
| return new self(\sprintf('Given value of %s is too close to zero for PostgreSQL %s type', \var_export($value, true), $type)); | ||
| } | ||
|
|
||
| public static function forValueThatExceedsMaximumPrecision(mixed $value, int $maxPrecision, string $type): self | ||
| { | ||
| return new self(\sprintf('Given value of %s exceeds maximum precision of %d for PostgreSQL %s type ', \var_export($value, true), $maxPrecision, $type)); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace MartinGeorgiev\Doctrine\DBAL\Types; | ||
|
|
||
| /** | ||
| * Implementation of PostgreSQL REAL[] data type. | ||
| * | ||
| * @see https://www.postgresql.org/docs/current/datatype-numeric.html | ||
| * @since 3.0 | ||
| * | ||
| * @author Martin Georgiev <martin.georgiev@gmail.com> | ||
| */ | ||
| class RealArray extends BaseFloatArray | ||
| { | ||
| protected const TYPE_NAME = 'real[]'; | ||
|
|
||
| protected function getMinValue(): string | ||
| { | ||
| return '-3.4028235E+38'; | ||
| } | ||
|
|
||
| protected function getMaxValue(): string | ||
| { | ||
| return '3.4028235E+38'; | ||
| } | ||
|
|
||
| protected function getMaxPrecision(): int | ||
| { | ||
| return 6; | ||
| } | ||
|
|
||
| protected function getMinAbsoluteValue(): string | ||
| { | ||
| return '1.17549435E-38'; | ||
| } | ||
| } |
81 changes: 81 additions & 0 deletions
81
tests/MartinGeorgiev/Doctrine/DBAL/Types/BaseFloatArrayTestCase.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Tests\MartinGeorgiev\Doctrine\DBAL\Types; | ||
|
|
||
| use Doctrine\DBAL\Types\ConversionException; | ||
| use MartinGeorgiev\Doctrine\DBAL\Types\BaseFloatArray; | ||
| use PHPUnit\Framework\TestCase; | ||
|
|
||
| abstract class BaseFloatArrayTestCase extends TestCase | ||
| { | ||
| protected BaseFloatArray $fixture; | ||
|
|
||
| /** | ||
| * @test | ||
| * | ||
| * @dataProvider provideInvalidTransformations | ||
| */ | ||
| public function can_detect_invalid_for_transformation_php_value(mixed $phpValue): void | ||
| { | ||
| self::assertFalse($this->fixture->isValidArrayItemForDatabase($phpValue)); | ||
| } | ||
|
|
||
| /** | ||
| * @return list<mixed> | ||
| */ | ||
| public static function provideInvalidTransformations(): array | ||
| { | ||
| return [ | ||
| [true], | ||
| [null], | ||
| ['string'], | ||
| [[]], | ||
| [new \stdClass()], | ||
| ['1e'], // Invalid scientific notation format | ||
| ['e1'], // Invalid scientific notation format | ||
| ['1.23.45'], // Invalid number format | ||
| ['not_a_number'], | ||
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * @test | ||
| * | ||
| * @dataProvider provideValidTransformations | ||
| */ | ||
| public function can_transform_from_php_value(float $phpValue, string $postgresValue): void | ||
| { | ||
| self::assertTrue($this->fixture->isValidArrayItemForDatabase($phpValue)); | ||
| } | ||
|
|
||
| /** | ||
| * @test | ||
| * | ||
| * @dataProvider provideValidTransformations | ||
| */ | ||
| public function can_transform_to_php_value(float $phpValue, string $postgresValue): void | ||
| { | ||
| self::assertEquals($phpValue, $this->fixture->transformArrayItemForPHP($postgresValue)); | ||
| } | ||
|
|
||
| /** | ||
| * @return list<array{ | ||
| * phpValue: float, | ||
| * postgresValue: string | ||
| * }> | ||
| */ | ||
| abstract public static function provideValidTransformations(): array; | ||
|
|
||
| /** | ||
| * @test | ||
| */ | ||
| public function throws_conversion_exception_when_invalid_array_item_value(): void | ||
| { | ||
| $this->expectException(ConversionException::class); | ||
| $this->expectExceptionMessage("Given value of '1.e234' content cannot be transformed to valid PHP float."); | ||
|
|
||
| $this->fixture->transformArrayItemForPHP('1.e234'); | ||
| } | ||
| } | ||
66 changes: 66 additions & 0 deletions
66
tests/MartinGeorgiev/Doctrine/DBAL/Types/DoublePrecisionArrayTest.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Tests\MartinGeorgiev\Doctrine\DBAL\Types; | ||
|
|
||
| use MartinGeorgiev\Doctrine\DBAL\Types\DoublePrecisionArray; | ||
| use MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidFloatValueException; | ||
|
|
||
| class DoublePrecisionArrayTest extends BaseFloatArrayTestCase | ||
| { | ||
| protected function setUp(): void | ||
| { | ||
| parent::setUp(); | ||
| $this->fixture = new DoublePrecisionArray(); | ||
| } | ||
|
|
||
| /** | ||
| * @test | ||
| */ | ||
| public function has_name(): void | ||
| { | ||
| self::assertEquals('double precision[]', $this->fixture->getName()); | ||
| } | ||
|
|
||
| public static function provideInvalidTransformations(): array | ||
| { | ||
| return \array_merge(parent::provideInvalidTransformations(), [ | ||
| ['1.7976931348623157E+309'], // Too large | ||
| ['-1.7976931348623157E+309'], // Too small | ||
| ['1.123456789012345678'], // Too many decimal places (>15) | ||
| ['2.2250738585072014E-309'], // Too close to zero | ||
| ['-2.2250738585072014E-309'], // Too close to zero (negative) | ||
| ['not_a_number'], | ||
| ['1.23.45'], | ||
| ['1e'], // Invalid scientific notation | ||
| ['e1'], // Invalid scientific notation | ||
| ]); | ||
| } | ||
|
|
||
| /** | ||
| * @return array<int, array{phpValue: float, postgresValue: string}> | ||
| */ | ||
| public static function provideValidTransformations(): array | ||
| { | ||
| return [ | ||
| ['phpValue' => 1.23e4, 'postgresValue' => '1.23e4'], | ||
| ['phpValue' => 1.23e-4, 'postgresValue' => '1.23e-4'], | ||
| ['phpValue' => 1.234567890123456, 'postgresValue' => '1.234567890123456'], | ||
| ['phpValue' => 1., 'postgresValue' => '1.0'], | ||
| ['phpValue' => 1.0, 'postgresValue' => '1.0'], | ||
| ['phpValue' => -1.0, 'postgresValue' => '-1.0'], | ||
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * @test | ||
| */ | ||
| public function throws_conversion_exception_when_value_is_too_close_to_zero(): void | ||
| { | ||
| $this->expectException(InvalidFloatValueException::class); | ||
| $this->expectExceptionMessage("Given value of '1.18E-308' is too close to zero for PostgreSQL double precision[] type"); | ||
|
|
||
| $this->fixture->transformArrayItemForPHP('1.18E-308'); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
The test method name doesn't match the assertion.
The method is named
can_transform_from_php_value, but it only tests if the value is valid for the database usingisValidArrayItemForDatabase. It doesn't actually test the transformation process itself.Alternatively, modify the test to actually check the transformation:
public function can_transform_from_php_value(float $phpValue, string $postgresValue): void { self::assertTrue($this->fixture->isValidArrayItemForDatabase($phpValue)); + // Test actual transformation + $transformedValue = $this->fixture->convertToDatabaseValue([$phpValue], \Doctrine\DBAL\Platforms\PostgreSQLPlatform::class); + self::assertStringContainsString($postgresValue, $transformedValue); }📝 Committable suggestion