diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2e06569..dcbc294f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,8 @@ - Enh #875: Ignore "Packets out of order..." warnings in `AbstractPdoCommand::internalExecute()` method (@Tigrov) - Enh #877: Separate column type constants (@Tigrov) - Enh #878: Realize `ColumnBuilder` class (@Tigrov) -- Enh #881: Refactor `ColumnSchemaInterface` and `AbstractColumnSchema` (@Tigrov) +- Enh #881: Refactor `ColumnSchemaInterface` and `AbstractColumnSchema` (@Tigrov) +- End #882: Move `ArrayColumnSchema` and `StructuredColumnSchema` classes from `db-pgsql` package (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/UPGRADE.md b/UPGRADE.md index 3eaab5f31..cb859fd69 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -97,6 +97,8 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace - `DoubleColumnSchema` for columns with fractional number type (float, double, decimal, money); - `StringColumnSchema` for columns with string or datetime type (char, string, text, datetime, timestamp, date, time); - `BinaryColumnSchema` for columns with binary type; +- `ArrayColumnSchema` for columns with array type; +- `StructuredColumnSchema` for columns with structured type (composite type in PostgreSQL); - `JsonColumnSchema` for columns with json type. ### New methods diff --git a/src/Expression/ArrayExpression.php b/src/Expression/ArrayExpression.php index 52e1d6788..7303e4e21 100644 --- a/src/Expression/ArrayExpression.php +++ b/src/Expression/ArrayExpression.php @@ -29,7 +29,7 @@ * @template-implements ArrayAccess * @template-implements IteratorAggregate */ -class ArrayExpression implements ExpressionInterface, ArrayAccess, Countable, IteratorAggregate +final class ArrayExpression implements ExpressionInterface, ArrayAccess, Countable, IteratorAggregate { public function __construct(private mixed $value = [], private string|null $type = null, private int $dimension = 1) { diff --git a/src/Expression/Expression.php b/src/Expression/Expression.php index 174f66399..374e202b2 100644 --- a/src/Expression/Expression.php +++ b/src/Expression/Expression.php @@ -26,7 +26,7 @@ * * @psalm-import-type ParamsType from ConnectionInterface */ -class Expression implements ExpressionInterface, Stringable +final class Expression implements ExpressionInterface, Stringable { /** * @psalm-param ParamsType $params diff --git a/src/Expression/JsonExpression.php b/src/Expression/JsonExpression.php index 884279ae4..71688d687 100644 --- a/src/Expression/JsonExpression.php +++ b/src/Expression/JsonExpression.php @@ -17,7 +17,7 @@ * new JsonExpression(['a' => 1, 'b' => 2]); // will be encoded to '{"a": 1, "b": 2}' * ``` */ -class JsonExpression implements ExpressionInterface, JsonSerializable +final class JsonExpression implements ExpressionInterface, JsonSerializable { public function __construct(protected mixed $value, private string|null $type = null) { diff --git a/src/Expression/StructuredExpression.php b/src/Expression/StructuredExpression.php new file mode 100644 index 000000000..ffbc6efc6 --- /dev/null +++ b/src/Expression/StructuredExpression.php @@ -0,0 +1,126 @@ + 10, 'currency_code' => 'USD']); + * ``` + * + * Will be encoded to `ROW(10, USD)` in PostgreSQL. + */ +final class StructuredExpression implements ExpressionInterface +{ + /** + * @param array|object $value The content of the structured type. It can be represented as + * - an associative `array` of column names and values; + * - an indexed `array` of column values in the order of structured type columns; + * - an `iterable` object that can be converted to an `array` using `iterator_to_array()`; + * - an `object` that can be converted to an `array` using `get_object_vars()`; + * - an `ExpressionInterface` object that represents a SQL expression. + * @param string|null $type The structured database type name. Defaults to `null` which means the type is not + * explicitly specified. Note that in the case where a type is not specified explicitly and DBMS cannot guess it + * from the context, SQL error will be raised. + * @param ColumnSchemaInterface[] $columns The structured type columns that are used for value normalization and type + * casting. + * + * @psalm-param array $columns + */ + public function __construct( + private array|object $value, + private string|null $type = null, + private array $columns = [], + ) { + } + + /** + * The structured type name. + * + * Defaults to `null` which means the type is not explicitly specified. + * + * Note that in the case where a type is not specified explicitly and DBMS cannot guess it from the context, + * SQL error will be raised. + */ + public function getType(): string|null + { + return $this->type; + } + + /** + * The structured type columns that are used for value normalization and type casting. + * + * @return ColumnSchemaInterface[] + */ + public function getColumns(): array + { + return $this->columns; + } + + /** + * The content of the structured type. It can be represented as + * - an associative `array` of column names and values; + * - an indexed `array` of column values in the order of structured type columns; + * - an `iterable` object that can be converted to an `array` using `iterator_to_array()`; + * - an `object` that can be converted to an `array` using `get_object_vars()`; + * - an `ExpressionInterface` object that represents a SQL expression. + */ + public function getValue(): array|object + { + return $this->value; + } + + /** + * Returns the normalized value of the structured type, where: + * - values sorted according to the order of structured type columns; + * - indexed keys are replaced with column names; + * - missing elements are filled in with default values; + * - excessive elements are removed. + * + * If the structured type columns are not specified or the value is an `ExpressionInterface` object, + * it will be returned as is. + */ + public function getNormalizedValue(): array|object + { + $value = $this->value; + + if (empty($this->columns) || $value instanceof ExpressionInterface) { + return $value; + } + + if (is_object($value)) { + $value = $value instanceof Traversable + ? iterator_to_array($value) + : get_object_vars($value); + } + + $normalized = []; + $columnsNames = array_keys($this->columns); + + foreach ($columnsNames as $i => $columnsName) { + $normalized[$columnsName] = match (true) { + array_key_exists($columnsName, $value) => $value[$columnsName], + array_key_exists($i, $value) => $value[$i], + default => $this->columns[$columnsName]->getDefaultValue(), + }; + } + + return $normalized; + } +} diff --git a/src/Schema/Column/AbstractColumnFactory.php b/src/Schema/Column/AbstractColumnFactory.php index 6cb1329f0..194d5c567 100644 --- a/src/Schema/Column/AbstractColumnFactory.php +++ b/src/Schema/Column/AbstractColumnFactory.php @@ -102,6 +102,7 @@ public function fromType(string $type, array $info = []): ColumnSchemaInterface ColumnType::FLOAT => new DoubleColumnSchema($type), ColumnType::DOUBLE => new DoubleColumnSchema($type), ColumnType::BINARY => new BinaryColumnSchema($type), + ColumnType::STRUCTURED => new StructuredColumnSchema($type), ColumnType::JSON => new JsonColumnSchema($type), default => new StringColumnSchema($type), }; @@ -162,6 +163,8 @@ protected function isType(string $type): bool ColumnType::TIMESTAMP, ColumnType::DATE, ColumnType::TIME, + ColumnType::ARRAY, + ColumnType::STRUCTURED, ColumnType::JSON => true, default => false, }; diff --git a/src/Schema/Column/ArrayColumnSchema.php b/src/Schema/Column/ArrayColumnSchema.php new file mode 100644 index 000000000..1f93a5d45 --- /dev/null +++ b/src/Schema/Column/ArrayColumnSchema.php @@ -0,0 +1,175 @@ +column = $column; + return $this; + } + + /** + * @return ColumnSchemaInterface the column of an array item. + */ + public function getColumn(): ColumnSchemaInterface + { + if ($this->column === null) { + $this->column = new StringColumnSchema(); + $this->column->dbType($this->getDbType()); + $this->column->enumValues($this->getEnumValues()); + $this->column->scale($this->getScale()); + $this->column->size($this->getSize()); + } + + return $this->column; + } + + /** + * Set dimension of an array, must be greater than 0. + */ + public function dimension(int $dimension): static + { + $this->dimension = $dimension; + return $this; + } + + /** + * @return int the dimension of the array. + */ + public function getDimension(): int + { + return $this->dimension; + } + + public function getPhpType(): string + { + return PhpType::ARRAY; + } + + public function dbTypecast(mixed $value): ExpressionInterface|null + { + if ($value === null || $value instanceof ExpressionInterface) { + return $value; + } + + if ($this->dimension === 1 && is_array($value)) { + $value = array_map($this->getColumn()->dbTypecast(...), $value); + } else { + $value = $this->dbTypecastArray($value, $this->dimension); + } + + return new ArrayExpression($value, $this->getDbType() ?? $this->getColumn()->getDbType(), $this->dimension); + } + + public function phpTypecast(mixed $value): array|null + { + if (is_string($value)) { + $value = $this->getParser()->parse($value); + } + + if (!is_array($value)) { + return null; + } + + $column = $this->getColumn(); + + if ($column->getType() === ColumnType::STRING) { + return $value; + } + + if ($this->dimension === 1 && $column->getType() !== ColumnType::JSON) { + return array_map($column->phpTypecast(...), $value); + } + + array_walk_recursive($value, function (string|null &$val) use ($column): void { + $val = $column->phpTypecast($val); + }); + + return $value; + } + + /** + * Recursively converts array values for use in a db query. + * + * @param mixed $value The array or iterable object. + * @param int $dimension The array dimension. Should be more than 0. + * + * @return array|null Converted values. + */ + protected function dbTypecastArray(mixed $value, int $dimension): array|null + { + if ($value === null) { + return null; + } + + if (!is_iterable($value)) { + return []; + } + + if ($dimension <= 1) { + return array_map( + $this->getColumn()->dbTypecast(...), + $value instanceof Traversable + ? iterator_to_array($value, false) + : $value + ); + } + + $items = []; + + foreach ($value as $val) { + $items[] = $this->dbTypecastArray($val, $dimension - 1); + } + + return $items; + } +} diff --git a/src/Schema/Column/ColumnBuilder.php b/src/Schema/Column/ColumnBuilder.php index ababbb70d..6dfb22722 100644 --- a/src/Schema/Column/ColumnBuilder.php +++ b/src/Schema/Column/ColumnBuilder.php @@ -227,6 +227,32 @@ public static function time(int|null $size = 0): ColumnSchemaInterface ->size($size); } + /** + * Builds a column with the abstract type `array`. + * + * @param ColumnSchemaInterface|null $column The column schema of the array elements. + */ + public static function array(ColumnSchemaInterface|null $column = null): ColumnSchemaInterface + { + return (new ArrayColumnSchema(ColumnType::ARRAY)) + ->column($column); + } + + /** + * Builds a column with the abstract type `structured`. + * + * @param string|null $dbType The DB type of the column. + * @param ColumnSchemaInterface[] $columns The columns (name -> instance) that the structured column should contain. + * + * @psalm-param array $columns + */ + public static function structured(string|null $dbType = null, array $columns = []): ColumnSchemaInterface + { + return (new StructuredColumnSchema(ColumnType::STRUCTURED)) + ->dbType($dbType) + ->columns($columns); + } + /** * Builds a column with the abstract type `json`. */ diff --git a/src/Schema/Column/ColumnSchemaInterface.php b/src/Schema/Column/ColumnSchemaInterface.php index 9e55230ee..4c5ff4388 100644 --- a/src/Schema/Column/ColumnSchemaInterface.php +++ b/src/Schema/Column/ColumnSchemaInterface.php @@ -15,10 +15,13 @@ * @psalm-type ColumnInfo = array{ * auto_increment?: bool|string, * check?: string|null, + * column?: ColumnSchemaInterface|null, + * columns?: array, * comment?: string|null, * computed?: bool|string, * db_type?: string|null, * default_value?: mixed, + * dimension?: int|string, * enum_values?: array|null, * extra?: string|null, * primary_key?: bool|string, diff --git a/src/Schema/Column/StructuredColumnSchema.php b/src/Schema/Column/StructuredColumnSchema.php new file mode 100644 index 000000000..241132989 --- /dev/null +++ b/src/Schema/Column/StructuredColumnSchema.php @@ -0,0 +1,109 @@ + + */ + private array $columns = []; + + /** + * Returns the parser for the column value. + */ + protected function getParser(): ParserToArrayInterface + { + throw new NotSupportedException(__METHOD__ . '() is not supported. Use concrete DBMS implementation.'); + } + + /** + * @psalm-param ColumnType::* $type + */ + public function __construct( + string $type = ColumnType::STRUCTURED, + ) { + parent::__construct($type); + } + + /** + * Set columns of the structured type. + * + * @param ColumnSchemaInterface[] $columns The metadata of the structured type columns. + * @psalm-param array $columns + */ + public function columns(array $columns): static + { + $this->columns = $columns; + return $this; + } + + /** + * Get the metadata of the structured type columns. + * + * @return ColumnSchemaInterface[] + */ + public function getColumns(): array + { + return $this->columns; + } + + public function getPhpType(): string + { + return PhpType::ARRAY; + } + + public function dbTypecast(mixed $value): mixed + { + if ($value === null || $value instanceof ExpressionInterface) { + return $value; + } + + /** @psalm-suppress MixedArgument */ + return new StructuredExpression($value, $this->getDbType(), $this->columns); + } + + public function phpTypecast(mixed $value): array|null + { + if (is_string($value)) { + $value = $this->getParser()->parse($value); + } + + if (!is_array($value)) { + return null; + } + + if (empty($this->columns)) { + return $value; + } + + $fields = []; + $columnNames = array_keys($this->columns); + + /** @psalm-var int|string $columnName */ + foreach ($value as $columnName => $item) { + $columnName = $columnNames[$columnName] ?? $columnName; + + if (isset($this->columns[$columnName])) { + $fields[$columnName] = $this->columns[$columnName]->phpTypecast($item); + } else { + $fields[$columnName] = $item; + } + } + + return $fields; + } +} diff --git a/src/Syntax/ParserToArrayInterface.php b/src/Syntax/ParserToArrayInterface.php new file mode 100644 index 000000000..1b30bbb8f --- /dev/null +++ b/src/Syntax/ParserToArrayInterface.php @@ -0,0 +1,20 @@ +assertInstanceOf($expectedInstanceOf, $column); $this->assertSame($expectedType, $column->getType()); $this->assertSame($dbType, $column->getDbType()); + + $db->close(); } /** @@ -51,6 +53,8 @@ public function testFromDefinition( foreach ($columnMethodResults as $method => $result) { $this->assertSame($result, $column->$method()); } + + $db->close(); } /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnFactoryProvider::pseudoTypes */ @@ -74,8 +78,10 @@ public function testFromPseudoType( ); foreach ($columnMethodResults as $method => $result) { - $this->assertSame($result, $column->$method()); + $this->assertEquals($result, $column->$method()); } + + $db->close(); } /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnFactoryProvider::types */ @@ -88,6 +94,8 @@ public function testFromType(string $type, string $expectedType, string $expecte $this->assertInstanceOf($expectedInstanceOf, $column); $this->assertSame($expectedType, $column->getType()); + + $db->close(); } public function testFromDefinitionWithExtra(): void @@ -101,5 +109,7 @@ public function testFromDefinitionWithExtra(): void $this->assertSame('char', $column->getType()); $this->assertSame(1, $column->getSize()); $this->assertSame('NOT NULL UNIQUE', $column->getExtra()); + + $db->close(); } } diff --git a/tests/Db/Expression/StructuredExpressionTest.php b/tests/Db/Expression/StructuredExpressionTest.php new file mode 100644 index 000000000..2c04ac6e6 --- /dev/null +++ b/tests/Db/Expression/StructuredExpressionTest.php @@ -0,0 +1,40 @@ + ColumnBuilder::money(10, 2), + 'currency' => ColumnBuilder::char(3), + ]; + + $expression = new StructuredExpression([5, 'USD'], 'currency_money_structured', $columns); + + $this->assertSame([5, 'USD'], $expression->getValue()); + $this->assertSame('currency_money_structured', $expression->getType()); + $this->assertSame($columns, $expression->getColumns()); + } + + /** @dataProvider \Yiisoft\Db\Tests\Provider\StructuredTypeProvider::normolizedValues */ + public function testGetNormalizedValue(mixed $value, mixed $expected, array $columns): void + { + $expression = new StructuredExpression($value, 'currency_money_structured', $columns); + + $this->assertSame($expected, $expression->getNormalizedValue()); + } +} diff --git a/tests/Db/Schema/ColumnSchemaTest.php b/tests/Db/Schema/ColumnSchemaTest.php index 71852b1a3..e76a1fef7 100644 --- a/tests/Db/Schema/ColumnSchemaTest.php +++ b/tests/Db/Schema/ColumnSchemaTest.php @@ -4,8 +4,19 @@ namespace Yiisoft\Db\Tests\Db\Schema; +use ArrayIterator; use PHPUnit\Framework\TestCase; use Yiisoft\Db\Constraint\ForeignKeyConstraint; +use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Expression\ArrayExpression; +use Yiisoft\Db\Expression\Expression; +use Yiisoft\Db\Expression\StructuredExpression; +use Yiisoft\Db\Schema\Column\ArrayColumnSchema; +use Yiisoft\Db\Schema\Column\ColumnBuilder; +use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; +use Yiisoft\Db\Schema\Column\IntegerColumnSchema; +use Yiisoft\Db\Schema\Column\StringColumnSchema; +use Yiisoft\Db\Schema\Column\StructuredColumnSchema; use Yiisoft\Db\Tests\Support\Stub\ColumnSchema; /** @@ -300,4 +311,125 @@ public function testUnsigned(): void $this->assertTrue($column->isUnsigned()); } + + public function testArrayColumnGetColumn(): void + { + $arrayCol = new ArrayColumnSchema(); + $intCol = new IntegerColumnSchema(); + + $this->assertInstanceOf(StringColumnSchema::class, $arrayCol->getColumn()); + $this->assertSame($arrayCol, $arrayCol->column($intCol)); + $this->assertSame($intCol, $arrayCol->getColumn()); + + $arrayCol->column(null); + + $this->assertInstanceOf(StringColumnSchema::class, $arrayCol->getColumn()); + } + + public function testArrayColumnGetDimension(): void + { + $arrayCol = new ArrayColumnSchema(); + + $this->assertSame(1, $arrayCol->getDimension()); + + $arrayCol->dimension(2); + $this->assertSame(2, $arrayCol->getDimension()); + } + + /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnSchemaProvider::dbTypecastArrayColumns */ + public function testArrayColumnDbTypecast(ColumnSchemaInterface $column, array $values): void + { + $arrayCol = ColumnBuilder::array($column); + + foreach ($values as [$dimension, $expected, $value]) { + $arrayCol->dimension($dimension); + $dbValue = $arrayCol->dbTypecast($value); + + $this->assertInstanceOf(ArrayExpression::class, $dbValue); + $this->assertSame($dimension, $dbValue->getDimension()); + + $this->assertEquals($expected, $dbValue->getValue()); + } + } + + public function testArrayColumnDbTypecastSimple() + { + $arrayCol = new ArrayColumnSchema(); + + $this->assertNull($arrayCol->dbTypecast(null)); + $this->assertEquals(new ArrayExpression([]), $arrayCol->dbTypecast('')); + $this->assertEquals(new ArrayExpression([1, 2, 3]), $arrayCol->dbTypecast(new ArrayIterator([1, 2, 3]))); + $this->assertSame($expression = new Expression('expression'), $arrayCol->dbTypecast($expression)); + } + + public function testArrayColumnPhpTypecast() + { + $arrayCol = new ArrayColumnSchema(); + + $this->assertNull($arrayCol->phpTypecast(null)); + $this->assertNull($arrayCol->phpTypecast(1)); + $this->assertSame([], $arrayCol->phpTypecast([])); + $this->assertSame(['1', '2', '3'], $arrayCol->phpTypecast(['1', '2', '3'])); + + $this->expectException(NotSupportedException::class); + $this->expectExceptionMessage( + 'Yiisoft\Db\Schema\Column\ArrayColumnSchema::getParser() is not supported. Use concrete DBMS implementation.' + ); + + $arrayCol->phpTypecast('{1,2,3}'); + } + + public function testStructuredColumnGetColumns(): void + { + $structuredCol = new StructuredColumnSchema(); + $columns = [ + 'value' => ColumnBuilder::money(), + 'currency_code' => ColumnBuilder::char(3), + ]; + + $this->assertSame([], $structuredCol->getColumns()); + $this->assertSame($structuredCol, $structuredCol->columns($columns)); + $this->assertSame($columns, $structuredCol->getColumns()); + } + + public function testStructuredColumnDbTypecast(): void + { + $structuredCol = new StructuredColumnSchema(); + $expression = new Expression('expression'); + $structuredExpression = new StructuredExpression(['value' => 1, 'currency_code' => 'USD']); + + $this->assertNull($structuredCol->dbTypecast(null)); + $this->assertSame($expression, $structuredCol->dbTypecast($expression)); + $this->assertSame($structuredExpression, $structuredCol->dbTypecast($structuredExpression)); + $this->assertEquals($structuredExpression, $structuredCol->dbTypecast(['value' => 1, 'currency_code' => 'USD'])); + } + + public function testStructuredColumnPhpTypecast(): void + { + $structuredCol = new StructuredColumnSchema(); + $columns = [ + 'int' => ColumnBuilder::integer(), + 'bool' => ColumnBuilder::boolean(), + ]; + + $this->assertNull($structuredCol->phpTypecast(null)); + $this->assertNull($structuredCol->phpTypecast(1)); + $this->assertSame( + ['int' => '1', 'bool' => '1'], + $structuredCol->phpTypecast(['int' => '1', 'bool' => '1']) + ); + + $structuredCol->columns($columns); + $this->assertSame( + ['int' => 1, 'bool' => true], + $structuredCol->phpTypecast(['int' => '1', 'bool' => '1']) + ); + + $this->expectException(NotSupportedException::class); + $this->expectExceptionMessage( + 'Yiisoft\Db\Schema\Column\StructuredColumnSchema::getParser() is not supported. Use concrete DBMS implementation.' + ); + + $structuredCol->phpTypecast('(1,true)'); + } } diff --git a/tests/Provider/ColumnBuilderProvider.php b/tests/Provider/ColumnBuilderProvider.php index ea4d997f9..0572bc219 100644 --- a/tests/Provider/ColumnBuilderProvider.php +++ b/tests/Provider/ColumnBuilderProvider.php @@ -5,13 +5,16 @@ namespace Yiisoft\Db\Tests\Provider; use Yiisoft\Db\Constant\ColumnType; +use Yiisoft\Db\Schema\Column\ArrayColumnSchema; use Yiisoft\Db\Schema\Column\BinaryColumnSchema; use Yiisoft\Db\Schema\Column\BitColumnSchema; use Yiisoft\Db\Schema\Column\BooleanColumnSchema; +use Yiisoft\Db\Schema\Column\ColumnBuilder; use Yiisoft\Db\Schema\Column\DoubleColumnSchema; use Yiisoft\Db\Schema\Column\IntegerColumnSchema; use Yiisoft\Db\Schema\Column\JsonColumnSchema; use Yiisoft\Db\Schema\Column\StringColumnSchema; +use Yiisoft\Db\Schema\Column\StructuredColumnSchema; class ColumnBuilderProvider { @@ -32,56 +35,76 @@ class ColumnBuilderProvider public static function buildingMethods(): array { + $column = ColumnBuilder::string(); + $columns = ['value' => ColumnBuilder::money(), 'currency_code' => ColumnBuilder::char(3)]; + return [ // building method, args, expected instance of, expected type, expected column method results - ['primaryKey', [], IntegerColumnSchema::class, ColumnType::INTEGER, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], - ['primaryKey', [false], IntegerColumnSchema::class, ColumnType::INTEGER, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], - ['smallPrimaryKey', [], IntegerColumnSchema::class, ColumnType::SMALLINT, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], - ['smallPrimaryKey', [false], IntegerColumnSchema::class, ColumnType::SMALLINT, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], - ['bigPrimaryKey', [], IntegerColumnSchema::class, ColumnType::BIGINT, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], - ['bigPrimaryKey', [false], IntegerColumnSchema::class, ColumnType::BIGINT, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], - ['uuidPrimaryKey', [], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], - ['uuidPrimaryKey', [true], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], - ['boolean', [], BooleanColumnSchema::class, ColumnType::BOOLEAN], - ['bit', [], BitColumnSchema::class, ColumnType::BIT], - ['bit', [1], BitColumnSchema::class, ColumnType::BIT, ['getSize' => 1]], - ['tinyint', [], IntegerColumnSchema::class, ColumnType::TINYINT], - ['tinyint', [1], IntegerColumnSchema::class, ColumnType::TINYINT, ['getSize' => 1]], - ['smallint', [], IntegerColumnSchema::class, ColumnType::SMALLINT], - ['smallint', [1], IntegerColumnSchema::class, ColumnType::SMALLINT, ['getSize' => 1]], - ['integer', [], IntegerColumnSchema::class, ColumnType::INTEGER], - ['integer', [1], IntegerColumnSchema::class, ColumnType::INTEGER, ['getSize' => 1]], - ['bigint', [], IntegerColumnSchema::class, ColumnType::BIGINT], - ['bigint', [1], IntegerColumnSchema::class, ColumnType::BIGINT, ['getSize' => 1]], - ['float', [], DoubleColumnSchema::class, ColumnType::FLOAT], - ['float', [8], DoubleColumnSchema::class, ColumnType::FLOAT, ['getSize' => 8]], - ['float', [8, 2], DoubleColumnSchema::class, ColumnType::FLOAT, ['getSize' => 8, 'getScale' => 2]], - ['double', [], DoubleColumnSchema::class, ColumnType::DOUBLE], - ['double', [8], DoubleColumnSchema::class, ColumnType::DOUBLE, ['getSize' => 8]], - ['double', [8, 2], DoubleColumnSchema::class, ColumnType::DOUBLE, ['getSize' => 8, 'getScale' => 2]], - ['decimal', [], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 10, 'getScale' => 0]], - ['decimal', [8], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 8, 'getScale' => 0]], - ['decimal', [8, 2], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 8, 'getScale' => 2]], - ['money', [], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 19, 'getScale' => 4]], - ['money', [8], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 8, 'getScale' => 4]], - ['money', [8, 2], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 8, 'getScale' => 2]], - ['char', [], StringColumnSchema::class, ColumnType::CHAR, ['getSize' => 1]], - ['char', [100], StringColumnSchema::class, ColumnType::CHAR, ['getSize' => 100]], - ['string', [], StringColumnSchema::class, ColumnType::STRING, ['getSize' => 255]], - ['string', [100], StringColumnSchema::class, ColumnType::STRING, ['getSize' => 100]], - ['text', [], StringColumnSchema::class, ColumnType::TEXT], - ['text', [5000], StringColumnSchema::class, ColumnType::TEXT, ['getSize' => 5000]], - ['binary', [], BinaryColumnSchema::class, ColumnType::BINARY], - ['binary', [8], BinaryColumnSchema::class, ColumnType::BINARY, ['getSize' => 8]], - ['uuid', [], StringColumnSchema::class, ColumnType::UUID], - ['datetime', [], StringColumnSchema::class, ColumnType::DATETIME, ['getSize' => 0]], - ['datetime', [3], StringColumnSchema::class, ColumnType::DATETIME, ['getSize' => 3]], - ['timestamp', [], StringColumnSchema::class, ColumnType::TIMESTAMP, ['getSize' => 0]], - ['timestamp', [3], StringColumnSchema::class, ColumnType::TIMESTAMP, ['getSize' => 3]], - ['date', [], StringColumnSchema::class, ColumnType::DATE], - ['time', [], StringColumnSchema::class, ColumnType::TIME, ['getSize' => 0]], - ['time', [3], StringColumnSchema::class, ColumnType::TIME, ['getSize' => 3]], - ['json', [], JsonColumnSchema::class, ColumnType::JSON], + 'primaryKey()' => ['primaryKey', [], IntegerColumnSchema::class, ColumnType::INTEGER, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], + 'primaryKey(false)' => ['primaryKey', [false], IntegerColumnSchema::class, ColumnType::INTEGER, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], + 'smallPrimaryKey()' => ['smallPrimaryKey', [], IntegerColumnSchema::class, ColumnType::SMALLINT, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], + 'smallPrimaryKey(false)' => ['smallPrimaryKey', [false], IntegerColumnSchema::class, ColumnType::SMALLINT, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], + 'bigPrimaryKey()' => ['bigPrimaryKey', [], IntegerColumnSchema::class, ColumnType::BIGINT, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], + 'bigPrimaryKey(false)' => ['bigPrimaryKey', [false], IntegerColumnSchema::class, ColumnType::BIGINT, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], + 'uuidPrimaryKey()' => ['uuidPrimaryKey', [], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], + 'uuidPrimaryKey(true)' => ['uuidPrimaryKey', [true], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], + 'boolean()' => ['boolean', [], BooleanColumnSchema::class, ColumnType::BOOLEAN], + 'bit()' => ['bit', [], BitColumnSchema::class, ColumnType::BIT], + 'bit(1)' => ['bit', [1], BitColumnSchema::class, ColumnType::BIT, ['getSize' => 1]], + 'tinyint()' => ['tinyint', [], IntegerColumnSchema::class, ColumnType::TINYINT], + 'tinyint(1)' => ['tinyint', [1], IntegerColumnSchema::class, ColumnType::TINYINT, ['getSize' => 1]], + 'smallint()' => ['smallint', [], IntegerColumnSchema::class, ColumnType::SMALLINT], + 'smallint(1)' => ['smallint', [1], IntegerColumnSchema::class, ColumnType::SMALLINT, ['getSize' => 1]], + 'integer()' => ['integer', [], IntegerColumnSchema::class, ColumnType::INTEGER], + 'integer(1)' => ['integer', [1], IntegerColumnSchema::class, ColumnType::INTEGER, ['getSize' => 1]], + 'bigint()' => ['bigint', [], IntegerColumnSchema::class, ColumnType::BIGINT], + 'bigint(1)' => ['bigint', [1], IntegerColumnSchema::class, ColumnType::BIGINT, ['getSize' => 1]], + 'float()' => ['float', [], DoubleColumnSchema::class, ColumnType::FLOAT], + 'float(8)' => ['float', [8], DoubleColumnSchema::class, ColumnType::FLOAT, ['getSize' => 8]], + 'float(8,2)' => ['float', [8, 2], DoubleColumnSchema::class, ColumnType::FLOAT, ['getSize' => 8, 'getScale' => 2]], + 'double()' => ['double', [], DoubleColumnSchema::class, ColumnType::DOUBLE], + 'double(8)' => ['double', [8], DoubleColumnSchema::class, ColumnType::DOUBLE, ['getSize' => 8]], + 'double(8,2)' => ['double', [8, 2], DoubleColumnSchema::class, ColumnType::DOUBLE, ['getSize' => 8, 'getScale' => 2]], + 'decimal()' => ['decimal', [], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 10, 'getScale' => 0]], + 'decimal(8)' => ['decimal', [8], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 8, 'getScale' => 0]], + 'decimal(8,2)' => ['decimal', [8, 2], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 8, 'getScale' => 2]], + 'money()' => ['money', [], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 19, 'getScale' => 4]], + 'money(8)' => ['money', [8], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 8, 'getScale' => 4]], + 'money(8,2)' => ['money', [8, 2], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 8, 'getScale' => 2]], + 'char()' => ['char', [], StringColumnSchema::class, ColumnType::CHAR, ['getSize' => 1]], + 'char(100)' => ['char', [100], StringColumnSchema::class, ColumnType::CHAR, ['getSize' => 100]], + 'string()' => ['string', [], StringColumnSchema::class, ColumnType::STRING, ['getSize' => 255]], + 'string(100)' => ['string', [100], StringColumnSchema::class, ColumnType::STRING, ['getSize' => 100]], + 'text()' => ['text', [], StringColumnSchema::class, ColumnType::TEXT], + 'text(5000)' => ['text', [5000], StringColumnSchema::class, ColumnType::TEXT, ['getSize' => 5000]], + 'binary()' => ['binary', [], BinaryColumnSchema::class, ColumnType::BINARY], + 'binary(8)' => ['binary', [8], BinaryColumnSchema::class, ColumnType::BINARY, ['getSize' => 8]], + 'uuid()' => ['uuid', [], StringColumnSchema::class, ColumnType::UUID], + 'datetime()' => ['datetime', [], StringColumnSchema::class, ColumnType::DATETIME, ['getSize' => 0]], + 'datetime(3)' => ['datetime', [3], StringColumnSchema::class, ColumnType::DATETIME, ['getSize' => 3]], + 'timestamp()' => ['timestamp', [], StringColumnSchema::class, ColumnType::TIMESTAMP, ['getSize' => 0]], + 'timestamp(3)' => ['timestamp', [3], StringColumnSchema::class, ColumnType::TIMESTAMP, ['getSize' => 3]], + 'date()' => ['date', [], StringColumnSchema::class, ColumnType::DATE], + 'time()' => ['time', [], StringColumnSchema::class, ColumnType::TIME, ['getSize' => 0]], + 'time(3)' => ['time', [3], StringColumnSchema::class, ColumnType::TIME, ['getSize' => 3]], + 'array()' => ['array', [], ArrayColumnSchema::class, ColumnType::ARRAY], + 'array($column)' => ['array', [$column], ArrayColumnSchema::class, ColumnType::ARRAY, ['getColumn' => $column]], + 'structured()' => ['structured', [], StructuredColumnSchema::class, ColumnType::STRUCTURED], + "structured('money_currency')" => [ + 'structured', + ['money_currency'], + StructuredColumnSchema::class, + ColumnType::STRUCTURED, + ['getDbType' => 'money_currency'], + ], + "structured('money_currency',\$columns)" => [ + 'structured', + ['money_currency', $columns], + StructuredColumnSchema::class, + ColumnType::STRUCTURED, + ['getDbType' => 'money_currency', 'getColumns' => $columns], + ], + 'json()' => ['json', [], JsonColumnSchema::class, ColumnType::JSON], ]; } } diff --git a/tests/Provider/ColumnFactoryProvider.php b/tests/Provider/ColumnFactoryProvider.php index cdb647b19..efd7106b7 100644 --- a/tests/Provider/ColumnFactoryProvider.php +++ b/tests/Provider/ColumnFactoryProvider.php @@ -13,6 +13,7 @@ use Yiisoft\Db\Schema\Column\IntegerColumnSchema; use Yiisoft\Db\Schema\Column\JsonColumnSchema; use Yiisoft\Db\Schema\Column\StringColumnSchema; +use Yiisoft\Db\Schema\Column\StructuredColumnSchema; class ColumnFactoryProvider { @@ -64,6 +65,7 @@ public static function types(): array 'timestamp' => [ColumnType::TIMESTAMP, ColumnType::TIMESTAMP, StringColumnSchema::class], 'time' => [ColumnType::TIME, ColumnType::TIME, StringColumnSchema::class], 'date' => [ColumnType::DATE, ColumnType::DATE, StringColumnSchema::class], + 'structured' => [ColumnType::STRUCTURED, ColumnType::STRUCTURED, StructuredColumnSchema::class], 'json' => [ColumnType::JSON, ColumnType::JSON, JsonColumnSchema::class], ]; } diff --git a/tests/Provider/ColumnSchemaProvider.php b/tests/Provider/ColumnSchemaProvider.php index 9bcbe4bca..6f00d19b2 100644 --- a/tests/Provider/ColumnSchemaProvider.php +++ b/tests/Provider/ColumnSchemaProvider.php @@ -12,14 +12,18 @@ use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Constant\PhpType; +use Yiisoft\Db\Expression\StructuredExpression; +use Yiisoft\Db\Schema\Column\ArrayColumnSchema; use Yiisoft\Db\Schema\Column\BigIntColumnSchema; use Yiisoft\Db\Schema\Column\BinaryColumnSchema; use Yiisoft\Db\Schema\Column\BitColumnSchema; use Yiisoft\Db\Schema\Column\BooleanColumnSchema; +use Yiisoft\Db\Schema\Column\ColumnBuilder; use Yiisoft\Db\Schema\Column\DoubleColumnSchema; use Yiisoft\Db\Schema\Column\IntegerColumnSchema; use Yiisoft\Db\Schema\Column\JsonColumnSchema; use Yiisoft\Db\Schema\Column\StringColumnSchema; +use Yiisoft\Db\Schema\Column\StructuredColumnSchema; use function fopen; @@ -36,6 +40,8 @@ public static function predefinedTypes(): array 'binary' => [BinaryColumnSchema::class, ColumnType::BINARY, PhpType::MIXED], 'bit' => [BitColumnSchema::class, ColumnType::BIT, PhpType::INT], 'boolean' => [BooleanColumnSchema::class, ColumnType::BOOLEAN, PhpType::BOOL], + 'array' => [ArrayColumnSchema::class, ColumnType::ARRAY, PhpType::ARRAY], + 'structured' => [StructuredColumnSchema::class, ColumnType::STRUCTURED, PhpType::ARRAY], 'json' => [JsonColumnSchema::class, ColumnType::JSON, PhpType::MIXED], ]; } @@ -245,6 +251,121 @@ public static function phpTypecastColumns(): array ]; } + public static function dbTypecastArrayColumns() + { + return [ + // [column, values] + ColumnType::BOOLEAN => [ + ColumnBuilder::boolean(), + [ + // [dimension, expected, typecast value] + [1, [true, true, true, false, false, false, null], [true, 1, '1', false, 0, '0', null]], + [2, [[true, true, true, false, false, false, null]], [[true, 1, '1', false, 0, '0', null]]], + ], + ], + ColumnType::BIT => [ + ColumnBuilder::bit(), + [ + [1, [0b1011, 1001, null], [0b1011, '1001', null]], + [2, [[0b1011, 1001, null]], [[0b1011, '1001', null]]], + ], + ], + ColumnType::INTEGER => [ + ColumnBuilder::integer(), + [ + [1, [1, 2, 3, null], [1, 2.0, '3', null]], + [2, [[1, 2], [3], null], [[1, 2.0], ['3'], null]], + [2, [null, null], [null, null]], + ], + ], + ColumnType::BIGINT => [ + new BigIntColumnSchema(), + [ + [1, ['1', '2', '3', '9223372036854775807'], [1, 2.0, '3', '9223372036854775807']], + [2, [['1', '2'], ['3'], ['9223372036854775807']], [[1, 2.0], ['3'], ['9223372036854775807']]], + ], + ], + ColumnType::DOUBLE => [ + ColumnBuilder::double(), + [ + [1, [1.0, 2.2, 3.3, null], [1, 2.2, '3.3', null]], + [2, [[1.0, 2.2], [3.3, null]], [[1, 2.2], ['3.3', null]]], + ], + ], + ColumnType::STRING => [ + ColumnBuilder::string(), + [ + [1, ['1', '2', '1', '0', '', null], [1, '2', true, false, '', null]], + [2, [['1', '2', '1', '0'], [''], [null]], [[1, '2', true, false], [''], [null]]], + ], + ], + ColumnType::BINARY => [ + ColumnBuilder::binary(), + [ + [1, [ + '1', + new Param("\x10", PDO::PARAM_LOB), + $resource = fopen('php://memory', 'rb'), + null, + ], [1, "\x10", $resource, null]], + [2, [[ + '1', + new Param("\x10", PDO::PARAM_LOB), + $resource = fopen('php://memory', 'rb'), + null, + ]], [[1, "\x10", $resource, null]]], + ], + ], + ColumnType::JSON => [ + ColumnBuilder::json(), + [ + [1, [ + new JsonExpression([1, 2, 3]), + new JsonExpression(['key' => 'value']), + new JsonExpression(['key' => 'value']), + null, + ], [[1, 2, 3], ['key' => 'value'], new JsonExpression(['key' => 'value']), null]], + [2, [ + [ + new JsonExpression([1, 2, 3]), + new JsonExpression(['key' => 'value']), + new JsonExpression(['key' => 'value']), + null, + ], + null, + ], [[[1, 2, 3], ['key' => 'value'], new JsonExpression(['key' => 'value']), null], null]], + ], + ], + ColumnType::STRUCTURED => [ + ColumnBuilder::structured('structured_type'), + [ + [ + 1, + [ + new StructuredExpression(['value' => 10, 'currency' => 'USD'], 'structured_type'), + null, + ], + [ + ['value' => 10, 'currency' => 'USD'], + null, + ], + ], + [ + 2, + [[ + new StructuredExpression(['value' => 10, 'currency' => 'USD'], 'structured_type'), + null, + ]], + [[ + ['value' => 10, 'currency' => 'USD'], + null, + ]], + ], + ], + ], + ]; + } + public static function load(): array { return [ diff --git a/tests/Provider/StructuredTypeProvider.php b/tests/Provider/StructuredTypeProvider.php new file mode 100644 index 000000000..3123897cb --- /dev/null +++ b/tests/Provider/StructuredTypeProvider.php @@ -0,0 +1,68 @@ + ColumnBuilder::money(10, 2)->defaultValue(5.0), + 'currency_code' => ColumnBuilder::char(3)->defaultValue('USD'), + ]; + + return [ + 'Sort according to `$columns` order' => [ + ['currency_code' => 'USD', 'value' => 10.0], + ['value' => 10.0, 'currency_code' => 'USD'], + $price5UsdColumns, + ], + 'Remove excessive elements' => [ + ['value' => 10.0, 'currency_code' => 'USD', 'excessive' => 'element'], + ['value' => 10.0, 'currency_code' => 'USD'], + $price5UsdColumns, + ], + 'Fill default values for skipped fields' => [ + ['currency_code' => 'CNY'], + ['value' => 5.0, 'currency_code' => 'CNY'], + $price5UsdColumns, + ], + 'Fill default values and column names for skipped indexed fields' => [ + [10.0], + ['value' => 10.0, 'currency_code' => 'USD'], + $price5UsdColumns, + ], + 'Fill default values and column names for an iterable object' => [ + new TraversableObject([10.0]), + ['value' => 10.0, 'currency_code' => 'USD'], + $price5UsdColumns, + ], + 'Fill default values for an object' => [ + (object) ['currency_code' => 'CNY'], + ['value' => 5.0, 'currency_code' => 'CNY'], + $price5UsdColumns, + ], + 'Fill default values for empty array' => [ + [], + ['value' => 5.0, 'currency_code' => 'USD'], + $price5UsdColumns, + ], + 'Do not normalize expressions' => [ + $expression = new Expression('(5,USD)'), + $expression, + $price5UsdColumns, + ], + 'Do not normalize with empty columns' => [ + [10.0], + [10.0], + [], + ], + ]; + } +}