From 85c27c55fdfe3032ea937a150eeb7dde9ea6139c Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Thu, 30 May 2024 15:55:08 +0700 Subject: [PATCH] ColumnSchema classes for performance of typecasting (#315) --- CHANGELOG.md | 2 + src/Column/ArrayColumnSchema.php | 181 ++++++++++ src/Column/BigIntColumnSchema.php | 12 + src/Column/BinaryColumnSchema.php | 24 ++ src/Column/BitColumnSchema.php | 48 +++ src/Column/BooleanColumnSchema.php | 19 ++ src/Column/IntegerColumnSchema.php | 12 + src/Column/SequenceColumnSchemaInterface.php | 20 ++ src/Column/SequenceColumnSchemaTrait.php | 23 ++ src/Column/StructuredColumnSchema.php | 88 +++++ .../StructuredColumnSchemaInterface.php | 25 ++ src/ColumnSchema.php | 296 ---------------- src/Schema.php | 158 ++++++--- src/StructuredExpression.php | 2 +- tests/ColumnSchemaTest.php | 141 ++++++-- tests/Provider/ColumnSchemaProvider.php | 317 ++++++++++++++++++ tests/Provider/SchemaProvider.php | 65 +++- tests/Provider/StructuredTypeProvider.php | 5 +- tests/Support/ColumnSchemaBuilder.php | 19 +- 19 files changed, 1058 insertions(+), 399 deletions(-) create mode 100644 src/Column/ArrayColumnSchema.php create mode 100644 src/Column/BigIntColumnSchema.php create mode 100644 src/Column/BinaryColumnSchema.php create mode 100644 src/Column/BitColumnSchema.php create mode 100644 src/Column/BooleanColumnSchema.php create mode 100644 src/Column/IntegerColumnSchema.php create mode 100644 src/Column/SequenceColumnSchemaInterface.php create mode 100644 src/Column/SequenceColumnSchemaTrait.php create mode 100644 src/Column/StructuredColumnSchema.php create mode 100644 src/Column/StructuredColumnSchemaInterface.php delete mode 100644 src/ColumnSchema.php create mode 100644 tests/Provider/ColumnSchemaProvider.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ff2b27051..947145e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## 2.0.0 under development - Enh #336: Implement `SqlParser` and `ExpressionBuilder` driver classes (@Tigrov) +- Enh #315: Implement `ColumnSchemaInterface` classes according to the data type of database table columns + for type casting performance. Related with yiisoft/db#752 (@Tigrov) - Chg #348: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/src/Column/ArrayColumnSchema.php b/src/Column/ArrayColumnSchema.php new file mode 100644 index 000000000..7d558c1e6 --- /dev/null +++ b/src/Column/ArrayColumnSchema.php @@ -0,0 +1,181 @@ +column = $column; + } + + /** + * @return ColumnSchemaInterface the column of an array item. + */ + public function getColumn(): ColumnSchemaInterface + { + if ($this->column === null) { + $type = $this->getType(); + $phpType = $this->getPhpType(); + + $this->column = match ($type) { + Schema::TYPE_BIT => new BitColumnSchema($type, $phpType), + Schema::TYPE_STRUCTURED => new StructuredColumnSchema($type, $phpType), + SchemaInterface::TYPE_BIGINT => PHP_INT_SIZE !== 8 + ? new BigIntColumnSchema($type, $phpType) + : new IntegerColumnSchema($type, $phpType), + default => match ($phpType) { + SchemaInterface::PHP_TYPE_INTEGER => new IntegerColumnSchema($type, $phpType), + SchemaInterface::PHP_TYPE_DOUBLE => new DoubleColumnSchema($type, $phpType), + SchemaInterface::PHP_TYPE_BOOLEAN => new BooleanColumnSchema($type, $phpType), + SchemaInterface::PHP_TYPE_RESOURCE => new BinaryColumnSchema($type, $phpType), + SchemaInterface::PHP_TYPE_ARRAY => new JsonColumnSchema($type, $phpType), + default => new StringColumnSchema($type, $phpType), + }, + }; + + $this->column->dbType($this->getDbType()); + $this->column->enumValues($this->getEnumValues()); + $this->column->precision($this->getPrecision()); + $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): void + { + $this->dimension = $dimension; + } + + /** + * @return int the dimension of the array. + */ + public function getDimension(): int + { + return $this->dimension; + } + + 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->dimension); + } + + public function phpTypecast(mixed $value): array|null + { + if (is_string($value)) { + $value = (new ArrayParser())->parse($value); + } + + if (!is_array($value)) { + return null; + } + + if ($this->getType() === SchemaInterface::TYPE_STRING) { + return $value; + } + + $column = $this->getColumn(); + + if ($this->dimension === 1 && $column->getType() !== SchemaInterface::TYPE_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. + */ + private 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/Column/BigIntColumnSchema.php b/src/Column/BigIntColumnSchema.php new file mode 100644 index 000000000..a07e32d22 --- /dev/null +++ b/src/Column/BigIntColumnSchema.php @@ -0,0 +1,12 @@ + str_pad(decbin((int) $value), (int) $this->getSize(), '0', STR_PAD_LEFT), + 'NULL' => null, + 'boolean' => $value ? '1' : '0', + 'string' => $value === '' ? null : $value, + default => $value instanceof ExpressionInterface ? $value : (string) $value, + }; + } + + public function phpTypecast(mixed $value): int|null + { + /** @var int|string|null $value */ + if (is_string($value)) { + /** @var int */ + return bindec($value); + } + + return $value; + } +} diff --git a/src/Column/BooleanColumnSchema.php b/src/Column/BooleanColumnSchema.php new file mode 100644 index 000000000..cb19cfd34 --- /dev/null +++ b/src/Column/BooleanColumnSchema.php @@ -0,0 +1,19 @@ +sequenceName; + } + + public function sequenceName(string|null $sequenceName = null): void + { + $this->sequenceName = $sequenceName; + } +} diff --git a/src/Column/StructuredColumnSchema.php b/src/Column/StructuredColumnSchema.php new file mode 100644 index 000000000..fac6ded96 --- /dev/null +++ b/src/Column/StructuredColumnSchema.php @@ -0,0 +1,88 @@ + + */ + private array $columns = []; + + public function __construct( + string $type = Schema::TYPE_STRUCTURED, + string|null $phpType = SchemaInterface::PHP_TYPE_ARRAY, + ) { + parent::__construct($type, $phpType); + } + + public function columns(array $columns): static + { + $this->columns = $columns; + return $this; + } + + public function getColumns(): array + { + return $this->columns; + } + + public function dbTypecast(mixed $value): mixed + { + if ($value === null || $value instanceof ExpressionInterface) { + return $value; + } + + return new StructuredExpression($value, $this->getDbType(), $this->columns); + } + + public function phpTypecast(mixed $value): array|null + { + if (is_string($value)) { + $value = (new StructuredParser())->parse($value); + } + + if (!is_iterable($value)) { + return null; + } + + if (empty($this->columns)) { + return $value instanceof Traversable + ? iterator_to_array($value) + : $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/Column/StructuredColumnSchemaInterface.php b/src/Column/StructuredColumnSchemaInterface.php new file mode 100644 index 000000000..35db4ce2a --- /dev/null +++ b/src/Column/StructuredColumnSchemaInterface.php @@ -0,0 +1,25 @@ + $columns + */ + public function columns(array $columns): static; + + /** + * Get the metadata of the composite type columns. + * + * @return ColumnSchemaInterface[] + */ + public function getColumns(): array; +} diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php deleted file mode 100644 index c035f1504..000000000 --- a/src/ColumnSchema.php +++ /dev/null @@ -1,296 +0,0 @@ -name('id'); - * $column->allowNull(false); - * $column->dbType('integer'); - * $column->phpType('integer'); - * $column->type('integer'); - * $column->defaultValue(0); - * $column->autoIncrement(true); - * $column->primaryKey(true); - * ``` - */ -final class ColumnSchema extends AbstractColumnSchema -{ - /** - * @var int The dimension of array. Defaults to 0, means this column isn't an array. - */ - private int $dimension = 0; - - /** - * @var string|null Name of an associated sequence if column is auto incremental. - */ - private string|null $sequenceName = null; - - /** - * @var ColumnSchemaInterface[] Columns metadata of the structured type. - * @psalm-var array - */ - private array $columns = []; - - /** - * Converts the input value according to {@see type} and {@see dbType} for use in a db query. - * - * If the value is null or an {@see Expression}, it won't be converted. - * - * @param mixed $value input value - * - * @return mixed Converted value. - */ - public function dbTypecast(mixed $value): mixed - { - if ($this->dimension > 0) { - if ($value === null || $value instanceof ExpressionInterface) { - return $value; - } - - if ($this->getType() === Schema::TYPE_STRUCTURED) { - $value = $this->dbTypecastArray($value, $this->dimension); - } - - return new ArrayExpression($value, $this->getDbType(), $this->dimension); - } - - return $this->dbTypecastValue($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. - */ - private function dbTypecastArray(mixed $value, int $dimension): array|null - { - if ($value === null) { - return null; - } - - if (!is_iterable($value)) { - return []; - } - - $items = []; - - if ($dimension > 1) { - foreach ($value as $val) { - $items[] = $this->dbTypecastArray($val, $dimension - 1); - } - } else { - foreach ($value as $val) { - $items[] = $this->dbTypecastValue($val); - } - } - - return $items; - } - - /** - * Converts the input value for use in a db query. - */ - private function dbTypecastValue(mixed $value): mixed - { - if ($value === null || $value instanceof ExpressionInterface) { - return $value; - } - - return match ($this->getType()) { - SchemaInterface::TYPE_JSON => new JsonExpression($value, $this->getDbType()), - - SchemaInterface::TYPE_BINARY => is_string($value) - ? new Param($value, PDO::PARAM_LOB) // explicitly setup PDO param type for binary column - : $this->typecast($value), - - Schema::TYPE_BIT => is_int($value) - ? str_pad(decbin($value), (int) $this->getSize(), '0', STR_PAD_LEFT) - : (string) $value, - - Schema::TYPE_STRUCTURED => new StructuredExpression($value, $this->getDbType(), $this->columns), - - default => $this->typecast($value), - }; - } - - /** - * Converts the input value according to {@see phpType} after retrieval from the database. - * - * If the value is null or an {@see Expression}, it won't be converted. - * - * @param mixed $value The input value - * - * @throws JsonException - * - * @return mixed The converted value - */ - public function phpTypecast(mixed $value): mixed - { - if ($this->dimension > 0) { - if (is_string($value)) { - $value = (new ArrayParser())->parse($value); - } - - if (!is_array($value)) { - return null; - } - - array_walk_recursive($value, function (mixed &$val) { - $val = $this->phpTypecastValue($val); - }); - - return $value; - } - - return $this->phpTypecastValue($value); - } - - /** - * Casts $value after retrieving from the DBMS to PHP representation. - * - * @throws JsonException - */ - private function phpTypecastValue(mixed $value): mixed - { - if ($value === null) { - return null; - } - - return match ($this->getType()) { - Schema::TYPE_BIT => is_string($value) ? bindec($value) : $value, - - SchemaInterface::TYPE_BOOLEAN => $value && $value !== 'f', - - SchemaInterface::TYPE_JSON - => json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR), - - Schema::TYPE_STRUCTURED => $this->phpTypecastStructured($value), - - default => parent::phpTypecast($value), - }; - } - - /** - * Converts the input value according to the structured type after retrieval from the database. - */ - private function phpTypecastStructured(mixed $value): array|null - { - if (is_string($value)) { - $value = (new StructuredParser())->parse($value); - } - - if (!is_iterable($value)) { - return null; - } - - $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])) { - $item = $this->columns[$columnName]->phpTypecast($item); - } - - $fields[$columnName] = $item; - } - - return $fields; - } - - /** - * @return int Get the dimension of the array. - * - * Defaults to 0, means this column isn't an array. - */ - public function getDimension(): int - { - return $this->dimension; - } - - /** - * @return string|null name of an associated sequence if column is auto incremental. - */ - public function getSequenceName(): string|null - { - return $this->sequenceName; - } - - /** - * Set dimension of an array. - * - * Defaults to 0, means this column isn't an array. - */ - public function dimension(int $dimension): void - { - $this->dimension = $dimension; - } - - /** - * Set the name of an associated sequence if a column is auto incremental. - */ - public function sequenceName(string|null $sequenceName): void - { - $this->sequenceName = $sequenceName; - } - - /** - * 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): void - { - $this->columns = $columns; - } - - /** - * Get the metadata of the structured type columns. - * - * @return ColumnSchemaInterface[] - */ - public function getColumns(): array - { - return $this->columns; - } -} diff --git a/src/Schema.php b/src/Schema.php index 913eabcb3..d6829405a 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -17,8 +17,19 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Helper\DbArrayHelper; +use Yiisoft\Db\Pgsql\Column\ArrayColumnSchema; +use Yiisoft\Db\Pgsql\Column\BigIntColumnSchema; +use Yiisoft\Db\Pgsql\Column\BinaryColumnSchema; +use Yiisoft\Db\Pgsql\Column\BitColumnSchema; +use Yiisoft\Db\Pgsql\Column\BooleanColumnSchema; +use Yiisoft\Db\Pgsql\Column\IntegerColumnSchema; +use Yiisoft\Db\Pgsql\Column\SequenceColumnSchemaInterface; +use Yiisoft\Db\Pgsql\Column\StructuredColumnSchema; +use Yiisoft\Db\Pgsql\Column\StructuredColumnSchemaInterface; use Yiisoft\Db\Schema\Builder\ColumnInterface; -use Yiisoft\Db\Schema\ColumnSchemaInterface; +use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; +use Yiisoft\Db\Schema\Column\StringColumnSchema; +use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Schema\TableSchemaInterface; use function array_change_key_case; @@ -27,7 +38,6 @@ use function array_unique; use function array_values; use function explode; -use function hex2bin; use function is_string; use function preg_match; use function preg_replace; @@ -80,6 +90,10 @@ * foreign_table_schema: string, * foreign_column_name: string, * } + * @psalm-type CreateInfo = array{ + * dimension?: int|string, + * columns?: array + * } */ final class Schema extends AbstractPdoSchema { @@ -87,6 +101,10 @@ final class Schema extends AbstractPdoSchema * Define the abstract column type as `bit`. */ public const TYPE_BIT = 'bit'; + /** + * Define the abstract column type as `array`. + */ + public const TYPE_ARRAY = 'array'; /** * Define the abstract column type as `structured`. */ @@ -763,15 +781,14 @@ protected function findColumns(TableSchemaInterface $table): bool /** @psalm-var ColumnArray $info */ $info = array_change_key_case($info); - /** @psalm-var ColumnSchema $column */ $column = $this->loadColumnSchema($info); - $table->column($column->getName(), $column); + $table->column($info['column_name'], $column); if ($column->isPrimaryKey()) { - $table->primaryKey($column->getName()); + $table->primaryKey($info['column_name']); - if ($table->getSequenceName() === null) { + if ($column instanceof SequenceColumnSchemaInterface && $table->getSequenceName() === null) { $table->sequenceName($column->getSequenceName()); } } @@ -787,28 +804,40 @@ protected function findColumns(TableSchemaInterface $table): bool * * @return ColumnSchemaInterface The column schema object. */ - protected function loadColumnSchema(array $info): ColumnSchemaInterface + private function loadColumnSchema(array $info): ColumnSchemaInterface { - $column = $this->createColumnSchema($info['column_name']); - $column->allowNull($info['is_nullable']); - $column->autoIncrement($info['is_autoinc']); - $column->comment($info['column_comment']); + $dbType = $info['data_type']; if (!in_array($info['type_scheme'], [$this->defaultSchema, 'pg_catalog'], true)) { - $column->dbType($info['type_scheme'] . '.' . $info['data_type']); + $dbType = $info['type_scheme'] . '.' . $dbType; + } + + $columns = []; + + if ($info['type_type'] === 'c') { + $type = self::TYPE_STRUCTURED; + $structured = $this->resolveTableName($dbType); + + if ($this->findColumns($structured)) { + $columns = $structured->getColumns(); + } } else { - $column->dbType($info['data_type']); + $type = self::TYPE_MAP[$dbType] ?? self::TYPE_STRING; } + $column = $this->createColumnSchema($type, dimension: $info['dimension'], columns: $columns); + $column->name($info['column_name']); + $column->dbType($dbType); + $column->allowNull($info['is_nullable']); + $column->autoIncrement($info['is_autoinc']); + $column->comment($info['column_comment']); $column->enumValues($info['enum_values'] !== null ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null); - $column->unsigned(false); // has no meaning in PG $column->primaryKey((bool) $info['is_pkey']); $column->precision($info['numeric_precision']); $column->scale($info['numeric_scale']); $column->size($info['size'] === null ? null : (int) $info['size']); - $column->dimension($info['dimension']); /** * pg_get_serial_sequence() doesn't track DEFAULT value change. @@ -816,32 +845,31 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface */ $defaultValue = $info['column_default']; - if ( - $defaultValue !== null - && preg_match("/^nextval\('([^']+)/", $defaultValue, $matches) === 1 - ) { - $column->sequenceName($matches[1]); - } elseif ($info['sequence_name'] !== null) { - $column->sequenceName($this->resolveTableName($info['sequence_name'])->getFullName()); - } - - if ($info['type_type'] === 'c') { - $column->type(self::TYPE_STRUCTURED); - $structured = $this->resolveTableName((string) $column->getDbType()); - - if ($this->findColumns($structured)) { - $column->columns($structured->getColumns()); + if ($column instanceof SequenceColumnSchemaInterface) { + if ( + $defaultValue !== null + && preg_match("/^nextval\('([^']+)/", $defaultValue, $matches) === 1 + ) { + $column->sequenceName($matches[1]); + } elseif ($info['sequence_name'] !== null) { + $column->sequenceName($this->resolveTableName($info['sequence_name'])->getFullName()); } - } else { - $column->type(self::TYPE_MAP[(string) $column->getDbType()] ?? self::TYPE_STRING); + } elseif ($column instanceof ArrayColumnSchema) { + /** @var ColumnSchemaInterface $arrayColumn */ + $arrayColumn = $column->getColumn(); + $arrayColumn->dbType($dbType); + $arrayColumn->enumValues($column->getEnumValues()); + $arrayColumn->precision($info['numeric_precision']); + $arrayColumn->scale($info['numeric_scale']); + $arrayColumn->size($info['size'] === null ? null : (int) $info['size']); } - $column->phpType($this->getColumnPhpType($column)); $column->defaultValue($this->normalizeDefaultValue($defaultValue, $column)); - if ($column->getType() === self::TYPE_STRUCTURED && $column->getDimension() === 0) { + if ($column instanceof StructuredColumnSchemaInterface) { /** @psalm-var array|null $defaultValue */ $defaultValue = $column->getDefaultValue(); + if (is_array($defaultValue)) { foreach ($column->getColumns() as $structuredColumnName => $structuredColumn) { $structuredColumn->defaultValue($defaultValue[$structuredColumnName] ?? null); @@ -852,19 +880,25 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface return $column; } - /** - * Extracts the PHP type from an abstract DB type. - * - * @param ColumnSchemaInterface $column The column schema information. - * - * @return string The PHP type name. - */ - protected function getColumnPhpType(ColumnSchemaInterface $column): string + protected function getColumnPhpType(string $type, bool $isUnsigned = false): string { - return match ($column->getType()) { + return match ($type) { self::TYPE_BIT => self::PHP_TYPE_INTEGER, self::TYPE_STRUCTURED => self::PHP_TYPE_ARRAY, - default => parent::getColumnPhpType($column), + default => parent::getColumnPhpType($type, $isUnsigned), + }; + } + + protected function createColumnSchemaFromPhpType(string $phpType, string $type): ColumnSchemaInterface + { + return match ($phpType) { + self::PHP_TYPE_STRING => $type === SchemaInterface::TYPE_BIGINT + ? new BigIntColumnSchema($type, $phpType) + : new StringColumnSchema($type, $phpType), + self::PHP_TYPE_INTEGER => new IntegerColumnSchema($type, $phpType), + self::PHP_TYPE_BOOLEAN => new BooleanColumnSchema($type, $phpType), + self::PHP_TYPE_RESOURCE => new BinaryColumnSchema($type, $phpType), + default => parent::createColumnSchemaFromPhpType($phpType, $type), }; } @@ -1043,17 +1077,35 @@ private function loadTableConstraints(string $tableName, string $returnType): ar } /** - * Creates a column schema for the database. - * - * This method may be overridden by child classes to create a DBMS-specific column schema. - * - * @param string $name Name of the column. - * - * @return ColumnSchema + * @psalm-param CreateInfo $info + * @psalm-suppress ImplementedParamTypeMismatch */ - private function createColumnSchema(string $name): ColumnSchema + protected function createColumnSchema(string $type, mixed ...$info): ColumnSchemaInterface { - return new ColumnSchema($name); + /** @var CreateInfo $info */ + $dimension = isset($info['dimension']) ? (int) $info['dimension'] : 0; + + if ($dimension > 0) { + $column = new ArrayColumnSchema(); + $column->dimension($dimension); + + unset($info['dimension']); + + $column->column($this->createColumnSchema($type, ...$info)); + + return $column; + } + + $phpType = $this->getColumnPhpType($type); + + return match ($type) { + self::TYPE_BIT => new BitColumnSchema($type, $phpType), + self::TYPE_STRUCTURED => (new StructuredColumnSchema($type, $phpType))->columns($info['columns'] ?? []), + self::TYPE_BIGINT => PHP_INT_SIZE !== 8 + ? new BigIntColumnSchema($type, $phpType) + : new IntegerColumnSchema($type, $phpType), + default => parent::createColumnSchema($type), + }; } /** diff --git a/src/StructuredExpression.php b/src/StructuredExpression.php index f317c3870..74ea76b6e 100644 --- a/src/StructuredExpression.php +++ b/src/StructuredExpression.php @@ -6,7 +6,7 @@ use Traversable; use Yiisoft\Db\Expression\ExpressionInterface; -use Yiisoft\Db\Schema\ColumnSchemaInterface; +use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; use function array_key_exists; use function array_keys; diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 11ed992f7..fcbb9dca3 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -4,18 +4,25 @@ namespace Yiisoft\Db\Pgsql\Tests; -use JsonException; -use PHPUnit\Framework\TestCase; use Throwable; use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\JsonExpression; -use Yiisoft\Db\Pgsql\ColumnSchema; +use Yiisoft\Db\Pgsql\Column\ArrayColumnSchema; +use Yiisoft\Db\Pgsql\Column\BigIntColumnSchema; +use Yiisoft\Db\Pgsql\Column\BinaryColumnSchema; +use Yiisoft\Db\Pgsql\Column\BitColumnSchema; +use Yiisoft\Db\Pgsql\Column\BooleanColumnSchema; +use Yiisoft\Db\Pgsql\Column\IntegerColumnSchema; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; +use Yiisoft\Db\Schema\Column\DoubleColumnSchema; +use Yiisoft\Db\Schema\Column\JsonColumnSchema; +use Yiisoft\Db\Schema\Column\StringColumnSchema; use Yiisoft\Db\Schema\SchemaInterface; +use Yiisoft\Db\Tests\Common\CommonColumnSchemaTest; use function stream_get_contents; @@ -24,7 +31,7 @@ * * @psalm-suppress PropertyNotSetInConstructor */ -final class ColumnSchemaTest extends TestCase +final class ColumnSchemaTest extends CommonColumnSchemaTest { use TestTrait; @@ -101,19 +108,6 @@ public function testPhpTypeCast(): void $db->close(); } - /** - * @throws JsonException - */ - public function testPhpTypeCastBool(): void - { - $columnSchema = new ColumnSchema('boolean'); - - $columnSchema->type('boolean'); - - $this->assertFalse($columnSchema->phpTypeCast('f')); - $this->assertTrue($columnSchema->phpTypeCast('t')); - } - public function testDbTypeCastJson(): void { $db = $this->getConnection(true); @@ -189,15 +183,6 @@ public function testNegativeDefaultValues() $this->assertSame(-33.22, $tableSchema->getColumn('numeric_col')->getDefaultValue()); } - public function testDbTypeCastBit() - { - $db = $this->getConnection(true); - $schema = $db->getSchema(); - $tableSchema = $schema->getTableSchema('type'); - - $this->assertSame('01100100', $tableSchema->getColumn('bit_col')->dbTypecast('01100100')); - } - public function testPrimaryKeyOfView() { $db = $this->getConnection(true); @@ -289,4 +274,108 @@ public function testStructuredType(): void $db->close(); } + + public function testColumnSchemaInstance() + { + $db = $this->getConnection(true); + $schema = $db->getSchema(); + $tableSchema = $schema->getTableSchema('type'); + + $this->assertInstanceOf(IntegerColumnSchema::class, $tableSchema->getColumn('int_col')); + $this->assertInstanceOf(StringColumnSchema::class, $tableSchema->getColumn('char_col')); + $this->assertInstanceOf(DoubleColumnSchema::class, $tableSchema->getColumn('float_col')); + $this->assertInstanceOf(BinaryColumnSchema::class, $tableSchema->getColumn('blob_col')); + $this->assertInstanceOf(BooleanColumnSchema::class, $tableSchema->getColumn('bool_col')); + $this->assertInstanceOf(BitColumnSchema::class, $tableSchema->getColumn('bit_col')); + $this->assertInstanceOf(ArrayColumnSchema::class, $tableSchema->getColumn('intarray_col')); + $this->assertInstanceOf(IntegerColumnSchema::class, $tableSchema->getColumn('intarray_col')->getColumn()); + $this->assertInstanceOf(JsonColumnSchema::class, $tableSchema->getColumn('json_col')); + } + + /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnSchemaProvider::predefinedTypes */ + public function testPredefinedType(string $className, string $type, string $phpType) + { + parent::testPredefinedType($className, $type, $phpType); + } + + /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnSchemaProvider::dbTypecastColumns */ + public function testDbTypecastColumns(string $className, array $values) + { + parent::testDbTypecastColumns($className, $values); + } + + /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnSchemaProvider::phpTypecastColumns */ + public function testPhpTypecastColumns(string $className, array $values) + { + parent::testPhpTypecastColumns($className, $values); + } + + /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnSchemaProvider::dbTypecastArrayColumns */ + public function testDbTypecastArrayColumnSchema(string $dbType, string $type, string $phpType, array $values): void + { + $arrayCol = new ArrayColumnSchema($type, $phpType); + $arrayCol->dbType($dbType); + + foreach ($values as [$dimension, $expected, $value]) { + $arrayCol->dimension($dimension); + $dbValue = $arrayCol->dbTypecast($value); + + $this->assertInstanceOf(ArrayExpression::class, $dbValue); + $this->assertSame($dbType, $dbValue->getType()); + $this->assertSame($dimension, $dbValue->getDimension()); + + if (is_object($expected)) { + $this->assertEquals($expected, $dbValue->getValue()); + } + } + } + + /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\ColumnSchemaProvider::phpTypecastArrayColumns */ + public function testPhpTypecastArrayColumnSchema(string $dbType, string $type, string $phpType, array $values): void + { + $arrayCol = new ArrayColumnSchema($type, $phpType); + $arrayCol->dbType($dbType); + + foreach ($values as [$dimension, $expected, $value]) { + $arrayCol->dimension($dimension); + $this->assertSame($expected, $arrayCol->phpTypecast($value)); + } + } + + public function testIntegerColumnSchema() + { + $intCol = new IntegerColumnSchema(); + + $this->assertNull($intCol->getSequenceName()); + + $intCol->sequenceName('int_seq'); + + $this->assertSame('int_seq', $intCol->getSequenceName()); + } + + public function testBigIntColumnSchema() + { + $bigintCol = new BigIntColumnSchema(); + + $this->assertNull($bigintCol->getSequenceName()); + + $bigintCol->sequenceName('bigint_seq'); + + $this->assertSame('bigint_seq', $bigintCol->getSequenceName()); + } + + public function testArrayColumnSchema() + { + $arrayCol = new ArrayColumnSchema(); + + $this->assertSame(1, $arrayCol->getDimension()); + + $this->assertNull($arrayCol->dbTypecast(null)); + $this->assertEquals(new ArrayExpression([]), $arrayCol->dbTypecast('')); + $this->assertSame($expression = new Expression('expression'), $arrayCol->dbTypecast($expression)); + $this->assertNull($arrayCol->phpTypecast(null)); + + $arrayCol->dimension(2); + $this->assertSame(2, $arrayCol->getDimension()); + } } diff --git a/tests/Provider/ColumnSchemaProvider.php b/tests/Provider/ColumnSchemaProvider.php new file mode 100644 index 000000000..855acd53f --- /dev/null +++ b/tests/Provider/ColumnSchemaProvider.php @@ -0,0 +1,317 @@ + 'value'], 'jsonb'), + new JsonExpression(['key' => 'value']), + null, + ], [[1, 2, 3], ['key' => 'value'], new JsonExpression(['key' => 'value']), null]], + [2, [ + [ + new JsonExpression([1, 2, 3], 'jsonb'), + new JsonExpression(['key' => 'value'], 'jsonb'), + new JsonExpression(['key' => 'value']), + null, + ], + null, + ], [[[1, 2, 3], ['key' => 'value'], new JsonExpression(['key' => 'value']), null], null]], + ], + ], + [ + 'varbit', + Schema::TYPE_BIT, + SchemaInterface::PHP_TYPE_INTEGER, + [ + [1, ['1011', '1001', null], [0b1011, '1001', null]], + [2, [['1011', '1001', null]], [[0b1011, '1001', null]]], + ], + ], + [ + 'price_composite', + Schema::TYPE_STRUCTURED, + SchemaInterface::PHP_TYPE_ARRAY, + [ + [ + 1, + [ + new StructuredExpression(['value' => 10, 'currency' => 'USD'], 'price_composite'), + null, + ], + [ + ['value' => 10, 'currency' => 'USD'], + null, + ], + ], + [ + 2, + [[ + new StructuredExpression(['value' => 10, 'currency' => 'USD'], 'price_composite'), + null, + ]], + [[ + ['value' => 10, 'currency' => 'USD'], + null, + ]], + ], + ], + ], + ]; + } + + public static function phpTypecastArrayColumns() + { + $bigInt = PHP_INT_SIZE === 8 ? 9223372036854775807 : '9223372036854775807'; + + return [ + // [dbtype, type, phpType, values] + [ + 'int4', + SchemaInterface::TYPE_INTEGER, + SchemaInterface::PHP_TYPE_INTEGER, + [ + // [dimension, expected, typecast value] + [1, [1, 2, 3, null], '{1,2,3,}'], + [2, [[1, 2], [3], null], '{{1,2},{3},}'], + ], + ], + [ + 'int8', + SchemaInterface::TYPE_BIGINT, + SchemaInterface::PHP_TYPE_INTEGER, + [ + [1, [1, 2, $bigInt], '{1,2,9223372036854775807}'], + [2, [[1, 2], [$bigInt]], '{{1,2},{9223372036854775807}}'], + ], + ], + [ + 'float8', + SchemaInterface::TYPE_DOUBLE, + SchemaInterface::PHP_TYPE_DOUBLE, + [ + [1, [1.0, 2.2, null], '{1,2.2,}'], + [2, [[1.0], [2.2, null]], '{{1},{2.2,}}'], + ], + ], + [ + 'bool', + SchemaInterface::TYPE_BOOLEAN, + SchemaInterface::PHP_TYPE_BOOLEAN, + [ + [1, [true, false, null], '{t,f,}'], + [2, [[true, false, null]], '{{t,f,}}'], + ], + ], + [ + 'varchar', + SchemaInterface::TYPE_STRING, + SchemaInterface::PHP_TYPE_STRING, + [ + [1, ['1', '2', '', null], '{1,2,"",}'], + [2, [['1', '2'], [''], [null]], '{{1,2},{""},{NULL}}'], + ], + ], + [ + 'bytea', + SchemaInterface::TYPE_BINARY, + SchemaInterface::PHP_TYPE_RESOURCE, + [ + [1, ["\x10\x11", '', null], '{\x1011,"",}'], + [2, [["\x10\x11"], ['', null]], '{{\x1011},{"",}}'], + ], + ], + [ + 'jsonb', + SchemaInterface::TYPE_JSON, + SchemaInterface::PHP_TYPE_ARRAY, + [ + [1, [[1, 2, 3], null], '{"[1,2,3]",}'], + [1, [[1, 2, 3]], '{{1,2,3}}'], + [2, [[[1, 2, 3, null], null]], '{{"[1,2,3,null]",}}'], + ], + ], + [ + 'varbit', + Schema::TYPE_BIT, + SchemaInterface::PHP_TYPE_INTEGER, + [ + [1, [0b1011, 0b1001, null], '{1011,1001,}'], + [2, [[0b1011, 0b1001, null]], '{{1011,1001,}}'], + ], + ], + [ + 'price_structured', + Schema::TYPE_STRUCTURED, + SchemaInterface::PHP_TYPE_ARRAY, + [ + [1, [['10', 'USD'], null], '{"(10,USD)",}'], + [2, [[['10', 'USD'], null]], '{{"(10,USD)",}}'], + ], + ], + ]; + } +} diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index bce885318..db7b9fc49 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -261,9 +261,9 @@ public static function columns(): array 'defaultValue' => null, ], 'intarray_col' => [ - 'type' => 'integer', + 'type' => 'array', 'dbType' => 'int4', - 'phpType' => 'integer', + 'phpType' => 'array', 'primaryKey' => false, 'allowNull' => true, 'autoIncrement' => false, @@ -273,11 +273,20 @@ public static function columns(): array 'scale' => 0, 'defaultValue' => null, 'dimension' => 1, + 'column' => [ + 'type' => 'integer', + 'dbType' => 'int4', + 'phpType' => 'integer', + 'enumValues' => null, + 'size' => null, + 'precision' => 32, + 'scale' => 0, + ], ], 'numericarray_col' => [ - 'type' => 'decimal', + 'type' => 'array', 'dbType' => 'numeric', - 'phpType' => 'double', + 'phpType' => 'array', 'primaryKey' => false, 'allowNull' => true, 'autoIncrement' => false, @@ -287,11 +296,20 @@ public static function columns(): array 'scale' => 2, 'defaultValue' => null, 'dimension' => 1, + 'column' => [ + 'type' => 'decimal', + 'dbType' => 'numeric', + 'phpType' => 'double', + 'enumValues' => null, + 'size' => null, + 'precision' => 5, + 'scale' => 2, + ], ], 'varchararray_col' => [ - 'type' => 'string', + 'type' => 'array', 'dbType' => 'varchar', - 'phpType' => 'string', + 'phpType' => 'array', 'primaryKey' => false, 'allowNull' => true, 'autoIncrement' => false, @@ -301,11 +319,20 @@ public static function columns(): array 'scale' => null, 'defaultValue' => null, 'dimension' => 1, + 'column' => [ + 'type' => 'string', + 'dbType' => 'varchar', + 'phpType' => 'string', + 'enumValues' => null, + 'size' => 100, + 'precision' => null, + 'scale' => null, + ], ], 'textarray2_col' => [ - 'type' => 'text', + 'type' => 'array', 'dbType' => 'text', - 'phpType' => 'string', + 'phpType' => 'array', 'primaryKey' => false, 'allowNull' => true, 'autoIncrement' => false, @@ -315,6 +342,15 @@ public static function columns(): array 'scale' => null, 'defaultValue' => null, 'dimension' => 2, + 'column' => [ + 'type' => 'text', + 'dbType' => 'text', + 'phpType' => 'string', + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + ], ], 'json_col' => [ 'type' => 'json', @@ -328,7 +364,6 @@ public static function columns(): array 'precision' => null, 'scale' => null, 'defaultValue' => ['a' => 1], - 'dimension' => 0, ], 'jsonb_col' => [ 'type' => 'json', @@ -342,10 +377,9 @@ public static function columns(): array 'precision' => null, 'scale' => null, 'defaultValue' => null, - 'dimension' => 0, ], 'jsonarray_col' => [ - 'type' => 'json', + 'type' => 'array', 'dbType' => 'json', 'phpType' => 'array', 'primaryKey' => false, @@ -357,6 +391,15 @@ public static function columns(): array 'scale' => null, 'defaultValue' => null, 'dimension' => 1, + 'column' => [ + 'type' => 'json', + 'dbType' => 'json', + 'phpType' => 'array', + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + ], ], ], 'tableName' => 'type', diff --git a/tests/Provider/StructuredTypeProvider.php b/tests/Provider/StructuredTypeProvider.php index c7c4540ff..3238335e1 100644 --- a/tests/Provider/StructuredTypeProvider.php +++ b/tests/Provider/StructuredTypeProvider.php @@ -111,7 +111,7 @@ public static function columns(): array ], ], 'price_array' => [ - 'type' => 'structured', + 'type' => 'array', 'dbType' => 'currency_money_structured', 'phpType' => 'array', 'primaryKey' => false, @@ -157,7 +157,7 @@ public static function columns(): array ], ], 'price_array2' => [ - 'type' => 'structured', + 'type' => 'array', 'dbType' => 'currency_money_structured', 'phpType' => 'array', 'primaryKey' => false, @@ -213,7 +213,6 @@ public static function columns(): array 'price_from' => ['value' => 0.0, 'currency_code' => 'USD'], 'price_to' => ['value' => 100.0, 'currency_code' => 'USD'], ], - 'dimension' => 0, 'columns' => [ 'price_from' => [ 'type' => 'structured', diff --git a/tests/Support/ColumnSchemaBuilder.php b/tests/Support/ColumnSchemaBuilder.php index dbbb8de21..119878d1d 100644 --- a/tests/Support/ColumnSchemaBuilder.php +++ b/tests/Support/ColumnSchemaBuilder.php @@ -4,16 +4,18 @@ namespace Yiisoft\Db\Pgsql\Tests\Support; -use Yiisoft\Db\Pgsql\ColumnSchema; +use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; +use Yiisoft\Db\Schema\Column\DoubleColumnSchema; +use Yiisoft\Db\Schema\Column\StringColumnSchema; +use Yiisoft\Db\Schema\SchemaInterface; class ColumnSchemaBuilder { - public static function numeric(string $name, int|null $precision, int|null $scale, mixed $defaultValue = null): ColumnSchema + public static function numeric(string $name, int|null $precision, int|null $scale, mixed $defaultValue = null): ColumnSchemaInterface { - $column = new ColumnSchema($name); - $column->type('decimal'); + $column = new DoubleColumnSchema(SchemaInterface::TYPE_DECIMAL); + $column->name($name); $column->dbType('numeric'); - $column->phpType('double'); $column->precision($precision); $column->scale($scale); $column->defaultValue($defaultValue); @@ -21,12 +23,11 @@ public static function numeric(string $name, int|null $precision, int|null $scal return $column; } - public static function char(string $name, int|null $size, mixed $defaultValue = null): ColumnSchema + public static function char(string $name, int|null $size, mixed $defaultValue = null): ColumnSchemaInterface { - $column = new ColumnSchema($name); - $column->type('char'); + $column = new StringColumnSchema(SchemaInterface::TYPE_CHAR); + $column->name($name); $column->dbType('bpchar'); - $column->phpType('string'); $column->size($size); $column->defaultValue($defaultValue);