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 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
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
103 changes: 103 additions & 0 deletions src/Expression/StructuredExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?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 is_iterable;
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 ColumnSchemaInterface[] $columns
* @psalm-param array<string, ColumnSchemaInterface> $columns
*/
public function __construct(
Tigrov marked this conversation as resolved.
Show resolved Hide resolved
private mixed $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 structured type column names
Tigrov marked this conversation as resolved.
Show resolved Hide resolved
* and values.
*/
public function getValue(): mixed
{
return $this->value;
}

/**
* Sorted values according to the order of structured type columns,
* indexed keys are replaced with column names,
Tigrov marked this conversation as resolved.
Show resolved Hide resolved
* missing elements are filled in with default values,
* excessive elements are removed.
*/
public function getNormalizedValue(): mixed
{
if (empty($this->columns) || !is_iterable($this->value)) {
return $this->value;
}

$normalized = [];
$value = $this->value;
$columnsNames = array_keys($this->columns);

if ($value instanceof Traversable) {
$value = iterator_to_array($value);
}

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
3 changes: 3 additions & 0 deletions src/Schema/Column/ColumnSchemaInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
* @psalm-type ColumnInfo = array{
* auto_increment?: bool|string,
* check?: string|null,
* column?: ColumnSchemaInterface|null,
* columns?: array<string, ColumnSchemaInterface>,
* 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,
Expand Down
Loading
Loading