Skip to content
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

Move ArrayColumnSchema and StructuredColumnSchema from db-pgsql #882

Merged
merged 8 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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(
Tigrov marked this conversation as resolved.
Show resolved Hide resolved
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