Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions docs/AVAILABLE-TYPES.md
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 src/MartinGeorgiev/Doctrine/DBAL/Types/BaseFloatArray.php
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 src/MartinGeorgiev/Doctrine/DBAL/Types/DoublePrecisionArray.php
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';
}
}
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));
}
}
38 changes: 38 additions & 0 deletions src/MartinGeorgiev/Doctrine/DBAL/Types/RealArray.php
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';
}
}
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));
}
Comment on lines +43 to +51
Copy link

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 using isValidArrayItemForDatabase. It doesn't actually test the transformation process itself.

-    public function can_transform_from_php_value(float $phpValue, string $postgresValue): void
+    public function can_validate_php_value_for_database(float $phpValue, string $postgresValue): void
     {
         self::assertTrue($this->fixture->isValidArrayItemForDatabase($phpValue));
     }

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @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_from_php_value(float $phpValue, string $postgresValue): void
+ public function can_validate_php_value_for_database(float $phpValue, string $postgresValue): void
{
self::assertTrue($this->fixture->isValidArrayItemForDatabase($phpValue));
}
Suggested change
/**
* @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_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);
}


/**
* @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');
}
}
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');
}
}
Loading
Loading