Skip to content

Commit

Permalink
Move ArrayColumnSchema and StructuredColumnSchema from db-pgsql (
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov authored Oct 8, 2024
1 parent 49c3a16 commit 42e1168
Show file tree
Hide file tree
Showing 19 changed files with 914 additions and 53 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Expression/ArrayExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
* @template-implements ArrayAccess<int, mixed>
* @template-implements IteratorAggregate<int>
*/
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)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Expression/Expression.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Expression/JsonExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
126 changes: 126 additions & 0 deletions src/Expression/StructuredExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Expression;

use Traversable;
use Yiisoft\Db\Schema\Column\ColumnSchemaInterface;

use function array_key_exists;
use function array_keys;
use function get_object_vars;
use function is_object;
use function iterator_to_array;

/**
* Represents a structured type SQL expression.
*
* @see https://en.wikipedia.org/wiki/Structured_type
*
* For example:
*
* ```php
* new StructuredExpression(['price' => 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<string, ColumnSchemaInterface> $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;
}
}
3 changes: 3 additions & 0 deletions src/Schema/Column/AbstractColumnFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
Expand Down Expand Up @@ -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,
};
Expand Down
175 changes: 175 additions & 0 deletions src/Schema/Column/ArrayColumnSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Schema\Column;

use Traversable;
use Yiisoft\Db\Constant\ColumnType;
use Yiisoft\Db\Constant\PhpType;
use Yiisoft\Db\Exception\NotSupportedException;
use Yiisoft\Db\Expression\ArrayExpression;
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Syntax\ParserToArrayInterface;

use function array_map;
use function array_walk_recursive;
use function is_array;
use function is_iterable;
use function is_string;
use function iterator_to_array;

class ArrayColumnSchema extends AbstractColumnSchema
{
/**
* @var ColumnSchemaInterface|null The column of an array item.
*/
private ColumnSchemaInterface|null $column = null;

/**
* @var int The dimension of array, must be greater than 0.
*/
private int $dimension = 1;

/**
* 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::ARRAY,
) {
parent::__construct($type);
}

/**
* Set column of an array item.
*/
public function column(ColumnSchemaInterface|null $column): static
{
$this->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;
}
}
26 changes: 26 additions & 0 deletions src/Schema/Column/ColumnBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ColumnSchemaInterface> $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`.
*/
Expand Down
Loading

0 comments on commit 42e1168

Please sign in to comment.