Skip to content

Commit

Permalink
Add NumberType
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus committed Jan 6, 2025
1 parent b17cf6b commit 8f39a40
Show file tree
Hide file tree
Showing 10 changed files with 462 additions and 227 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
strategy:
matrix:
php-version:
- "8.3"
- "8.4"

steps:
- name: "Checkout code"
Expand All @@ -49,6 +49,8 @@ jobs:

- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--ignore-platform-req=php+"

- name: "Run a static analysis with phpstan/phpstan"
run: "vendor/bin/phpstan --error-format=checkstyle | cs2pr"
Expand All @@ -60,7 +62,7 @@ jobs:
strategy:
matrix:
php-version:
- "8.3"
- "8.4"

steps:
- name: Checkout code
Expand All @@ -75,6 +77,8 @@ jobs:

- name: Install dependencies with Composer
uses: ramsey/composer-install@v3
with:
composer-options: "--ignore-platform-req=php+"

- name: Run static analysis with Vimeo Psalm
run: vendor/bin/psalm --shepherd
Expand Down
6 changes: 3 additions & 3 deletions docs/en/reference/schema-representation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ The following options are considered to be fully portable across all database pl
in the platform.
- **fixed** (boolean): Whether a ``string`` or ``binary`` Doctrine type column has
a fixed length. Defaults to ``false``.
- **precision** (integer): The precision of a Doctrine ``decimal`` or ``float`` type
column that determines the overall maximum number of digits to be stored (including scale).
- **precision** (integer): The precision of a Doctrine ``decimal``, ``number`` or ``float``
type column that determines the overall maximum number of digits to be stored (including scale).
Defaults to ``10``.
- **scale** (integer): The exact number of decimal digits to be stored in a Doctrine
``decimal`` or ``float`` type column. Defaults to ``0``.
``decimal``, ``number`` or ``float`` type column. Defaults to ``0``.
- **customSchemaOptions** (array): Additional options for the column that are
supported by all vendors:

Expand Down
449 changes: 233 additions & 216 deletions docs/en/reference/types.rst

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions src/Types/NumberType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Types;

use BcMath\Number;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Exception\InvalidType;
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
use TypeError;
use ValueError;

use function is_float;

final class NumberType extends Type
{
/** {@inheritDoc} */
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return $platform->getDecimalTypeDeclarationSQL($column);
}

public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string
{
if ($value === null) {
return null;
}

if (! $value instanceof Number) {
throw InvalidType::new($value, static::class, ['null', Number::class]);
}

return (string) $value;
}

public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Number
{
if ($value === null) {
return null;
}

// SQLite might return a decimal as float.
if (is_float($value)) {
$value = (string) $value;
}

try {
return new Number($value);
} catch (TypeError | ValueError $e) {
throw ValueNotConvertible::new($value, static::class, previous: $e);
}
}
}
1 change: 1 addition & 0 deletions src/Types/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ abstract class Type
Types::DATETIMETZ_MUTABLE => DateTimeTzType::class,
Types::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class,
Types::DECIMAL => DecimalType::class,
Types::NUMBER => NumberType::class,
Types::ENUM => EnumType::class,
Types::FLOAT => FloatType::class,
Types::GUID => GuidType::class,
Expand Down
1 change: 1 addition & 0 deletions src/Types/Types.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final class Types
public const DATETIMETZ_MUTABLE = 'datetimetz';
public const DATETIMETZ_IMMUTABLE = 'datetimetz_immutable';
public const DECIMAL = 'decimal';
public const NUMBER = 'number';
public const FLOAT = 'float';
public const ENUM = 'enum';
public const GUID = 'guid';
Expand Down
16 changes: 10 additions & 6 deletions tests/Functional/Platform/AlterDecimalColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
use Doctrine\DBAL\Types\Types;
use PHPUnit\Framework\Attributes\DataProvider;

use function sprintf;

class AlterDecimalColumnTest extends FunctionalTestCase
{
#[DataProvider('scaleAndPrecisionProvider')]
public function testAlterPrecisionAndScale(int $newPrecision, int $newScale): void
public function testAlterPrecisionAndScale(int $newPrecision, int $newScale, string $type): void
{
$table = new Table('decimal_table');
$column = $table->addColumn('val', Types::DECIMAL, ['precision' => 16, 'scale' => 6]);
$column = $table->addColumn('val', $type, ['precision' => 16, 'scale' => 6]);

$this->dropAndCreateTable($table);

Expand All @@ -36,11 +38,13 @@ public function testAlterPrecisionAndScale(int $newPrecision, int $newScale): vo
self::assertSame($newScale, $column->getScale());
}

/** @return iterable<string,array{int,int}> */
/** @return iterable<string,array{int,int,Types::*}> */
public static function scaleAndPrecisionProvider(): iterable
{
yield 'Precision' => [12, 6];
yield 'Scale' => [16, 8];
yield 'Precision and scale' => [10, 4];
foreach ([Types::DECIMAL, Types::NUMBER] as $type) {
yield sprintf('Precision (%s)', $type) => [12, 6, $type];
yield sprintf('Scale (%s)', $type) => [16, 8, $type];
yield sprintf('Precision and scale (%s)', $type) => [10, 4, $type];
}
}
}
19 changes: 19 additions & 0 deletions tests/Functional/TypeConversionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

namespace Doctrine\DBAL\Tests\Functional;

use BcMath\Number;
use DateTime;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Tests\TestUtil;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;

use function str_repeat;

Expand Down Expand Up @@ -38,6 +41,7 @@ protected function setUp(): void
$table->addColumn('test_float', Types::FLOAT, ['notnull' => false]);
$table->addColumn('test_smallfloat', Types::SMALLFLOAT, ['notnull' => false]);
$table->addColumn('test_decimal', Types::DECIMAL, ['notnull' => false, 'scale' => 2, 'precision' => 10]);
$table->addColumn('test_number', Types::NUMBER, ['notnull' => false, 'scale' => 2, 'precision' => 10]);
$table->setPrimaryKey(['id']);

$this->dropAndCreateTable($table);
Expand Down Expand Up @@ -154,6 +158,21 @@ public static function toDateTimeProvider(): iterable
];
}

public function testDecimal(): void
{
self::assertSame('13.37', $this->processValue(Types::DECIMAL, '13.37'));
}

#[RequiresPhp('8.4')]
#[RequiresPhpExtension('bcmath')]
public function testNumber(): void
{
$originalValue = new Number('13.37');
$dbValue = $this->processValue(Types::NUMBER, $originalValue);

self::assertSame(0, $originalValue <=> $dbValue);
}

private function processValue(string $type, mixed $originalValue): mixed
{
$columnName = 'test_' . $type;
Expand Down
60 changes: 60 additions & 0 deletions tests/Functional/Types/NumberTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Functional\Types;

use BcMath\Number;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Types\Types;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\Attributes\TestWith;

#[RequiresPhp('8.4')]
#[RequiresPhpExtension('bcmath')]
final class NumberTest extends FunctionalTestCase
{
#[TestWith(['13.37'])]
#[TestWith(['13.0'])]
public function testInsertAndRetrieveNumber(string $numberAsString): void
{
$expected = new Number($numberAsString);

$table = new Table('number_table');
$table->addColumn('val', Types::NUMBER, ['precision' => 4, 'scale' => 2]);

$this->dropAndCreateTable($table);

$this->connection->insert(
'number_table',
['val' => $expected],
['val' => Types::NUMBER],
);

$value = $this->connection->convertToPHPValue(
$this->connection->fetchOne('SELECT val FROM number_table'),
Types::NUMBER,
);

self::assertInstanceOf(Number::class, $value);
self::assertSame(0, $expected <=> $value);
}

public function testCompareNumberTable(): void
{
$table = new Table('number_table');
$table->addColumn('val', Types::NUMBER, ['precision' => 4, 'scale' => 2]);

$this->dropAndCreateTable($table);

$schemaManager = $this->connection->createSchemaManager();

self::assertTrue(
$schemaManager->createComparator()
->compareTables($schemaManager->introspectTable('number_table'), $table)
->isEmpty(),
);
}
}
75 changes: 75 additions & 0 deletions tests/Types/NumberTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Types;

use BcMath\Number;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Exception\InvalidType;
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
use Doctrine\DBAL\Types\NumberType;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use stdClass;

#[RequiresPhp('8.4')]
#[RequiresPhpExtension('bcmath')]
final class NumberTest extends TestCase
{
private AbstractPlatform&MockObject $platform;
private NumberType $type;

protected function setUp(): void
{
$this->platform = $this->createMock(AbstractPlatform::class);
$this->type = new NumberType();
}

#[TestWith(['5.5'])]
#[TestWith(['5.5000'])]
#[TestWith([5.5])]
public function testDecimalConvertsToPHPValue(mixed $dbValue): void
{
$phpValue = $this->type->convertToPHPValue($dbValue, $this->platform);

self::assertInstanceOf(Number::class, $phpValue);
self::assertSame(0, $phpValue <=> new Number('5.5'));
}

public function testDecimalNullConvertsToPHPValue(): void
{
self::assertNull($this->type->convertToPHPValue(null, $this->platform));
}

public function testNumberConvertsToDecimalString(): void
{
self::assertSame('5.5', $this->type->convertToDatabaseValue(new Number('5.5'), $this->platform));
}

public function testNumberNullConvertsToNull(): void
{
self::assertNull($this->type->convertToDatabaseValue(null, $this->platform));
}

#[TestWith(['5.5'])]
#[TestWith([new stdClass()])]
public function testInvalidPhpValuesTriggerException(mixed $value): void
{
self::expectException(InvalidType::class);

$this->type->convertToDatabaseValue($value, $this->platform);
}

#[TestWith(['foo'])]
#[TestWith([true])]
public function testUnexpectedValuesReturnedByTheDatabaseTriggerException(mixed $value): void
{
self::expectException(ValueNotConvertible::class);

$this->type->convertToPHPValue($value, $this->platform);
}
}

0 comments on commit 8f39a40

Please sign in to comment.