From 9a25d99c3554ebba39e6474e12d3ccfc1c315120 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 26 Jul 2023 12:51:43 +0700 Subject: [PATCH 01/34] Support Composite types --- src/Builder/CompositeExpressionBuilder.php | 134 ++++++++++++++ src/ColumnSchema.php | 112 +++++++++++- src/CompositeExpression.php | 200 +++++++++++++++++++++ src/CompositeParser.php | 76 ++++++++ src/DQLQueryBuilder.php | 2 + src/Schema.php | 34 +++- tests/ColumnSchemaTest.php | 40 +++++ tests/CompositeParserTest.php | 32 ++++ tests/Provider/SchemaProvider.php | 79 ++++++++ tests/Support/Fixture/pgsql.sql | 23 +++ 10 files changed, 723 insertions(+), 9 deletions(-) create mode 100644 src/Builder/CompositeExpressionBuilder.php create mode 100644 src/CompositeExpression.php create mode 100644 src/CompositeParser.php create mode 100644 tests/CompositeParserTest.php diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php new file mode 100644 index 00000000..39a4de3c --- /dev/null +++ b/src/Builder/CompositeExpressionBuilder.php @@ -0,0 +1,134 @@ +getValue(); + + if (empty($value)) { + return 'NULL'; + } + + if ($value instanceof QueryInterface) { + [$sql, $params] = $this->queryBuilder->build($value, $params); + return "($sql)" . $this->getTypeHint($expression); + } + + /** @psalm-var string[] $placeholders */ + $placeholders = $this->buildPlaceholders($expression, $params); + + if (empty($placeholders)) { + return 'NULL'; + } + + return 'ROW(' . implode(', ', $placeholders) . ')' . $this->getTypeHint($expression); + } + + /** + * Builds a placeholder array out of $expression values. + * + * @param array $params The binding parameters. + * + * @throws Exception + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws NotSupportedException + * + * @psalm-param CompositeExpression $expression + */ + private function buildPlaceholders(ExpressionInterface $expression, array &$params): array + { + $placeholders = []; + + /** @psalm-var mixed $value */ + $value = $expression->getNormalizedValue(); + + if (!is_iterable($value)) { + return $placeholders; + } + + $columns = (array) $expression->getColumns(); + $columnNames = array_keys($columns); + + /** + * @psalm-var int|string $columnName + * @psalm-var mixed $item + */ + foreach ($value as $columnName => $item) { + if (is_int($columnName)) { + $columnName = $columnNames[$columnName] ?? null; + } + + if ($columnName === null || !isset($columns[$columnName])) { + continue; + } + + /** @psalm-var mixed $item */ + $item = $columns[$columnName]->dbTypecast($item); + + if ($item instanceof ExpressionInterface) { + $placeholders[] = $this->queryBuilder->buildExpression($item, $params); + } else { + $placeholders[] = $this->queryBuilder->bindParam($item, $params); + } + } + + return $placeholders; + } + + /** + * @return string The typecast expression based on {@see type}. + * + * @psalm-param CompositeExpression $expression + */ + private function getTypeHint(ExpressionInterface $expression): string + { + $type = $expression->getType(); + + if ($type === null) { + return ''; + } + + return '::' . $type; + } +} diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index a40a6ab7..8458abcd 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -11,6 +11,7 @@ use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Schema\AbstractColumnSchema; +use Yiisoft\Db\Schema\ColumnSchemaInterface; use Yiisoft\Db\Schema\SchemaInterface; use function array_walk_recursive; @@ -58,6 +59,12 @@ final class ColumnSchema extends AbstractColumnSchema */ private string|null $sequenceName = null; + /** + * @var ColumnSchemaInterface[]|null Columns metadata of the composite type. + * @psalm-var array|null + */ + private array|null $columns = null; + /** * Converts the input value according to {@see type} and {@see dbType} for use in a db query. * @@ -70,14 +77,50 @@ final class ColumnSchema extends AbstractColumnSchema */ public function dbTypecast(mixed $value): mixed { - if ($value === null || $value instanceof ExpressionInterface) { - return $value; - } - if ($this->dimension > 0) { + if ($value === null || $value instanceof ExpressionInterface) { + return $value; + } + + if ($this->getType() === Schema::TYPE_COMPOSITE) { + $value = $this->dbTypecastArray($value, $this->dimension); + } + return new ArrayExpression($value, $this->getDbType(), $this->dimension); } + return $this->dbTypecastValue($value); + } + + /** + * @param int $dimension Should be more than 0 + */ + private function dbTypecastArray(mixed $value, int $dimension): array|null + { + $items = []; + + if (!is_iterable($value)) { + return $items; + } + + /** @psalm-var mixed $val */ + foreach ($value as $val) { + if ($dimension > 1) { + $items[] = $this->dbTypecastArray($val, $dimension - 1); + } else { + $items[] = $this->dbTypecastValue($val); + } + } + + return $items; + } + + 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()), @@ -89,6 +132,8 @@ public function dbTypecast(mixed $value): mixed ? str_pad(decbin($value), (int) $this->getSize(), '0', STR_PAD_LEFT) : $this->typecast($value), + Schema::TYPE_COMPOSITE => new CompositeExpression($value, $this->getDbType(), $this->columns), + default => $this->typecast($value), }; } @@ -145,10 +190,44 @@ protected function phpTypecastValue(mixed $value): mixed SchemaInterface::TYPE_JSON => json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR), + Schema::TYPE_COMPOSITE => $this->phpTypecastComposite($value), + default => parent::phpTypecast($value), }; } + private function phpTypecastComposite(mixed $value): array|null + { + if ($this->columns === null) { + return null; + } + + if (is_string($value)) { + $value = $this->getCompositeParser()->parse($value); + } + + if (!is_iterable($value)) { + return null; + } + + $fields = []; + $columnNames = array_keys($this->columns); + + /** + * @psalm-var int|string $key + * @psalm-var mixed $val + */ + foreach ($value as $key => $val) { + $columnName = $columnNames[$key] ?? $key; + + if (isset($this->columns[$columnName])) { + $fields[$columnName] = $this->columns[$columnName]->phpTypecast($val); + } + } + + return $fields; + } + /** * Creates instance of ArrayParser. */ @@ -192,4 +271,29 @@ public function sequenceName(string|null $sequenceName): void { $this->sequenceName = $sequenceName; } + + /** + * @param ColumnSchemaInterface[]|null $columns The columns metadata of the composite type. + * @psalm-param array|null $columns + */ + public function columns(array|null $columns): void + { + $this->columns = $columns; + } + + /** + * @return ColumnSchemaInterface[]|null Columns metadata of the composite type. + */ + public function getColumns(): array|null + { + return $this->columns; + } + + /** + * Creates instance of CompositeParser. + */ + private function getCompositeParser(): CompositeParser + { + return new CompositeParser(); + } } diff --git a/src/CompositeExpression.php b/src/CompositeExpression.php new file mode 100644 index 00000000..e4e93216 --- /dev/null +++ b/src/CompositeExpression.php @@ -0,0 +1,200 @@ + 10, 'currency_code' => 'USD]); + * ``` + * + * Will be encoded to `ROW[10, USD]` + * + * @template-implements ArrayAccess + * @template-implements IteratorAggregate + */ +class CompositeExpression implements ExpressionInterface, ArrayAccess, Countable, IteratorAggregate +{ + /** + * @param ColumnSchemaInterface[]|null $columns + * @psalm-param array|null $columns + */ + public function __construct( + private mixed $value = [], + private string|null $type = null, + private array|null $columns = null, + ) {} + + /** + * The composite type name. + * + * Defaults to `null` which means the type isn't explicitly specified. + * + * Note that in the case where a type isn't specified explicitly and DBMS can't guess it from the context, SQL error + * will be raised. + */ + public function getType(): string|null + { + return $this->type; + } + + /** + * @return ColumnSchemaInterface[]|null + */ + public function getColumns(): array|null + { + return $this->columns; + } + + /** + * The composite type's content. It can be represented as an associative array of the composite type's column names + * and values. + */ + public function getValue(): mixed + { + return $this->value; + } + + /** + * Sort values according to `$columns` order and fill skipped items with default values + */ + public function getNormalizedValue(): mixed + { + if ($this->columns === null || !is_array($this->value) || !is_string(array_key_first($this->value))) { + return $this->value; + } + + $value = []; + + foreach ($this->columns as $name => $column) { + if (array_key_exists($name, $this->value)) { + $value[$name] = $this->value[$name]; + } else { + $value[$name] = $column->getDefaultValue(); + } + } + + return $value; + } + + /** + * Whether an offset exists. + * + * @link https://php.net/manual/en/arrayaccess.offsetexists.php + * + * @param int|string $offset An offset to check for. + * + * @return bool Its `true` on success or `false` on failure. + * + * @throws InvalidConfigException If value is not an array. + */ + public function offsetExists(mixed $offset): bool + { + $this->value = $this->validateValue($this->value); + return array_key_exists($offset, $this->value); + } + + /** + * Offset to retrieve. + * + * @link https://php.net/manual/en/arrayaccess.offsetget.php + * + * @param int|string $offset The offset to retrieve. + * + * @return mixed Can return all value types. + * + * @throws InvalidConfigException If value is not an array. + */ + public function offsetGet(mixed $offset): mixed + { + $this->value = $this->validateValue($this->value); + return $this->value[$offset]; + } + + /** + * Offset to set. + * + * @link https://php.net/manual/en/arrayaccess.offsetset.php + * + * @param int|string $offset The offset to assign the value to. + * @param mixed $value The value to set. + * + * @throws InvalidConfigException If content value is not an array. + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->value = $this->validateValue($this->value); + $this->value[$offset] = $value; + } + + /** + * Offset to unset. + * + * @param int|string $offset The offset to unset. + * + * @throws InvalidConfigException If value is not an array. + * + * @link https://php.net/manual/en/arrayaccess.offsetunset.php + */ + public function offsetUnset(mixed $offset): void + { + $this->value = $this->validateValue($this->value); + unset($this->value[$offset]); + } + + /** + * Count elements of the composite type's content. + * + * @link https://php.net/manual/en/countable.count.php + * + * @return int The custom count as an integer. + */ + public function count(): int + { + return count((array) $this->value); + } + + /** + * Retrieve an external iterator. + * + * @link https://php.net/manual/en/iteratoraggregate.getiterator.php + * + * @throws InvalidConfigException If value is not an array. + * + * @return ArrayIterator An instance of an object implementing `Iterator` or `Traversable`. + */ + public function getIterator(): ArrayIterator + { + $this->value = $this->validateValue($this->value); + return new ArrayIterator($this->value); + } + + /** + * Validates the value of the composite expression is an array. + * + * @throws InvalidConfigException If value is not an array. + */ + private function validateValue(mixed $value): array + { + if (!is_array($value)) { + throw new InvalidConfigException('The CompositeExpression value must be an array.'); + } + + return $value; + } +} diff --git a/src/CompositeParser.php b/src/CompositeParser.php new file mode 100644 index 00000000..2ec06929 --- /dev/null +++ b/src/CompositeParser.php @@ -0,0 +1,76 @@ +parseComposite($value); + } + + /** + * Parse PostgreSQL composite type encoded in string. + * + * @param string $value String to parse. + */ + private function parseComposite(string $value): array + { + for ($result = [], $i = 1;; ++$i) { + $result[] = match ($value[$i]) { + ',', ')' => null, + '"' => $this->parseQuotedString($value, $i), + default => $this->parseUnquotedString($value, $i), + }; + + if ($value[$i] === ')') { + return $result; + } + } + } + + /** + * Parses quoted string. + */ + private function parseQuotedString(string $value, int &$i): string + { + for ($result = '', ++$i;; ++$i) { + if ($value[$i] === '\\') { + ++$i; + } elseif ($value[$i] === '"') { + ++$i; + return $result; + } + + $result .= $value[$i]; + } + } + + /** + * Parses unquoted string. + */ + private function parseUnquotedString(string $value, int &$i): string + { + for ($result = '';; ++$i) { + if (in_array($value[$i], [',', ')'], true)) { + return $result; + } + + $result .= $value[$i]; + } + } +} diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index 604bbf37..cd0553d7 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -8,6 +8,7 @@ use Yiisoft\Db\Expression\ExpressionBuilderInterface; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Pgsql\Builder\ArrayExpressionBuilder; +use Yiisoft\Db\Pgsql\Builder\CompositeExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\JsonExpressionBuilder; use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder; use Yiisoft\Db\QueryBuilder\Condition\LikeCondition; @@ -50,6 +51,7 @@ protected function defaultExpressionBuilders(): array return array_merge(parent::defaultExpressionBuilders(), [ ArrayExpression::class => ArrayExpressionBuilder::class, JsonExpression::class => JsonExpressionBuilder::class, + CompositeExpression::class => CompositeExpressionBuilder::class, ]); } } diff --git a/src/Schema.php b/src/Schema.php index f40a3fa3..ba4d2ca8 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -85,6 +85,10 @@ final class Schema extends AbstractPdoSchema * Define the abstract column type as `bit`. */ public const TYPE_BIT = 'bit'; + /** + * Define the abstract column type as `composite`. + */ + public const TYPE_COMPOSITE = 'composite'; /** * @var array The mapping from physical column types (keys) to abstract column types (values). @@ -812,9 +816,29 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface } $column->type($this->typeMap[(string) $column->getDbType()] ?? self::TYPE_STRING); + + if ($info['type_type'] === 'c') { + $column->type(self::TYPE_COMPOSITE); + $composite = $this->resolveTableName((string) $column->getDbType()); + + if ($this->findColumns($composite)) { + $column->columns($composite->getColumns()); + } + } + $column->phpType($this->getColumnPhpType($column)); $column->defaultValue($this->normalizeDefaultValue($defaultValue, $column)); + if ($column->getType() === self::TYPE_COMPOSITE && $column->getDimension() === 0) { + /** @psalm-var array|null $defaultValue */ + $defaultValue = $column->getDefaultValue(); + if (is_array($defaultValue)) { + foreach ((array) $column->getColumns() as $compositeName => $compositeColumn) { + $compositeColumn->defaultValue($defaultValue[$compositeName] ?? null); + } + } + } + return $column; } @@ -827,11 +851,11 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface */ protected function getColumnPhpType(ColumnSchemaInterface $column): string { - if ($column->getType() === self::TYPE_BIT) { - return self::PHP_TYPE_INTEGER; - } - - return parent::getColumnPhpType($column); + return match ($column->getType()) { + self::TYPE_BIT => self::PHP_TYPE_INTEGER, + self::TYPE_COMPOSITE => self::PHP_TYPE_ARRAY, + default => parent::getColumnPhpType($column), + }; } /** diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 01b8ed03..9fa5c2d8 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -188,4 +188,44 @@ public function testNegativeDefaultValues() $this->assertSame(-12345.6789, $tableSchema->getColumn('float_col')->getDefaultValue()); $this->assertSame(-33.22, $tableSchema->getColumn('numeric_col')->getDefaultValue()); } + + public function testCompositeType(): void + { + $db = $this->getConnection(true); + $command = $db->createCommand(); + $schema = $db->getSchema(); + $tableSchema = $schema->getTableSchema('test_composite_type'); + + $command->insert('test_composite_type', [ + 'price_col' => ['value' => 10.0, 'currency_code' => 'USD'], + 'price_array' => [null, ['value' => 11.11, 'currency_code' => 'USD'], ['value' => null, 'currency_code' => null]], + 'range_price_col' => [ + 'price_from' => ['value' => 1000.0, 'currency_code' => 'USD'], + 'price_to' => ['value' => 2000.0, 'currency_code' => 'USD'], + ], + ])->execute(); + + $query = (new Query($db))->from('test_composite_type')->one(); + + $priceColPhpType = $tableSchema->getColumn('price_col')->phpTypecast($query['price_col']); + $priceDefaultPhpType = $tableSchema->getColumn('price_default')->phpTypecast($query['price_default']); + $priceArrayPhpType = $tableSchema->getColumn('price_array')->phpTypecast($query['price_array']); + $rangePriceColPhpType = $tableSchema->getColumn('range_price_col')->phpTypecast($query['range_price_col']); + + $this->assertSame(['value' => 10.0, 'currency_code' => 'USD'], $priceColPhpType); + $this->assertSame(['value' => 5.0, 'currency_code' => 'USD'], $priceDefaultPhpType); + $this->assertSame( + [null, ['value' => 11.11, 'currency_code' => 'USD'], ['value' => null, 'currency_code' => null]], + $priceArrayPhpType + ); + $this->assertSame( + [ + 'price_from' => ['value' => 1000.0, 'currency_code' => 'USD'], + 'price_to' => ['value' => 2000.0, 'currency_code' => 'USD'], + ], + $rangePriceColPhpType + ); + + $db->close(); + } } diff --git a/tests/CompositeParserTest.php b/tests/CompositeParserTest.php new file mode 100644 index 00000000..bcb9423c --- /dev/null +++ b/tests/CompositeParserTest.php @@ -0,0 +1,32 @@ +assertSame([null], $compositeParse->parse('()')); + $this->assertSame([0 => null, 1 => null], $compositeParse->parse('(,)')); + $this->assertSame([0 => '1', 1 => '2', 2 => '3'], $compositeParse->parse('(1,2,3)')); + $this->assertSame([0 => '1', 1 => '-2', 2 => null, 3 => '42'], $compositeParse->parse('(1,-2,,42)')); + $this->assertSame([0 => ''], $compositeParse->parse('("")')); + $this->assertSame( + [0 => '[",","null",true,"false","f"]'], + $compositeParse->parse('("[\",\",\"null\",true,\"false\",\"f\"]")') + ); + + // Default values can have any expressions + $this->assertSame(null, $compositeParse->parse("'(1,2,3)::composite_type'")); + } +} diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index 62ee902c..d246c3c4 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -423,6 +423,85 @@ public static function columns(): array ], 'table_uuid', ], + [ + [ + 'id' => [ + 'type' => 'integer', + 'dbType' => 'int4', + 'phpType' => 'integer', + 'primaryKey' => true, + 'allowNull' => false, + 'autoIncrement' => true, + 'enumValues' => null, + 'size' => null, + 'precision' => 32, + 'scale' => 0, + 'defaultValue' => null, + ], + 'price_col' => [ + 'type' => 'composite', + 'dbType' => 'currency_money_composite', + 'phpType' => 'array', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + ], + 'price_default' => [ + 'type' => 'composite', + 'dbType' => 'currency_money_composite', + 'phpType' => 'array', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => ['value' => 5.0, 'currency_code' => 'USD'], + ], + 'price_array' => [ + 'type' => 'composite', + 'dbType' => 'currency_money_composite', + 'phpType' => 'array', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => [ + null, + ['value' => 10.55, 'currency_code' => 'USD'], + ['value' => -1.0, 'currency_code' => null] + ], + 'dimension' => 1, + ], + 'range_price_col' => [ + 'type' => 'composite', + 'dbType' => 'range_price_composite', + 'phpType' => 'array', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => [ + 'price_from' => ['value' => 0.0, 'currency_code' => 'USD'], + 'price_to' => ['value' => 100.0, 'currency_code' => 'USD'], + ], + 'dimension' => 0, + ], + ], + 'test_composite_type', + ], ]; } diff --git a/tests/Support/Fixture/pgsql.sql b/tests/Support/Fixture/pgsql.sql index 97df6930..ca4fe255 100644 --- a/tests/Support/Fixture/pgsql.sql +++ b/tests/Support/Fixture/pgsql.sql @@ -454,3 +454,26 @@ CREATE TABLE "table_uuid" ( "uuid" uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), "col" varchar(16) ); + +DROP TYPE IF EXISTS "currency_money_composite" CASCADE; +DROP TYPE IF EXISTS "range_price_composite" CASCADE; +DROP TABLE IF EXISTS "test_composite_type" CASCADE; + +CREATE TYPE "currency_money_composite" AS ( + "value" numeric(10,2), + "currency_code" char(3) +); + +CREATE TYPE "range_price_composite" AS ( + "price_from" "currency_money_composite", + "price_to" "currency_money_composite" +); + +CREATE TABLE "test_composite_type" +( + "id" SERIAL NOT NULL PRIMARY KEY, + "price_col" "currency_money_composite", + "price_default" "currency_money_composite" DEFAULT '(5,USD)', + "price_array" "currency_money_composite"[] DEFAULT '{null,"(10.55,USD)","(-1,)"}', + "range_price_col" "range_price_composite" DEFAULT '("(0,USD)","(100,USD)")' +); From 97863dc1fa03c1d6ae27f554021c09658b9aedb4 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 26 Jul 2023 13:05:12 +0700 Subject: [PATCH 02/34] Update comment and test --- src/CompositeExpression.php | 4 ++-- tests/ColumnSchemaTest.php | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/CompositeExpression.php b/src/CompositeExpression.php index e4e93216..a382aae3 100644 --- a/src/CompositeExpression.php +++ b/src/CompositeExpression.php @@ -20,10 +20,10 @@ * For example: * * ```php - * new CompositeExpression(['price' => 10, 'currency_code' => 'USD]); + * new CompositeExpression(['price' => 10, 'currency_code' => 'USD']); * ``` * - * Will be encoded to `ROW[10, USD]` + * Will be encoded to `ROW(10, USD)` * * @template-implements ArrayAccess * @template-implements IteratorAggregate diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 9fa5c2d8..06a16fdc 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -198,7 +198,11 @@ public function testCompositeType(): void $command->insert('test_composite_type', [ 'price_col' => ['value' => 10.0, 'currency_code' => 'USD'], - 'price_array' => [null, ['value' => 11.11, 'currency_code' => 'USD'], ['value' => null, 'currency_code' => null]], + 'price_array' => [ + null, + ['value' => 11.11, 'currency_code' => 'USD'], + ['value' => null, 'currency_code' => null] + ], 'range_price_col' => [ 'price_from' => ['value' => 1000.0, 'currency_code' => 'USD'], 'price_to' => ['value' => 2000.0, 'currency_code' => 'USD'], @@ -215,7 +219,11 @@ public function testCompositeType(): void $this->assertSame(['value' => 10.0, 'currency_code' => 'USD'], $priceColPhpType); $this->assertSame(['value' => 5.0, 'currency_code' => 'USD'], $priceDefaultPhpType); $this->assertSame( - [null, ['value' => 11.11, 'currency_code' => 'USD'], ['value' => null, 'currency_code' => null]], + [ + null, + ['value' => 11.11, 'currency_code' => 'USD'], + ['value' => null, 'currency_code' => null] + ], $priceArrayPhpType ); $this->assertSame( From 913ea1f42b257ba5eaefd704e01f7d852554d795 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 26 Jul 2023 13:12:01 +0700 Subject: [PATCH 03/34] Fix styleci issues --- src/CompositeExpression.php | 9 ++++----- tests/ColumnSchemaTest.php | 4 ++-- tests/Provider/SchemaProvider.php | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/CompositeExpression.php b/src/CompositeExpression.php index a382aae3..d2cc1f9c 100644 --- a/src/CompositeExpression.php +++ b/src/CompositeExpression.php @@ -38,7 +38,8 @@ public function __construct( private mixed $value = [], private string|null $type = null, private array|null $columns = null, - ) {} + ) { + } /** * The composite type name. @@ -99,9 +100,8 @@ public function getNormalizedValue(): mixed * * @param int|string $offset An offset to check for. * - * @return bool Its `true` on success or `false` on failure. - * * @throws InvalidConfigException If value is not an array. + * @return bool Its `true` on success or `false` on failure. */ public function offsetExists(mixed $offset): bool { @@ -116,9 +116,8 @@ public function offsetExists(mixed $offset): bool * * @param int|string $offset The offset to retrieve. * - * @return mixed Can return all value types. - * * @throws InvalidConfigException If value is not an array. + * @return mixed Can return all value types. */ public function offsetGet(mixed $offset): mixed { diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 06a16fdc..1890adfd 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -201,7 +201,7 @@ public function testCompositeType(): void 'price_array' => [ null, ['value' => 11.11, 'currency_code' => 'USD'], - ['value' => null, 'currency_code' => null] + ['value' => null, 'currency_code' => null], ], 'range_price_col' => [ 'price_from' => ['value' => 1000.0, 'currency_code' => 'USD'], @@ -222,7 +222,7 @@ public function testCompositeType(): void [ null, ['value' => 11.11, 'currency_code' => 'USD'], - ['value' => null, 'currency_code' => null] + ['value' => null, 'currency_code' => null], ], $priceArrayPhpType ); diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index d246c3c4..2f1c552d 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -478,7 +478,7 @@ public static function columns(): array 'defaultValue' => [ null, ['value' => 10.55, 'currency_code' => 'USD'], - ['value' => -1.0, 'currency_code' => null] + ['value' => -1.0, 'currency_code' => null], ], 'dimension' => 1, ], From 8d88a2a9331b0aa6409f543a3d05e6b1269f05ba Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 26 Jul 2023 13:22:37 +0700 Subject: [PATCH 04/34] Fix psalm issues --- src/ColumnSchema.php | 4 +++- src/CompositeExpression.php | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 8458abcd..9e5992b5 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -95,7 +95,7 @@ public function dbTypecast(mixed $value): mixed /** * @param int $dimension Should be more than 0 */ - private function dbTypecastArray(mixed $value, int $dimension): array|null + private function dbTypecastArray(mixed $value, int $dimension): array { $items = []; @@ -108,6 +108,7 @@ private function dbTypecastArray(mixed $value, int $dimension): array|null if ($dimension > 1) { $items[] = $this->dbTypecastArray($val, $dimension - 1); } else { + /** @psalm-suppress MixedAssignment */ $items[] = $this->dbTypecastValue($val); } } @@ -221,6 +222,7 @@ private function phpTypecastComposite(mixed $value): array|null $columnName = $columnNames[$key] ?? $key; if (isset($this->columns[$columnName])) { + /** @psalm-suppress MixedAssignment */ $fields[$columnName] = $this->columns[$columnName]->phpTypecast($val); } } diff --git a/src/CompositeExpression.php b/src/CompositeExpression.php index d2cc1f9c..c41c2ff6 100644 --- a/src/CompositeExpression.php +++ b/src/CompositeExpression.php @@ -84,8 +84,10 @@ public function getNormalizedValue(): mixed foreach ($this->columns as $name => $column) { if (array_key_exists($name, $this->value)) { + /** @psalm-suppress MixedAssignment */ $value[$name] = $this->value[$name]; } else { + /** @psalm-suppress MixedAssignment */ $value[$name] = $column->getDefaultValue(); } } From 17beaef818c60a5631556d70ed814ebc35994e32 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Thu, 27 Jul 2023 14:36:59 +0700 Subject: [PATCH 05/34] Fill skipped items for indexed `$value` --- src/CompositeExpression.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/CompositeExpression.php b/src/CompositeExpression.php index c41c2ff6..567d39e9 100644 --- a/src/CompositeExpression.php +++ b/src/CompositeExpression.php @@ -76,10 +76,22 @@ public function getValue(): mixed */ public function getNormalizedValue(): mixed { - if ($this->columns === null || !is_array($this->value) || !is_string(array_key_first($this->value))) { + if ($this->columns === null || !is_array($this->value)) { return $this->value; } + if (!is_string(array_key_first($this->value))) { + $value = $this->value; + $columns = array_values($this->columns); + + for ($i = count($value); $i < count($columns); ++$i) { + /** @psalm-suppress MixedAssignment */ + $value[$i] = $columns[$i]->getDefaultValue(); + } + + return $value; + } + $value = []; foreach ($this->columns as $name => $column) { From 7e24fd4b5222a8a6d0cb611109978ffdedcf5657 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Thu, 27 Jul 2023 15:21:50 +0700 Subject: [PATCH 06/34] Add CompositeExpressionInterface --- src/Builder/CompositeExpressionBuilder.php | 13 +++---- src/ColumnSchema.php | 1 + src/DQLQueryBuilder.php | 1 + src/{ => Expression}/CompositeExpression.php | 7 ++-- .../CompositeExpressionInterface.php | 35 +++++++++++++++++++ 5 files changed, 45 insertions(+), 12 deletions(-) rename src/{ => Expression}/CompositeExpression.php (95%) create mode 100644 src/Expression/CompositeExpressionInterface.php diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php index 39a4de3c..b4295a87 100644 --- a/src/Builder/CompositeExpressionBuilder.php +++ b/src/Builder/CompositeExpressionBuilder.php @@ -10,7 +10,8 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\ExpressionBuilderInterface; use Yiisoft\Db\Expression\ExpressionInterface; -use Yiisoft\Db\Pgsql\CompositeExpression; +use Yiisoft\Db\Pgsql\Expression\CompositeExpression; +use Yiisoft\Db\Pgsql\Expression\CompositeExpressionInterface; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; @@ -38,7 +39,7 @@ public function __construct(private QueryBuilderInterface $queryBuilder) * * @return string The raw SQL that won't be additionally escaped or quoted. * - * @psalm-param CompositeExpression $expression + * @psalm-param CompositeExpressionInterface $expression */ public function build(ExpressionInterface $expression, array &$params = []): string { @@ -73,10 +74,8 @@ public function build(ExpressionInterface $expression, array &$params = []): str * @throws InvalidArgumentException * @throws InvalidConfigException * @throws NotSupportedException - * - * @psalm-param CompositeExpression $expression */ - private function buildPlaceholders(ExpressionInterface $expression, array &$params): array + private function buildPlaceholders(CompositeExpressionInterface $expression, array &$params): array { $placeholders = []; @@ -118,10 +117,8 @@ private function buildPlaceholders(ExpressionInterface $expression, array &$para /** * @return string The typecast expression based on {@see type}. - * - * @psalm-param CompositeExpression $expression */ - private function getTypeHint(ExpressionInterface $expression): string + private function getTypeHint(CompositeExpressionInterface $expression): string { $type = $expression->getType(); diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 9e5992b5..a37105c8 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -10,6 +10,7 @@ use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Expression\JsonExpression; +use Yiisoft\Db\Pgsql\Expression\CompositeExpression; use Yiisoft\Db\Schema\AbstractColumnSchema; use Yiisoft\Db\Schema\ColumnSchemaInterface; use Yiisoft\Db\Schema\SchemaInterface; diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index cd0553d7..e8f5d9ba 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -10,6 +10,7 @@ use Yiisoft\Db\Pgsql\Builder\ArrayExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\CompositeExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\JsonExpressionBuilder; +use Yiisoft\Db\Pgsql\Expression\CompositeExpression; use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder; use Yiisoft\Db\QueryBuilder\Condition\LikeCondition; diff --git a/src/CompositeExpression.php b/src/Expression/CompositeExpression.php similarity index 95% rename from src/CompositeExpression.php rename to src/Expression/CompositeExpression.php index 567d39e9..36169411 100644 --- a/src/CompositeExpression.php +++ b/src/Expression/CompositeExpression.php @@ -2,14 +2,13 @@ declare(strict_types=1); -namespace Yiisoft\Db\Pgsql; +namespace Yiisoft\Db\Pgsql\Expression; use ArrayAccess; use ArrayIterator; use Countable; use IteratorAggregate; use Yiisoft\Db\Exception\InvalidConfigException; -use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Schema\ColumnSchemaInterface; use function count; @@ -28,7 +27,7 @@ * @template-implements ArrayAccess * @template-implements IteratorAggregate */ -class CompositeExpression implements ExpressionInterface, ArrayAccess, Countable, IteratorAggregate +class CompositeExpression implements CompositeExpressionInterface, ArrayAccess, Countable, IteratorAggregate { /** * @param ColumnSchemaInterface[]|null $columns @@ -72,7 +71,7 @@ public function getValue(): mixed } /** - * Sort values according to `$columns` order and fill skipped items with default values + * Sorted values according to order of the composite type columns and filled with default values skipped items. */ public function getNormalizedValue(): mixed { diff --git a/src/Expression/CompositeExpressionInterface.php b/src/Expression/CompositeExpressionInterface.php new file mode 100644 index 00000000..d1b4b1d0 --- /dev/null +++ b/src/Expression/CompositeExpressionInterface.php @@ -0,0 +1,35 @@ + Date: Fri, 28 Jul 2023 10:34:14 +0700 Subject: [PATCH 07/34] Add tests and fixes --- src/Builder/CompositeExpressionBuilder.php | 21 +- src/DQLQueryBuilder.php | 21 ++ src/Expression/CompositeExpression.php | 128 +-------- tests/CompositeExpressionTest.php | 39 +++ .../Provider/CompositeTypeSchemaProvider.php | 261 ++++++++++++++++++ tests/Provider/QueryBuilderProvider.php | 77 ++++++ tests/Provider/SchemaProvider.php | 79 ------ tests/QueryBuilderTest.php | 21 ++ tests/SchemaTest.php | 30 ++ 9 files changed, 471 insertions(+), 206 deletions(-) create mode 100644 tests/CompositeExpressionTest.php create mode 100644 tests/Provider/CompositeTypeSchemaProvider.php diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php index b4295a87..08705d28 100644 --- a/src/Builder/CompositeExpressionBuilder.php +++ b/src/Builder/CompositeExpressionBuilder.php @@ -43,6 +43,13 @@ public function __construct(private QueryBuilderInterface $queryBuilder) */ public function build(ExpressionInterface $expression, array &$params = []): string { + if (!$expression instanceof CompositeExpressionInterface) { + throw new \InvalidArgumentException( + 'TypeError: ' . self::class. '::build(): Argument #1 ($expression) must be instance of ' + . CompositeExpressionInterface::class . ', instance of ' . $expression::class . ' given.' + ); + } + /** @psalm-var mixed $value */ $value = $expression->getValue(); @@ -87,6 +94,12 @@ private function buildPlaceholders(CompositeExpressionInterface $expression, arr } $columns = (array) $expression->getColumns(); + + // TODO retrieve columns from schema + // if (empty($columns) && $expression->getType() !== null) { + // $columns = $schema->findColumns((string) $expression->getType()); + // } + $columnNames = array_keys($columns); /** @@ -98,13 +111,11 @@ private function buildPlaceholders(CompositeExpressionInterface $expression, arr $columnName = $columnNames[$columnName] ?? null; } - if ($columnName === null || !isset($columns[$columnName])) { - continue; + if ($columnName !== null && isset($columns[$columnName])) { + /** @psalm-var mixed $item */ + $item = $columns[$columnName]->dbTypecast($item); } - /** @psalm-var mixed $item */ - $item = $columns[$columnName]->dbTypecast($item); - if ($item instanceof ExpressionInterface) { $placeholders[] = $this->queryBuilder->buildExpression($item, $params); } else { diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index e8f5d9ba..267b43e1 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -4,8 +4,10 @@ namespace Yiisoft\Db\Pgsql; +use Yiisoft\Db\Exception\InvalidArgumentException; use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\ExpressionBuilderInterface; +use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Pgsql\Builder\ArrayExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\CompositeExpressionBuilder; @@ -55,4 +57,23 @@ protected function defaultExpressionBuilders(): array CompositeExpression::class => CompositeExpressionBuilder::class, ]); } + + public function getExpressionBuilder(ExpressionInterface $expression): object + { + $className = $expression::class; + + if (!isset($this->expressionBuilders[$className])) { + foreach ($this->expressionBuilders as $expressionClassName => $builderClassName) { + if ($className instanceof $expressionClassName) { + return new $builderClassName($this->queryBuilder); + } + } + + throw new InvalidArgumentException( + 'Expression of class ' . $className . ' can not be built in ' . static::class + ); + } + + return new $this->expressionBuilders[$className]($this->queryBuilder); + } } diff --git a/src/Expression/CompositeExpression.php b/src/Expression/CompositeExpression.php index 36169411..496a7286 100644 --- a/src/Expression/CompositeExpression.php +++ b/src/Expression/CompositeExpression.php @@ -5,14 +5,9 @@ namespace Yiisoft\Db\Pgsql\Expression; use ArrayAccess; -use ArrayIterator; -use Countable; use IteratorAggregate; -use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Schema\ColumnSchemaInterface; -use function count; - /** * Represents a composite SQL expression. * @@ -27,7 +22,7 @@ * @template-implements ArrayAccess * @template-implements IteratorAggregate */ -class CompositeExpression implements CompositeExpressionInterface, ArrayAccess, Countable, IteratorAggregate +class CompositeExpression implements CompositeExpressionInterface { /** * @param ColumnSchemaInterface[]|null $columns @@ -79,21 +74,14 @@ public function getNormalizedValue(): mixed return $this->value; } - if (!is_string(array_key_first($this->value))) { - $value = $this->value; - $columns = array_values($this->columns); - - for ($i = count($value); $i < count($columns); ++$i) { - /** @psalm-suppress MixedAssignment */ - $value[$i] = $columns[$i]->getDefaultValue(); - } + $value = []; + $columns = $this->columns; - return $value; + if (is_int(array_key_first($this->value))) { + $columns = array_values($this->columns); } - $value = []; - - foreach ($this->columns as $name => $column) { + foreach ($columns as $name => $column) { if (array_key_exists($name, $this->value)) { /** @psalm-suppress MixedAssignment */ $value[$name] = $this->value[$name]; @@ -105,108 +93,4 @@ public function getNormalizedValue(): mixed return $value; } - - /** - * Whether an offset exists. - * - * @link https://php.net/manual/en/arrayaccess.offsetexists.php - * - * @param int|string $offset An offset to check for. - * - * @throws InvalidConfigException If value is not an array. - * @return bool Its `true` on success or `false` on failure. - */ - public function offsetExists(mixed $offset): bool - { - $this->value = $this->validateValue($this->value); - return array_key_exists($offset, $this->value); - } - - /** - * Offset to retrieve. - * - * @link https://php.net/manual/en/arrayaccess.offsetget.php - * - * @param int|string $offset The offset to retrieve. - * - * @throws InvalidConfigException If value is not an array. - * @return mixed Can return all value types. - */ - public function offsetGet(mixed $offset): mixed - { - $this->value = $this->validateValue($this->value); - return $this->value[$offset]; - } - - /** - * Offset to set. - * - * @link https://php.net/manual/en/arrayaccess.offsetset.php - * - * @param int|string $offset The offset to assign the value to. - * @param mixed $value The value to set. - * - * @throws InvalidConfigException If content value is not an array. - */ - public function offsetSet(mixed $offset, mixed $value): void - { - $this->value = $this->validateValue($this->value); - $this->value[$offset] = $value; - } - - /** - * Offset to unset. - * - * @param int|string $offset The offset to unset. - * - * @throws InvalidConfigException If value is not an array. - * - * @link https://php.net/manual/en/arrayaccess.offsetunset.php - */ - public function offsetUnset(mixed $offset): void - { - $this->value = $this->validateValue($this->value); - unset($this->value[$offset]); - } - - /** - * Count elements of the composite type's content. - * - * @link https://php.net/manual/en/countable.count.php - * - * @return int The custom count as an integer. - */ - public function count(): int - { - return count((array) $this->value); - } - - /** - * Retrieve an external iterator. - * - * @link https://php.net/manual/en/iteratoraggregate.getiterator.php - * - * @throws InvalidConfigException If value is not an array. - * - * @return ArrayIterator An instance of an object implementing `Iterator` or `Traversable`. - */ - public function getIterator(): ArrayIterator - { - $this->value = $this->validateValue($this->value); - return new ArrayIterator($this->value); - } - - /** - * Validates the value of the composite expression is an array. - * - * @throws InvalidConfigException If value is not an array. - */ - private function validateValue(mixed $value): array - { - if (!is_array($value)) { - throw new InvalidConfigException('The CompositeExpression value must be an array.'); - } - - return $value; - } } diff --git a/tests/CompositeExpressionTest.php b/tests/CompositeExpressionTest.php new file mode 100644 index 00000000..799f97ce --- /dev/null +++ b/tests/CompositeExpressionTest.php @@ -0,0 +1,39 @@ +getConnection(true); + $schema = $db->getSchema(); + $tableSchema = $schema->getTableSchema('test_composite_type'); + + $columns = $tableSchema->getColumn('price_default')->getColumns(); + $this->assertNotNull($columns); + + $compositeExpression = new CompositeExpression(['currency_code' => 'USD', 'value' => 10.0], 'currency_money_composite', $columns); + $this->assertSame(['value' => 10.0, 'currency_code' => 'USD'], $compositeExpression->getNormalizedValue()); + + $compositeExpression = new CompositeExpression(['value' => 10.0], 'currency_money_composite', $columns); + $this->assertSame(['value' => 10.0, 'currency_code' => 'USD'], $compositeExpression->getNormalizedValue()); + + $compositeExpression = new CompositeExpression([10.0], 'currency_money_composite', $columns); + $this->assertSame([10.0, 'USD'], $compositeExpression->getNormalizedValue()); + + $compositeExpression = new CompositeExpression([], 'currency_money_composite', $columns); + $this->assertSame(['value' => 5.0, 'currency_code' => 'USD'], $compositeExpression->getNormalizedValue()); + } +} diff --git a/tests/Provider/CompositeTypeSchemaProvider.php b/tests/Provider/CompositeTypeSchemaProvider.php new file mode 100644 index 00000000..ef5a6c72 --- /dev/null +++ b/tests/Provider/CompositeTypeSchemaProvider.php @@ -0,0 +1,261 @@ + [ + 'type' => 'integer', + 'dbType' => 'int4', + 'phpType' => 'integer', + 'primaryKey' => true, + 'allowNull' => false, + 'autoIncrement' => true, + 'enumValues' => null, + 'size' => null, + 'precision' => 32, + 'scale' => 0, + 'defaultValue' => null, + ], + 'price_col' => [ + 'type' => 'composite', + 'dbType' => 'currency_money_composite', + 'phpType' => 'array', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + 'columns' => [ + 'value' => [ + 'type' => 'decimal', + 'dbType' => 'numeric', + 'phpType' => 'double', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => 10, + 'scale' => 2, + 'defaultValue' => null, + ], + 'currency_code' => [ + 'type' => 'char', + 'dbType' => 'bpchar', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => 3, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + ], + ], + ], + 'price_default' => [ + 'type' => 'composite', + 'dbType' => 'currency_money_composite', + 'phpType' => 'array', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => ['value' => 5.0, 'currency_code' => 'USD'], + 'columns' => [ + 'value' => [ + 'type' => 'decimal', + 'dbType' => 'numeric', + 'phpType' => 'double', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => 10, + 'scale' => 2, + 'defaultValue' => null, + ], + 'currency_code' => [ + 'type' => 'char', + 'dbType' => 'bpchar', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => 3, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + ], + ], + ], + 'price_array' => [ + 'type' => 'composite', + 'dbType' => 'currency_money_composite', + 'phpType' => 'array', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => [ + null, + ['value' => 10.55, 'currency_code' => 'USD'], + ['value' => -1.0, 'currency_code' => null], + ], + 'dimension' => 1, + 'columns' => [ + 'value' => [ + 'type' => 'decimal', + 'dbType' => 'numeric', + 'phpType' => 'double', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => 10, + 'scale' => 2, + 'defaultValue' => null, + ], + 'currency_code' => [ + 'type' => 'char', + 'dbType' => 'bpchar', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => 3, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + ], + ], + ], + 'range_price_col' => [ + 'type' => 'composite', + 'dbType' => 'range_price_composite', + 'phpType' => 'array', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => [ + 'price_from' => ['value' => 0.0, 'currency_code' => 'USD'], + 'price_to' => ['value' => 100.0, 'currency_code' => 'USD'], + ], + 'dimension' => 0, + 'columns' => [ + 'price_from' => [ + 'type' => 'composite', + 'dbType' => 'currency_money_composite', + 'phpType' => 'array', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + 'columns' => [ + 'value' => [ + 'type' => 'decimal', + 'dbType' => 'numeric', + 'phpType' => 'double', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => 10, + 'scale' => 2, + 'defaultValue' => null, + ], + 'currency_code' => [ + 'type' => 'char', + 'dbType' => 'bpchar', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => 3, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + ], + ], + ], + 'price_to' => [ + 'type' => 'composite', + 'dbType' => 'currency_money_composite', + 'phpType' => 'array', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + 'columns' => [ + 'value' => [ + 'type' => 'decimal', + 'dbType' => 'numeric', + 'phpType' => 'double', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => 10, + 'scale' => 2, + 'defaultValue' => null, + ], + 'currency_code' => [ + 'type' => 'char', + 'dbType' => 'bpchar', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => 3, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + ], + ], + ], + ], + ], + ], + 'test_composite_type', + ], + ]; + } +} diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 0e35e6dc..c4da81a5 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -7,6 +7,8 @@ use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\JsonExpression; +use Yiisoft\Db\Pgsql\ColumnSchema; +use Yiisoft\Db\Pgsql\Expression\CompositeExpression; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Schema\SchemaInterface; @@ -24,6 +26,24 @@ public static function buildCondition(): array { $buildCondition = parent::buildCondition(); + $valueColumn = new ColumnSchema('value'); + $valueColumn->type('decimal'); + $valueColumn->dbType('numeric'); + $valueColumn->phpType('double'); + $valueColumn->precision(10); + $valueColumn->scale(2); + + $currencyCodeColumn = new ColumnSchema('currency_code'); + $currencyCodeColumn->type('char'); + $currencyCodeColumn->dbType('bpchar'); + $currencyCodeColumn->phpType('string'); + $currencyCodeColumn->size(3); + + $priceColumns = [ + 'value' => $valueColumn, + 'currency_code' => $currencyCodeColumn, + ]; + return array_merge( $buildCondition, [ @@ -245,6 +265,63 @@ public static function buildCondition(): array [['>=', 'id', new ArrayExpression([1])], '"id" >= ARRAY[:qp0]', [':qp0' => 1]], [['<=', 'id', new ArrayExpression([1])], '"id" <= ARRAY[:qp0]', [':qp0' => 1]], [['&&', 'id', new ArrayExpression([1])], '"id" && ARRAY[:qp0]', [':qp0' => 1]], + + /* composite conditions */ + 'composite without type' => [ + ['=', 'price_col', new CompositeExpression(['value' => 10, 'currency_code' => 'USD'])], + '[[price_col]] = ROW(:qp0, :qp1)', + [':qp0' => 10, ':qp1' => 'USD'], + ], + 'composite with type' => [ + ['=', 'price_col', new CompositeExpression(['value' => 10, 'currency_code' => 'USD'], 'currency_money_composite')], + '[[price_col]] = ROW(:qp0, :qp1)::currency_money_composite', + [':qp0' => 10, ':qp1' => 'USD'], + ], + 'composite with columns' => [ + ['=', 'price_col', new CompositeExpression(['value' => 10, 'currency_code' => 'USD'], 'currency_money_composite', $priceColumns)], + '[[price_col]] = ROW(:qp0, :qp1)::currency_money_composite', + [':qp0' => 10.0, ':qp1' => 'USD'], + ], + 'scalar can not be converted to composite' => [['=', 'price_col', new CompositeExpression(1)], '"price_col" = NULL', []], + 'array of composite' => [ + ['=', 'price_array', new ArrayExpression( + [ + null, + new CompositeExpression(['value' => 11.11, 'currency_code' => 'USD']), + new CompositeExpression(['value' => null, 'currency_code' => null]), + ] + )], + '"price_array" = ARRAY[:qp0, ROW(:qp1, :qp2), ROW(:qp3, :qp4)]', + [':qp0' => null, ':qp1' => 11.11, ':qp2' => 'USD', ':qp3' => null, ':qp4' => null], + ], + 'composite null value' => [['=', 'price_col', new CompositeExpression(null)], '"price_col" = NULL', []], + 'composite null values' => [ + ['=', 'price_col', new CompositeExpression([null, null])], '"price_col" = ROW(:qp0, :qp1)', [':qp0' => null, ':qp1' => null], + ], + 'composite query' => [ + ['=', 'price_col', new CompositeExpression( + (new Query(self::getDb()))->select('price')->from('product')->where(['id' => 1]) + )], + '[[price_col]] = (SELECT [[price]] FROM [[product]] WHERE [[id]]=:qp0)', + [':qp0' => 1], + ], + 'composite query with type' => [ + [ + '=', + 'price_col', + new CompositeExpression( + (new Query(self::getDb()))->select('price')->from('product')->where(['id' => 1]), + 'currency_money_composite' + ), + ], + '[[price_col]] = (SELECT [[price]] FROM [[product]] WHERE [[id]]=:qp0)::currency_money_composite', + [':qp0' => 1], + ], + 'traversable objects are supported in composite' => [ + ['=', 'price_col', new CompositeExpression(new TraversableObject([10, 'USD']))], + '[[price_col]] = ROW(:qp0, :qp1)', + [':qp0' => 10, ':qp1' => 'USD'], + ], ] ); } diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index 2f1c552d..62ee902c 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -423,85 +423,6 @@ public static function columns(): array ], 'table_uuid', ], - [ - [ - 'id' => [ - 'type' => 'integer', - 'dbType' => 'int4', - 'phpType' => 'integer', - 'primaryKey' => true, - 'allowNull' => false, - 'autoIncrement' => true, - 'enumValues' => null, - 'size' => null, - 'precision' => 32, - 'scale' => 0, - 'defaultValue' => null, - ], - 'price_col' => [ - 'type' => 'composite', - 'dbType' => 'currency_money_composite', - 'phpType' => 'array', - 'primaryKey' => false, - 'allowNull' => true, - 'autoIncrement' => false, - 'enumValues' => null, - 'size' => null, - 'precision' => null, - 'scale' => null, - 'defaultValue' => null, - ], - 'price_default' => [ - 'type' => 'composite', - 'dbType' => 'currency_money_composite', - 'phpType' => 'array', - 'primaryKey' => false, - 'allowNull' => true, - 'autoIncrement' => false, - 'enumValues' => null, - 'size' => null, - 'precision' => null, - 'scale' => null, - 'defaultValue' => ['value' => 5.0, 'currency_code' => 'USD'], - ], - 'price_array' => [ - 'type' => 'composite', - 'dbType' => 'currency_money_composite', - 'phpType' => 'array', - 'primaryKey' => false, - 'allowNull' => true, - 'autoIncrement' => false, - 'enumValues' => null, - 'size' => null, - 'precision' => null, - 'scale' => null, - 'defaultValue' => [ - null, - ['value' => 10.55, 'currency_code' => 'USD'], - ['value' => -1.0, 'currency_code' => null], - ], - 'dimension' => 1, - ], - 'range_price_col' => [ - 'type' => 'composite', - 'dbType' => 'range_price_composite', - 'phpType' => 'array', - 'primaryKey' => false, - 'allowNull' => true, - 'autoIncrement' => false, - 'enumValues' => null, - 'size' => null, - 'precision' => null, - 'scale' => null, - 'defaultValue' => [ - 'price_from' => ['value' => 0.0, 'currency_code' => 'USD'], - 'price_to' => ['value' => 100.0, 'currency_code' => 'USD'], - ], - 'dimension' => 0, - ], - ], - 'test_composite_type', - ], ]; } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index b5737815..6bc9d1c1 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -11,8 +11,12 @@ use Yiisoft\Db\Exception\IntegrityException; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\ExpressionInterface; +use Yiisoft\Db\Pgsql\Builder\CompositeExpressionBuilder; use Yiisoft\Db\Pgsql\Column; +use Yiisoft\Db\Pgsql\Expression\CompositeExpression; +use Yiisoft\Db\Pgsql\Expression\CompositeExpressionInterface; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\Schema\SchemaInterface; @@ -659,4 +663,21 @@ public function testUpsertExecute( ): void { parent::testUpsertExecute($table, $insertColumns, $updateColumns); } + + public function testCompositeExpressionBuilder() + { + $db = $this->getConnection(); + $queryBuilder = $db->getQueryBuilder(); + $expressionBuilder = $queryBuilder->getExpressionBuilder(new CompositeExpression()); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage( + 'TypeError: ' . CompositeExpressionBuilder::class. '::build(): Argument #1 ($expression) must be instance of ' + . CompositeExpressionInterface::class . ', instance of ' . ArrayExpression::class . ' given.' + ); + + $expressionBuilder->build(new ArrayExpression()); + + $db->close(); + } } diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 293845f8..eb6b4ad7 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -557,4 +557,34 @@ public function testDomainType(): void $db->close(); } + + /** + * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CompositeTypeSchemaProvider::columns + * + * @throws Exception + */ + public function testCompositeTypeColumnSchema(array $columns, string $tableName): void + { + $this->testCompositeTypeColumnSchemaRecursive($columns, $tableName); + } + + private function testCompositeTypeColumnSchemaRecursive(array $columns, string $tableName): void + { + $this->columnSchema($columns, $tableName); + + $db = $this->getConnection(true); + $table = $db->getTableSchema($tableName, true); + + foreach ($table->getColumns() as $name => $column) { + if ($column->getType() === 'composite') { + $this->assertTrue( + isset($columns[$name]['columns']), + "Composite type's columns of column `$name` do not exist. type is `{$column->getType()}`, dbType is `{$column->getDbType()}`." + ); + $this->testCompositeTypeColumnSchemaRecursive($columns[$name]['columns'], $column->getDbType()); + } + } + + $db->close(); + } } From 42ff0062801fed25277c7292d4cad4b4f7313e54 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 28 Jul 2023 10:43:13 +0700 Subject: [PATCH 08/34] Remove CompositeExpressionInterface --- src/Builder/CompositeExpressionBuilder.php | 11 +++--- src/DQLQueryBuilder.php | 21 ----------- src/Expression/CompositeExpression.php | 10 ++---- .../CompositeExpressionInterface.php | 35 ------------------- tests/QueryBuilderTest.php | 5 ++- 5 files changed, 9 insertions(+), 73 deletions(-) delete mode 100644 src/Expression/CompositeExpressionInterface.php diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php index 08705d28..6abe2bac 100644 --- a/src/Builder/CompositeExpressionBuilder.php +++ b/src/Builder/CompositeExpressionBuilder.php @@ -11,7 +11,6 @@ use Yiisoft\Db\Expression\ExpressionBuilderInterface; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Expression\CompositeExpression; -use Yiisoft\Db\Pgsql\Expression\CompositeExpressionInterface; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; @@ -38,15 +37,13 @@ public function __construct(private QueryBuilderInterface $queryBuilder) * @throws NotSupportedException * * @return string The raw SQL that won't be additionally escaped or quoted. - * - * @psalm-param CompositeExpressionInterface $expression */ public function build(ExpressionInterface $expression, array &$params = []): string { - if (!$expression instanceof CompositeExpressionInterface) { + if (!$expression instanceof CompositeExpression) { throw new \InvalidArgumentException( 'TypeError: ' . self::class. '::build(): Argument #1 ($expression) must be instance of ' - . CompositeExpressionInterface::class . ', instance of ' . $expression::class . ' given.' + . CompositeExpression::class . ', instance of ' . $expression::class . ' given.' ); } @@ -82,7 +79,7 @@ public function build(ExpressionInterface $expression, array &$params = []): str * @throws InvalidConfigException * @throws NotSupportedException */ - private function buildPlaceholders(CompositeExpressionInterface $expression, array &$params): array + private function buildPlaceholders(CompositeExpression $expression, array &$params): array { $placeholders = []; @@ -129,7 +126,7 @@ private function buildPlaceholders(CompositeExpressionInterface $expression, arr /** * @return string The typecast expression based on {@see type}. */ - private function getTypeHint(CompositeExpressionInterface $expression): string + private function getTypeHint(CompositeExpression $expression): string { $type = $expression->getType(); diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index 267b43e1..e8f5d9ba 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -4,10 +4,8 @@ namespace Yiisoft\Db\Pgsql; -use Yiisoft\Db\Exception\InvalidArgumentException; use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\ExpressionBuilderInterface; -use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Pgsql\Builder\ArrayExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\CompositeExpressionBuilder; @@ -57,23 +55,4 @@ protected function defaultExpressionBuilders(): array CompositeExpression::class => CompositeExpressionBuilder::class, ]); } - - public function getExpressionBuilder(ExpressionInterface $expression): object - { - $className = $expression::class; - - if (!isset($this->expressionBuilders[$className])) { - foreach ($this->expressionBuilders as $expressionClassName => $builderClassName) { - if ($className instanceof $expressionClassName) { - return new $builderClassName($this->queryBuilder); - } - } - - throw new InvalidArgumentException( - 'Expression of class ' . $className . ' can not be built in ' . static::class - ); - } - - return new $this->expressionBuilders[$className]($this->queryBuilder); - } } diff --git a/src/Expression/CompositeExpression.php b/src/Expression/CompositeExpression.php index 496a7286..a08b4b4d 100644 --- a/src/Expression/CompositeExpression.php +++ b/src/Expression/CompositeExpression.php @@ -4,8 +4,7 @@ namespace Yiisoft\Db\Pgsql\Expression; -use ArrayAccess; -use IteratorAggregate; +use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Schema\ColumnSchemaInterface; /** @@ -18,18 +17,15 @@ * ``` * * Will be encoded to `ROW(10, USD)` - * - * @template-implements ArrayAccess - * @template-implements IteratorAggregate */ -class CompositeExpression implements CompositeExpressionInterface +class CompositeExpression implements ExpressionInterface { /** * @param ColumnSchemaInterface[]|null $columns * @psalm-param array|null $columns */ public function __construct( - private mixed $value = [], + private mixed $value, private string|null $type = null, private array|null $columns = null, ) { diff --git a/src/Expression/CompositeExpressionInterface.php b/src/Expression/CompositeExpressionInterface.php deleted file mode 100644 index d1b4b1d0..00000000 --- a/src/Expression/CompositeExpressionInterface.php +++ /dev/null @@ -1,35 +0,0 @@ -getConnection(); $queryBuilder = $db->getQueryBuilder(); - $expressionBuilder = $queryBuilder->getExpressionBuilder(new CompositeExpression()); + $expressionBuilder = $queryBuilder->getExpressionBuilder(new CompositeExpression([])); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage( 'TypeError: ' . CompositeExpressionBuilder::class. '::build(): Argument #1 ($expression) must be instance of ' - . CompositeExpressionInterface::class . ', instance of ' . ArrayExpression::class . ' given.' + . CompositeExpression::class . ', instance of ' . ArrayExpression::class . ' given.' ); $expressionBuilder->build(new ArrayExpression()); From 6cc292091cbb0e3b2b71e9c064cf4fe172087ce2 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 28 Jul 2023 10:44:56 +0700 Subject: [PATCH 09/34] Remove TODO retrieve columns from schema --- src/Builder/CompositeExpressionBuilder.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php index 6abe2bac..03fb5356 100644 --- a/src/Builder/CompositeExpressionBuilder.php +++ b/src/Builder/CompositeExpressionBuilder.php @@ -91,12 +91,6 @@ private function buildPlaceholders(CompositeExpression $expression, array &$para } $columns = (array) $expression->getColumns(); - - // TODO retrieve columns from schema - // if (empty($columns) && $expression->getType() !== null) { - // $columns = $schema->findColumns((string) $expression->getType()); - // } - $columnNames = array_keys($columns); /** From 3f6c6806613efdcc6a5408a3fb6304430f715cda Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 28 Jul 2023 10:57:22 +0700 Subject: [PATCH 10/34] Move to folder `Composite` --- src/Builder/CompositeExpressionBuilder.php | 2 +- src/ColumnSchema.php | 3 ++- src/{Expression => Composite}/CompositeExpression.php | 2 +- src/{ => Composite}/CompositeParser.php | 2 +- src/DQLQueryBuilder.php | 2 +- tests/CompositeExpressionTest.php | 2 +- tests/CompositeParserTest.php | 2 +- tests/Provider/QueryBuilderProvider.php | 2 +- tests/QueryBuilderTest.php | 2 +- 9 files changed, 10 insertions(+), 9 deletions(-) rename src/{Expression => Composite}/CompositeExpression.php (98%) rename src/{ => Composite}/CompositeParser.php (97%) diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php index 03fb5356..9fa55ce6 100644 --- a/src/Builder/CompositeExpressionBuilder.php +++ b/src/Builder/CompositeExpressionBuilder.php @@ -10,7 +10,7 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\ExpressionBuilderInterface; use Yiisoft\Db\Expression\ExpressionInterface; -use Yiisoft\Db\Pgsql\Expression\CompositeExpression; +use Yiisoft\Db\Pgsql\Composite\CompositeExpression; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index a37105c8..9df5ad53 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -10,7 +10,8 @@ use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Expression\JsonExpression; -use Yiisoft\Db\Pgsql\Expression\CompositeExpression; +use Yiisoft\Db\Pgsql\Composite\CompositeExpression; +use Yiisoft\Db\Pgsql\Composite\CompositeParser; use Yiisoft\Db\Schema\AbstractColumnSchema; use Yiisoft\Db\Schema\ColumnSchemaInterface; use Yiisoft\Db\Schema\SchemaInterface; diff --git a/src/Expression/CompositeExpression.php b/src/Composite/CompositeExpression.php similarity index 98% rename from src/Expression/CompositeExpression.php rename to src/Composite/CompositeExpression.php index a08b4b4d..d13afa56 100644 --- a/src/Expression/CompositeExpression.php +++ b/src/Composite/CompositeExpression.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Yiisoft\Db\Pgsql\Expression; +namespace Yiisoft\Db\Pgsql\Composite; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Schema\ColumnSchemaInterface; diff --git a/src/CompositeParser.php b/src/Composite/CompositeParser.php similarity index 97% rename from src/CompositeParser.php rename to src/Composite/CompositeParser.php index 2ec06929..84cb41fe 100644 --- a/src/CompositeParser.php +++ b/src/Composite/CompositeParser.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Yiisoft\Db\Pgsql; +namespace Yiisoft\Db\Pgsql\Composite; /** * Composite type representation to PHP array parser for PostgreSQL Server. diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index e8f5d9ba..1ed4a577 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -10,7 +10,7 @@ use Yiisoft\Db\Pgsql\Builder\ArrayExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\CompositeExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\JsonExpressionBuilder; -use Yiisoft\Db\Pgsql\Expression\CompositeExpression; +use Yiisoft\Db\Pgsql\Composite\CompositeExpression; use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder; use Yiisoft\Db\QueryBuilder\Condition\LikeCondition; diff --git a/tests/CompositeExpressionTest.php b/tests/CompositeExpressionTest.php index 799f97ce..b1c237c8 100644 --- a/tests/CompositeExpressionTest.php +++ b/tests/CompositeExpressionTest.php @@ -5,7 +5,7 @@ namespace Yiisoft\Db\Pgsql\Tests; use PHPUnit\Framework\TestCase; -use Yiisoft\Db\Pgsql\Expression\CompositeExpression; +use Yiisoft\Db\Pgsql\Composite\CompositeExpression; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; /** diff --git a/tests/CompositeParserTest.php b/tests/CompositeParserTest.php index bcb9423c..87fa2c8f 100644 --- a/tests/CompositeParserTest.php +++ b/tests/CompositeParserTest.php @@ -5,7 +5,7 @@ namespace Yiisoft\Db\Pgsql\Tests; use PHPUnit\Framework\TestCase; -use Yiisoft\Db\Pgsql\CompositeParser; +use Yiisoft\Db\Pgsql\Composite\CompositeParser; /** * @group pgsql diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index c4da81a5..51c76a04 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -8,7 +8,7 @@ use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Pgsql\ColumnSchema; -use Yiisoft\Db\Pgsql\Expression\CompositeExpression; +use Yiisoft\Db\Pgsql\Composite\CompositeExpression; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Schema\SchemaInterface; diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index ba227d29..20dfa9a8 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -15,7 +15,7 @@ use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Builder\CompositeExpressionBuilder; use Yiisoft\Db\Pgsql\Column; -use Yiisoft\Db\Pgsql\Expression\CompositeExpression; +use Yiisoft\Db\Pgsql\Composite\CompositeExpression; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\Schema\SchemaInterface; From 47c0622318235b675e30ee4dfbc2982466885e5c Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 28 Jul 2023 12:14:21 +0700 Subject: [PATCH 11/34] Apply fixes from StyleCI --- src/Builder/CompositeExpressionBuilder.php | 2 +- tests/QueryBuilderTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php index 9fa55ce6..eacd1cec 100644 --- a/src/Builder/CompositeExpressionBuilder.php +++ b/src/Builder/CompositeExpressionBuilder.php @@ -42,7 +42,7 @@ public function build(ExpressionInterface $expression, array &$params = []): str { if (!$expression instanceof CompositeExpression) { throw new \InvalidArgumentException( - 'TypeError: ' . self::class. '::build(): Argument #1 ($expression) must be instance of ' + 'TypeError: ' . self::class . '::build(): Argument #1 ($expression) must be instance of ' . CompositeExpression::class . ', instance of ' . $expression::class . ' given.' ); } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 20dfa9a8..911469f3 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -671,7 +671,7 @@ public function testCompositeExpressionBuilder() $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage( - 'TypeError: ' . CompositeExpressionBuilder::class. '::build(): Argument #1 ($expression) must be instance of ' + 'TypeError: ' . CompositeExpressionBuilder::class . '::build(): Argument #1 ($expression) must be instance of ' . CompositeExpression::class . ', instance of ' . ArrayExpression::class . ' given.' ); From d0c21d3a02efc37b3f519ed3b4eb3a1d1305a005 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 28 Jul 2023 13:09:57 +0700 Subject: [PATCH 12/34] Add tests and fixes --- src/ColumnSchema.php | 21 +++++----- tests/ColumnSchemaTest.php | 23 ++++++++++ .../Provider/CompositeTypeSchemaProvider.php | 42 +++++++++++++++++++ tests/Support/Fixture/pgsql.sql | 1 + 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 9df5ad53..8960713e 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -201,10 +201,6 @@ protected function phpTypecastValue(mixed $value): mixed private function phpTypecastComposite(mixed $value): array|null { - if ($this->columns === null) { - return null; - } - if (is_string($value)) { $value = $this->getCompositeParser()->parse($value); } @@ -214,19 +210,22 @@ private function phpTypecastComposite(mixed $value): array|null } $fields = []; - $columnNames = array_keys($this->columns); + $columnNames = array_keys((array) $this->columns); /** - * @psalm-var int|string $key - * @psalm-var mixed $val + * @psalm-var int|string $columnName + * @psalm-var mixed $item */ - foreach ($value as $key => $val) { - $columnName = $columnNames[$key] ?? $key; + foreach ($value as $columnName => $item) { + $columnName = $columnNames[$columnName] ?? $columnName; if (isset($this->columns[$columnName])) { - /** @psalm-suppress MixedAssignment */ - $fields[$columnName] = $this->columns[$columnName]->phpTypecast($val); + /** @psalm-var mixed $item */ + $item = $this->columns[$columnName]->phpTypecast($item); } + + /** @psalm-suppress MixedAssignment */ + $fields[$columnName] = $item; } return $fields; diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 1890adfd..cb9ec0be 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -203,6 +203,9 @@ public function testCompositeType(): void ['value' => 11.11, 'currency_code' => 'USD'], ['value' => null, 'currency_code' => null], ], + 'price_array2' => [[ + ['value' => 123.45, 'currency_code' => 'USD'], + ]], 'range_price_col' => [ 'price_from' => ['value' => 1000.0, 'currency_code' => 'USD'], 'price_to' => ['value' => 2000.0, 'currency_code' => 'USD'], @@ -214,6 +217,7 @@ public function testCompositeType(): void $priceColPhpType = $tableSchema->getColumn('price_col')->phpTypecast($query['price_col']); $priceDefaultPhpType = $tableSchema->getColumn('price_default')->phpTypecast($query['price_default']); $priceArrayPhpType = $tableSchema->getColumn('price_array')->phpTypecast($query['price_array']); + $priceArray2PhpType = $tableSchema->getColumn('price_array2')->phpTypecast($query['price_array2']); $rangePriceColPhpType = $tableSchema->getColumn('range_price_col')->phpTypecast($query['range_price_col']); $this->assertSame(['value' => 10.0, 'currency_code' => 'USD'], $priceColPhpType); @@ -226,6 +230,12 @@ public function testCompositeType(): void ], $priceArrayPhpType ); + $this->assertSame( + [[ + ['value' => 123.45, 'currency_code' => 'USD'], + ]], + $priceArray2PhpType + ); $this->assertSame( [ 'price_from' => ['value' => 1000.0, 'currency_code' => 'USD'], @@ -234,6 +244,19 @@ public function testCompositeType(): void $rangePriceColPhpType ); + $priceCol = $tableSchema->getColumn('price_col'); + $this->assertNull($priceCol->phpTypecast(1), 'For scalar value will return null'); + + $priceCol->columns(null); + $this->assertSame([5, 'USD'], $priceCol->phpTypecast([5, 'USD']), 'Will not typecast for empty columns'); + + $priceArray = $tableSchema->getColumn('price_array'); + $this->assertEquals( + new ArrayExpression([], 'currency_money_composite', 1), + $priceArray->dbTypecast(1), + 'For scalar value will return empty array' + ); + $db->close(); } } diff --git a/tests/Provider/CompositeTypeSchemaProvider.php b/tests/Provider/CompositeTypeSchemaProvider.php index ef5a6c72..bb75e3ff 100644 --- a/tests/Provider/CompositeTypeSchemaProvider.php +++ b/tests/Provider/CompositeTypeSchemaProvider.php @@ -152,6 +152,48 @@ public static function columns(): array ], ], ], + 'price_array2' => [ + 'type' => 'composite', + 'dbType' => 'currency_money_composite', + 'phpType' => 'array', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + 'dimension' => 2, + 'columns' => [ + 'value' => [ + 'type' => 'decimal', + 'dbType' => 'numeric', + 'phpType' => 'double', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => 10, + 'scale' => 2, + 'defaultValue' => null, + ], + 'currency_code' => [ + 'type' => 'char', + 'dbType' => 'bpchar', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => 3, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + ], + ], + ], 'range_price_col' => [ 'type' => 'composite', 'dbType' => 'range_price_composite', diff --git a/tests/Support/Fixture/pgsql.sql b/tests/Support/Fixture/pgsql.sql index ca4fe255..86a518a5 100644 --- a/tests/Support/Fixture/pgsql.sql +++ b/tests/Support/Fixture/pgsql.sql @@ -475,5 +475,6 @@ CREATE TABLE "test_composite_type" "price_col" "currency_money_composite", "price_default" "currency_money_composite" DEFAULT '(5,USD)', "price_array" "currency_money_composite"[] DEFAULT '{null,"(10.55,USD)","(-1,)"}', + "price_array2" "currency_money_composite"[][], "range_price_col" "range_price_composite" DEFAULT '("(0,USD)","(100,USD)")' ); From a455ab83c252d639962b2208be8eba57e8be08d0 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 28 Jul 2023 13:57:29 +0700 Subject: [PATCH 13/34] Remove check of `CompositeExpression` type --- src/Builder/CompositeExpressionBuilder.php | 9 +-------- tests/QueryBuilderTest.php | 17 ----------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php index eacd1cec..46724056 100644 --- a/src/Builder/CompositeExpressionBuilder.php +++ b/src/Builder/CompositeExpressionBuilder.php @@ -28,7 +28,7 @@ public function __construct(private QueryBuilderInterface $queryBuilder) /** * The Method builds the raw SQL from the expression that won't be additionally escaped or quoted. * - * @param ExpressionInterface $expression The expression build. + * @param CompositeExpression $expression The expression build. * @param array $params The binding parameters. * * @throws Exception @@ -40,13 +40,6 @@ public function __construct(private QueryBuilderInterface $queryBuilder) */ public function build(ExpressionInterface $expression, array &$params = []): string { - if (!$expression instanceof CompositeExpression) { - throw new \InvalidArgumentException( - 'TypeError: ' . self::class . '::build(): Argument #1 ($expression) must be instance of ' - . CompositeExpression::class . ', instance of ' . $expression::class . ' given.' - ); - } - /** @psalm-var mixed $value */ $value = $expression->getValue(); diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 40115f36..990dca41 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -673,21 +673,4 @@ public function testUpsertExecute( ): void { parent::testUpsertExecute($table, $insertColumns, $updateColumns); } - - public function testCompositeExpressionBuilder() - { - $db = $this->getConnection(); - $queryBuilder = $db->getQueryBuilder(); - $expressionBuilder = $queryBuilder->getExpressionBuilder(new CompositeExpression([])); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage( - 'TypeError: ' . CompositeExpressionBuilder::class . '::build(): Argument #1 ($expression) must be instance of ' - . CompositeExpression::class . ', instance of ' . ArrayExpression::class . ' given.' - ); - - $expressionBuilder->build(new ArrayExpression()); - - $db->close(); - } } From 65a388bd6e903fb0cd287c9cf39f4f7222de42e2 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 29 Jul 2023 16:32:21 +0700 Subject: [PATCH 14/34] Refactor --- src/Builder/CompositeExpressionBuilder.php | 7 +-- src/ColumnSchema.php | 7 ++- src/Composite/CompositeExpression.php | 28 ++++++---- src/Schema.php | 4 +- tests/ColumnSchemaTest.php | 6 +- tests/CompositeExpressionTest.php | 25 ++------- .../Provider/CompositeTypeSchemaProvider.php | 55 +++++++++++++++++++ tests/Provider/QueryBuilderProvider.php | 19 +------ tests/Support/ColumnSchemaBuilder.php | 33 +++++++++++ 9 files changed, 124 insertions(+), 60 deletions(-) create mode 100644 tests/Support/ColumnSchemaBuilder.php diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php index 46724056..f6f27f16 100644 --- a/src/Builder/CompositeExpressionBuilder.php +++ b/src/Builder/CompositeExpressionBuilder.php @@ -84,18 +84,13 @@ private function buildPlaceholders(CompositeExpression $expression, array &$para } $columns = (array) $expression->getColumns(); - $columnNames = array_keys($columns); /** * @psalm-var int|string $columnName * @psalm-var mixed $item */ foreach ($value as $columnName => $item) { - if (is_int($columnName)) { - $columnName = $columnNames[$columnName] ?? null; - } - - if ($columnName !== null && isset($columns[$columnName])) { + if (isset($columns[$columnName])) { /** @psalm-var mixed $item */ $item = $columns[$columnName]->dbTypecast($item); } diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 8960713e..8c13f6f4 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -210,7 +210,8 @@ private function phpTypecastComposite(mixed $value): array|null } $fields = []; - $columnNames = array_keys((array) $this->columns); + $columns = (array) $this->columns; + $columnNames = array_keys($columns); /** * @psalm-var int|string $columnName @@ -219,9 +220,9 @@ private function phpTypecastComposite(mixed $value): array|null foreach ($value as $columnName => $item) { $columnName = $columnNames[$columnName] ?? $columnName; - if (isset($this->columns[$columnName])) { + if (isset($columns[$columnName])) { /** @psalm-var mixed $item */ - $item = $this->columns[$columnName]->phpTypecast($item); + $item = $columns[$columnName]->phpTypecast($item); } /** @psalm-suppress MixedAssignment */ diff --git a/src/Composite/CompositeExpression.php b/src/Composite/CompositeExpression.php index d13afa56..3f768003 100644 --- a/src/Composite/CompositeExpression.php +++ b/src/Composite/CompositeExpression.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Pgsql\Composite; +use Traversable; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Schema\ColumnSchemaInterface; @@ -62,31 +63,36 @@ public function getValue(): mixed } /** - * Sorted values according to order of the composite type columns and filled with default values skipped items. + * Sorted values according to order of the composite type columns, indexed keys replaced with column names + * and skipped items filled with default values. */ public function getNormalizedValue(): mixed { - if ($this->columns === null || !is_array($this->value)) { + if (empty($this->columns) || !is_iterable($this->value)) { return $this->value; } - $value = []; - $columns = $this->columns; + $normalized = []; + $value = $this->value; + $columnsNames = array_keys($this->columns); - if (is_int(array_key_first($this->value))) { - $columns = array_values($this->columns); + if ($value instanceof Traversable) { + $value = iterator_to_array($value); } - foreach ($columns as $name => $column) { - if (array_key_exists($name, $this->value)) { + foreach ($columnsNames as $i => $columnsName) { + if (array_key_exists($columnsName, $value)) { /** @psalm-suppress MixedAssignment */ - $value[$name] = $this->value[$name]; + $normalized[$columnsName] = $value[$columnsName]; + } elseif (array_key_exists($i, $value)) { + /** @psalm-suppress MixedAssignment */ + $normalized[$columnsName] = $value[$i]; } else { /** @psalm-suppress MixedAssignment */ - $value[$name] = $column->getDefaultValue(); + $normalized[$columnsName] = $this->columns[$columnsName]->getDefaultValue(); } } - return $value; + return $normalized; } } diff --git a/src/Schema.php b/src/Schema.php index ba4d2ca8..03475851 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -833,8 +833,8 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface /** @psalm-var array|null $defaultValue */ $defaultValue = $column->getDefaultValue(); if (is_array($defaultValue)) { - foreach ((array) $column->getColumns() as $compositeName => $compositeColumn) { - $compositeColumn->defaultValue($defaultValue[$compositeName] ?? null); + foreach ((array) $column->getColumns() as $fieldName => $compositeColumn) { + $compositeColumn->defaultValue($defaultValue[$fieldName] ?? null); } } } diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index cb9ec0be..e38a28da 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -245,16 +245,16 @@ public function testCompositeType(): void ); $priceCol = $tableSchema->getColumn('price_col'); - $this->assertNull($priceCol->phpTypecast(1), 'For scalar value will return null'); + $this->assertNull($priceCol->phpTypecast(1), 'For scalar value returns `null`'); $priceCol->columns(null); - $this->assertSame([5, 'USD'], $priceCol->phpTypecast([5, 'USD']), 'Will not typecast for empty columns'); + $this->assertSame([5, 'USD'], $priceCol->phpTypecast([5, 'USD']), 'No type casting for empty columns'); $priceArray = $tableSchema->getColumn('price_array'); $this->assertEquals( new ArrayExpression([], 'currency_money_composite', 1), $priceArray->dbTypecast(1), - 'For scalar value will return empty array' + 'For scalar value returns empty array' ); $db->close(); diff --git a/tests/CompositeExpressionTest.php b/tests/CompositeExpressionTest.php index b1c237c8..4db36f42 100644 --- a/tests/CompositeExpressionTest.php +++ b/tests/CompositeExpressionTest.php @@ -15,25 +15,12 @@ final class CompositeExpressionTest extends TestCase { use TestTrait; - public function testGetNormalizedValue(): void + /** + * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CompositeTypeSchemaProvider::normolizedValues + */ + public function testGetNormalizedValue(mixed $value, mixed $expected, array|null $columns): void { - $db = $this->getConnection(true); - $schema = $db->getSchema(); - $tableSchema = $schema->getTableSchema('test_composite_type'); - - $columns = $tableSchema->getColumn('price_default')->getColumns(); - $this->assertNotNull($columns); - - $compositeExpression = new CompositeExpression(['currency_code' => 'USD', 'value' => 10.0], 'currency_money_composite', $columns); - $this->assertSame(['value' => 10.0, 'currency_code' => 'USD'], $compositeExpression->getNormalizedValue()); - - $compositeExpression = new CompositeExpression(['value' => 10.0], 'currency_money_composite', $columns); - $this->assertSame(['value' => 10.0, 'currency_code' => 'USD'], $compositeExpression->getNormalizedValue()); - - $compositeExpression = new CompositeExpression([10.0], 'currency_money_composite', $columns); - $this->assertSame([10.0, 'USD'], $compositeExpression->getNormalizedValue()); - - $compositeExpression = new CompositeExpression([], 'currency_money_composite', $columns); - $this->assertSame(['value' => 5.0, 'currency_code' => 'USD'], $compositeExpression->getNormalizedValue()); + $compositeExpression = new CompositeExpression($value, 'currency_money_composite', $columns); + $this->assertSame($expected, $compositeExpression->getNormalizedValue()); } } diff --git a/tests/Provider/CompositeTypeSchemaProvider.php b/tests/Provider/CompositeTypeSchemaProvider.php index bb75e3ff..4e58476c 100644 --- a/tests/Provider/CompositeTypeSchemaProvider.php +++ b/tests/Provider/CompositeTypeSchemaProvider.php @@ -4,6 +4,10 @@ namespace Yiisoft\Db\Pgsql\Tests\Provider; +use ArrayIterator; +use Yiisoft\Db\Pgsql\Tests\Support\ColumnSchemaBuilder; +use Yiisoft\Db\Tests\Support\TraversableObject; + final class CompositeTypeSchemaProvider extends \Yiisoft\Db\Tests\Provider\SchemaProvider { public static function columns(): array @@ -300,4 +304,55 @@ public static function columns(): array ], ]; } + + public static function normolizedValues() + { + $price5UsdColumns = [ + 'value' => ColumnSchemaBuilder::numeric(name: 'value', precision: 10, scale: 2, defaultValue: 5.0), + 'currency_code' => ColumnSchemaBuilder::char(name: 'currency_code', size: 3, defaultValue: 'USD'), + ]; + + return [ + 'Sort according to `$columns` order' => [ + ['currency_code' => 'USD', 'value' => 10.0], + ['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 iterable object' => [ + new TraversableObject([10.0]), + ['value' => 10.0, 'currency_code' => 'USD'], + $price5UsdColumns, + ], + 'Fill default values for iterable object' => [ + new ArrayIterator(['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 scalar values' => [ + 1, + 1, + $price5UsdColumns, + ], + 'Do not normalize with empty columns' => [ + [10.0], + [10.0], + null, + ], + ]; + } } diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 51c76a04..e189a917 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -7,8 +7,8 @@ use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\JsonExpression; -use Yiisoft\Db\Pgsql\ColumnSchema; use Yiisoft\Db\Pgsql\Composite\CompositeExpression; +use Yiisoft\Db\Pgsql\Tests\Support\ColumnSchemaBuilder; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Schema\SchemaInterface; @@ -26,22 +26,9 @@ public static function buildCondition(): array { $buildCondition = parent::buildCondition(); - $valueColumn = new ColumnSchema('value'); - $valueColumn->type('decimal'); - $valueColumn->dbType('numeric'); - $valueColumn->phpType('double'); - $valueColumn->precision(10); - $valueColumn->scale(2); - - $currencyCodeColumn = new ColumnSchema('currency_code'); - $currencyCodeColumn->type('char'); - $currencyCodeColumn->dbType('bpchar'); - $currencyCodeColumn->phpType('string'); - $currencyCodeColumn->size(3); - $priceColumns = [ - 'value' => $valueColumn, - 'currency_code' => $currencyCodeColumn, + 'value' => ColumnSchemaBuilder::numeric(name: 'value', precision: 10, scale: 2), + 'currency_code' => ColumnSchemaBuilder::char(name: 'currency_code', size: 3), ]; return array_merge( diff --git a/tests/Support/ColumnSchemaBuilder.php b/tests/Support/ColumnSchemaBuilder.php new file mode 100644 index 00000000..2ff104a1 --- /dev/null +++ b/tests/Support/ColumnSchemaBuilder.php @@ -0,0 +1,33 @@ +type('decimal'); + $column->dbType('numeric'); + $column->phpType('double'); + $column->precision($precision); + $column->scale($scale); + $column->defaultValue($defaultValue); + + return $column; + } + + public static function char(string $name, int|null $size, mixed $defaultValue = null): ColumnSchema + { + $column = new ColumnSchema($name); + $column->type('char'); + $column->dbType('bpchar'); + $column->phpType('string'); + $column->size($size); + $column->defaultValue($defaultValue); + + return $column; + } +} From 32645ec28762c833a4889710fe31db1c5a1c92c7 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 29 Jul 2023 17:03:16 +0700 Subject: [PATCH 15/34] Apply fixes from StyleCI --- tests/QueryBuilderTest.php | 3 --- tests/Support/ColumnSchemaBuilder.php | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 990dca41..39e6ba95 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -11,11 +11,8 @@ use Yiisoft\Db\Exception\IntegrityException; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; -use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\ExpressionInterface; -use Yiisoft\Db\Pgsql\Builder\CompositeExpressionBuilder; use Yiisoft\Db\Pgsql\Column; -use Yiisoft\Db\Pgsql\Composite\CompositeExpression; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\Schema\SchemaInterface; diff --git a/tests/Support/ColumnSchemaBuilder.php b/tests/Support/ColumnSchemaBuilder.php index 2ff104a1..dbbb8de2 100644 --- a/tests/Support/ColumnSchemaBuilder.php +++ b/tests/Support/ColumnSchemaBuilder.php @@ -1,5 +1,7 @@ Date: Sat, 29 Jul 2023 18:13:36 +0700 Subject: [PATCH 16/34] Update tests and comments --- src/ColumnSchema.php | 23 +++++++++++++++---- src/Composite/CompositeExpression.php | 4 ++-- tests/CompositeExpressionTest.php | 2 +- tests/CompositeParserTest.php | 4 ++-- ...Provider.php => CompositeTypeProvider.php} | 2 +- tests/SchemaTest.php | 4 ++-- 6 files changed, 27 insertions(+), 12 deletions(-) rename tests/Provider/{CompositeTypeSchemaProvider.php => CompositeTypeProvider.php} (99%) diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 8c13f6f4..f0dc93ca 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -95,7 +95,12 @@ public function dbTypecast(mixed $value): mixed } /** - * @param int $dimension Should be more than 0 + * Recursively converts array values for use in a db query. + * + * @param mixed $value The array values. + * @param int $dimension The array dimension. Should be more than 0. + * + * @return array Converted values. */ private function dbTypecastArray(mixed $value, int $dimension): array { @@ -118,6 +123,9 @@ private function dbTypecastArray(mixed $value, int $dimension): array return $items; } + /** + * Converts the input value for use in a db query. + */ private function dbTypecastValue(mixed $value): mixed { if ($value === null || $value instanceof ExpressionInterface) { @@ -199,6 +207,9 @@ protected function phpTypecastValue(mixed $value): mixed }; } + /** + * Converts the input value according to the composite type after retrieval from the database. + */ private function phpTypecastComposite(mixed $value): array|null { if (is_string($value)) { @@ -277,7 +288,9 @@ public function sequenceName(string|null $sequenceName): void } /** - * @param ColumnSchemaInterface[]|null $columns The columns metadata of the composite type. + * Set columns of the composite type. + * + * @param ColumnSchemaInterface[]|null $columns The metadata of the composite type columns. * @psalm-param array|null $columns */ public function columns(array|null $columns): void @@ -286,7 +299,9 @@ public function columns(array|null $columns): void } /** - * @return ColumnSchemaInterface[]|null Columns metadata of the composite type. + * Get the metadata of the composite type columns. + * + * @return ColumnSchemaInterface[]|null */ public function getColumns(): array|null { @@ -294,7 +309,7 @@ public function getColumns(): array|null } /** - * Creates instance of CompositeParser. + * Creates instance of `CompositeParser`. */ private function getCompositeParser(): CompositeParser { diff --git a/src/Composite/CompositeExpression.php b/src/Composite/CompositeExpression.php index 3f768003..f08b8e58 100644 --- a/src/Composite/CompositeExpression.php +++ b/src/Composite/CompositeExpression.php @@ -63,8 +63,8 @@ public function getValue(): mixed } /** - * Sorted values according to order of the composite type columns, indexed keys replaced with column names - * and skipped items filled with default values. + * Sorted values according to order of the composite type columns, indexed keys replaced with column names, + * skipped items filled with default values, extra items removed. */ public function getNormalizedValue(): mixed { diff --git a/tests/CompositeExpressionTest.php b/tests/CompositeExpressionTest.php index 4db36f42..6a856aec 100644 --- a/tests/CompositeExpressionTest.php +++ b/tests/CompositeExpressionTest.php @@ -16,7 +16,7 @@ final class CompositeExpressionTest extends TestCase use TestTrait; /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CompositeTypeSchemaProvider::normolizedValues + * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CompositeTypeProvider::normolizedValues */ public function testGetNormalizedValue(mixed $value, mixed $expected, array|null $columns): void { diff --git a/tests/CompositeParserTest.php b/tests/CompositeParserTest.php index 87fa2c8f..2dfb494a 100644 --- a/tests/CompositeParserTest.php +++ b/tests/CompositeParserTest.php @@ -18,7 +18,7 @@ public function testParser(): void $this->assertSame([null], $compositeParse->parse('()')); $this->assertSame([0 => null, 1 => null], $compositeParse->parse('(,)')); - $this->assertSame([0 => '1', 1 => '2', 2 => '3'], $compositeParse->parse('(1,2,3)')); + $this->assertSame([0 => '10.0', 1 => 'USD'], $compositeParse->parse('(10.0,USD)')); $this->assertSame([0 => '1', 1 => '-2', 2 => null, 3 => '42'], $compositeParse->parse('(1,-2,,42)')); $this->assertSame([0 => ''], $compositeParse->parse('("")')); $this->assertSame( @@ -27,6 +27,6 @@ public function testParser(): void ); // Default values can have any expressions - $this->assertSame(null, $compositeParse->parse("'(1,2,3)::composite_type'")); + $this->assertSame(null, $compositeParse->parse("'(10.0,USD)::composite_type'")); } } diff --git a/tests/Provider/CompositeTypeSchemaProvider.php b/tests/Provider/CompositeTypeProvider.php similarity index 99% rename from tests/Provider/CompositeTypeSchemaProvider.php rename to tests/Provider/CompositeTypeProvider.php index 4e58476c..c189cba2 100644 --- a/tests/Provider/CompositeTypeSchemaProvider.php +++ b/tests/Provider/CompositeTypeProvider.php @@ -8,7 +8,7 @@ use Yiisoft\Db\Pgsql\Tests\Support\ColumnSchemaBuilder; use Yiisoft\Db\Tests\Support\TraversableObject; -final class CompositeTypeSchemaProvider extends \Yiisoft\Db\Tests\Provider\SchemaProvider +final class CompositeTypeProvider extends \Yiisoft\Db\Tests\Provider\SchemaProvider { public static function columns(): array { diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index eb6b4ad7..f10fbc9c 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -559,7 +559,7 @@ public function testDomainType(): void } /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CompositeTypeSchemaProvider::columns + * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CompositeTypeProvider::columns * * @throws Exception */ @@ -579,7 +579,7 @@ private function testCompositeTypeColumnSchemaRecursive(array $columns, string $ if ($column->getType() === 'composite') { $this->assertTrue( isset($columns[$name]['columns']), - "Composite type's columns of column `$name` do not exist. type is `{$column->getType()}`, dbType is `{$column->getDbType()}`." + "Columns of composite type `$name` do not exist, dbType is `{$column->getDbType()}`." ); $this->testCompositeTypeColumnSchemaRecursive($columns[$name]['columns'], $column->getDbType()); } From ae6f41c5be78e68c01448556ff2fb200eb31cfc4 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 29 Jul 2023 18:39:45 +0700 Subject: [PATCH 17/34] Update comments --- src/ColumnSchema.php | 2 +- src/Composite/CompositeExpression.php | 18 +++++++++++------- src/Composite/CompositeParser.php | 4 ++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index f0dc93ca..4cb80eba 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -97,7 +97,7 @@ public function dbTypecast(mixed $value): mixed /** * Recursively converts array values for use in a db query. * - * @param mixed $value The array values. + * @param mixed $value The array or iterable object. * @param int $dimension The array dimension. Should be more than 0. * * @return array Converted values. diff --git a/src/Composite/CompositeExpression.php b/src/Composite/CompositeExpression.php index f08b8e58..7908e43f 100644 --- a/src/Composite/CompositeExpression.php +++ b/src/Composite/CompositeExpression.php @@ -9,7 +9,7 @@ use Yiisoft\Db\Schema\ColumnSchemaInterface; /** - * Represents a composite SQL expression. + * Represents a composite type SQL expression. * * For example: * @@ -35,10 +35,10 @@ public function __construct( /** * The composite type name. * - * Defaults to `null` which means the type isn't explicitly specified. + * Defaults to `null` which means the type is not explicitly specified. * - * Note that in the case where a type isn't specified explicitly and DBMS can't guess it from the context, SQL error - * will be raised. + * 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 { @@ -46,6 +46,8 @@ public function getType(): string|null } /** + * The composite type columns that are used for value normalization and type casting. + * * @return ColumnSchemaInterface[]|null */ public function getColumns(): array|null @@ -54,7 +56,7 @@ public function getColumns(): array|null } /** - * The composite type's content. It can be represented as an associative array of the composite type's column names + * The content of the composite type. It can be represented as an associative array of composite type column names * and values. */ public function getValue(): mixed @@ -63,8 +65,10 @@ public function getValue(): mixed } /** - * Sorted values according to order of the composite type columns, indexed keys replaced with column names, - * skipped items filled with default values, extra items removed. + * Sorted values according to the order of composite type columns, + * indexed keys are replaced with column names, + * missing elements are filled in with default values, + * redundant elements are removed. */ public function getNormalizedValue(): mixed { diff --git a/src/Composite/CompositeParser.php b/src/Composite/CompositeParser.php index 84cb41fe..ee49e683 100644 --- a/src/Composite/CompositeParser.php +++ b/src/Composite/CompositeParser.php @@ -10,7 +10,7 @@ class CompositeParser { /** - * Convert composite type from PostgreSQL to PHP array + * Converts composite type value from PostgreSQL to PHP array * * @param string $value String to convert. */ @@ -24,7 +24,7 @@ public function parse(string $value): array|null } /** - * Parse PostgreSQL composite type encoded in string. + * Parses PostgreSQL composite type value encoded in string. * * @param string $value String to parse. */ From d5f295efa537af28354f4b2911c389eacf41f9f8 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 30 Jul 2023 13:56:51 +0700 Subject: [PATCH 18/34] Fix typo in `$compositeParser` --- tests/CompositeParserTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/CompositeParserTest.php b/tests/CompositeParserTest.php index 2dfb494a..6e1bfb4b 100644 --- a/tests/CompositeParserTest.php +++ b/tests/CompositeParserTest.php @@ -14,19 +14,19 @@ final class CompositeParserTest extends TestCase { public function testParser(): void { - $compositeParse = new CompositeParser(); + $compositeParser = new CompositeParser(); - $this->assertSame([null], $compositeParse->parse('()')); - $this->assertSame([0 => null, 1 => null], $compositeParse->parse('(,)')); - $this->assertSame([0 => '10.0', 1 => 'USD'], $compositeParse->parse('(10.0,USD)')); - $this->assertSame([0 => '1', 1 => '-2', 2 => null, 3 => '42'], $compositeParse->parse('(1,-2,,42)')); - $this->assertSame([0 => ''], $compositeParse->parse('("")')); + $this->assertSame([null], $compositeParser->parse('()')); + $this->assertSame([0 => null, 1 => null], $compositeParser->parse('(,)')); + $this->assertSame([0 => '10.0', 1 => 'USD'], $compositeParser->parse('(10.0,USD)')); + $this->assertSame([0 => '1', 1 => '-2', 2 => null, 3 => '42'], $compositeParser->parse('(1,-2,,42)')); + $this->assertSame([0 => ''], $compositeParser->parse('("")')); $this->assertSame( [0 => '[",","null",true,"false","f"]'], - $compositeParse->parse('("[\",\",\"null\",true,\"false\",\"f\"]")') + $compositeParser->parse('("[\",\",\"null\",true,\"false\",\"f\"]")') ); // Default values can have any expressions - $this->assertSame(null, $compositeParse->parse("'(10.0,USD)::composite_type'")); + $this->assertSame(null, $compositeParser->parse("'(10.0,USD)::composite_type'")); } } From 4119262bdba5d28e06c575f83187ae4c1bede26b Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Sun, 30 Jul 2023 02:42:05 +0700 Subject: [PATCH 19/34] Refactor ColumnSchema.php (#302) --- CHANGELOG.md | 2 ++ src/ColumnSchema.php | 21 ++++++++++----------- tests/ColumnSchemaTest.php | 9 +++++++++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e85fe6fd..094958f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Enh #301: Refactor `JsonExpressionBuilder` (@Tigrov) - Enh #300: Refactor `ArrayExpressionBuilder` (@Tigrov) +- Enh #302: Refactor `ColumnSchema` (@Tigrov) +- Bug #302: Fix incorrect convert string value for BIT type (@Tigrov) ## 1.1.0 July 24, 2023 diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 4cb80eba..6f0b0636 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -74,8 +74,7 @@ final class ColumnSchema extends AbstractColumnSchema * * @param mixed $value input value * - * @return mixed Converted value. This may also be an array containing the value as the first element and the PDO - * type as the second element. + * @return mixed Converted value. */ public function dbTypecast(mixed $value): mixed { @@ -141,7 +140,7 @@ private function dbTypecastValue(mixed $value): mixed Schema::TYPE_BIT => is_int($value) ? str_pad(decbin($value), (int) $this->getSize(), '0', STR_PAD_LEFT) - : $this->typecast($value), + : (string) $value, Schema::TYPE_COMPOSITE => new CompositeExpression($value, $this->getDbType(), $this->columns), @@ -167,15 +166,15 @@ public function phpTypecast(mixed $value): mixed $value = $this->getArrayParser()->parse($value); } - if (is_array($value)) { - array_walk_recursive($value, function (string|null &$val) { - /** @psalm-var mixed $val */ - $val = $this->phpTypecastValue($val); - }); - } else { + if (!is_array($value)) { return null; } + array_walk_recursive($value, function (mixed &$val) { + /** @psalm-var mixed $val */ + $val = $this->phpTypecastValue($val); + }); + return $value; } @@ -187,7 +186,7 @@ public function phpTypecast(mixed $value): mixed * * @throws JsonException */ - protected function phpTypecastValue(mixed $value): mixed + private function phpTypecastValue(mixed $value): mixed { if ($value === null) { return null; @@ -246,7 +245,7 @@ private function phpTypecastComposite(mixed $value): array|null /** * Creates instance of ArrayParser. */ - protected function getArrayParser(): ArrayParser + private function getArrayParser(): ArrayParser { return new ArrayParser(); } diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index e38a28da..d92aa391 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -189,6 +189,15 @@ 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 testCompositeType(): void { $db = $this->getConnection(true); From 7ef91fda22dc900e991b076172ff90e415ca125f Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 30 Jul 2023 14:37:43 +0700 Subject: [PATCH 20/34] Revert "Refactor ColumnSchema.php (#302)" This reverts commit 4119262bdba5d28e06c575f83187ae4c1bede26b. --- CHANGELOG.md | 2 -- src/ColumnSchema.php | 21 +++++++++++---------- tests/ColumnSchemaTest.php | 9 --------- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 094958f1..e85fe6fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,6 @@ - Enh #301: Refactor `JsonExpressionBuilder` (@Tigrov) - Enh #300: Refactor `ArrayExpressionBuilder` (@Tigrov) -- Enh #302: Refactor `ColumnSchema` (@Tigrov) -- Bug #302: Fix incorrect convert string value for BIT type (@Tigrov) ## 1.1.0 July 24, 2023 diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 6f0b0636..4cb80eba 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -74,7 +74,8 @@ final class ColumnSchema extends AbstractColumnSchema * * @param mixed $value input value * - * @return mixed Converted value. + * @return mixed Converted value. This may also be an array containing the value as the first element and the PDO + * type as the second element. */ public function dbTypecast(mixed $value): mixed { @@ -140,7 +141,7 @@ private function dbTypecastValue(mixed $value): mixed Schema::TYPE_BIT => is_int($value) ? str_pad(decbin($value), (int) $this->getSize(), '0', STR_PAD_LEFT) - : (string) $value, + : $this->typecast($value), Schema::TYPE_COMPOSITE => new CompositeExpression($value, $this->getDbType(), $this->columns), @@ -166,15 +167,15 @@ public function phpTypecast(mixed $value): mixed $value = $this->getArrayParser()->parse($value); } - if (!is_array($value)) { + if (is_array($value)) { + array_walk_recursive($value, function (string|null &$val) { + /** @psalm-var mixed $val */ + $val = $this->phpTypecastValue($val); + }); + } else { return null; } - array_walk_recursive($value, function (mixed &$val) { - /** @psalm-var mixed $val */ - $val = $this->phpTypecastValue($val); - }); - return $value; } @@ -186,7 +187,7 @@ public function phpTypecast(mixed $value): mixed * * @throws JsonException */ - private function phpTypecastValue(mixed $value): mixed + protected function phpTypecastValue(mixed $value): mixed { if ($value === null) { return null; @@ -245,7 +246,7 @@ private function phpTypecastComposite(mixed $value): array|null /** * Creates instance of ArrayParser. */ - private function getArrayParser(): ArrayParser + protected function getArrayParser(): ArrayParser { return new ArrayParser(); } diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index d92aa391..e38a28da 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -189,15 +189,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 testCompositeType(): void { $db = $this->getConnection(true); From ec137b7172cf4dfb8e4e751d49ae46bb96f7c1df Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Tue, 1 Aug 2023 16:28:09 +0700 Subject: [PATCH 21/34] Improve initialization of ColumnSchema type Co-authored-by: Sergei Predvoditelev --- src/Schema.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Schema.php b/src/Schema.php index 03475851..da8604cb 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -815,8 +815,6 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface $column->sequenceName($this->resolveTableName($info['sequence_name'])->getFullName()); } - $column->type($this->typeMap[(string) $column->getDbType()] ?? self::TYPE_STRING); - if ($info['type_type'] === 'c') { $column->type(self::TYPE_COMPOSITE); $composite = $this->resolveTableName((string) $column->getDbType()); @@ -824,6 +822,8 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface if ($this->findColumns($composite)) { $column->columns($composite->getColumns()); } + } else { + $column->type($this->typeMap[(string) $column->getDbType()] ?? self::TYPE_STRING); } $column->phpType($this->getColumnPhpType($column)); From 07479381385de97bc87dd7a48d341028a9c753fd Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Tue, 1 Aug 2023 16:30:24 +0700 Subject: [PATCH 22/34] Mark `CompositeParser` class as `final` Co-authored-by: Sergei Predvoditelev --- src/Composite/CompositeParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composite/CompositeParser.php b/src/Composite/CompositeParser.php index ee49e683..5e2064df 100644 --- a/src/Composite/CompositeParser.php +++ b/src/Composite/CompositeParser.php @@ -7,7 +7,7 @@ /** * Composite type representation to PHP array parser for PostgreSQL Server. */ -class CompositeParser +final class CompositeParser { /** * Converts composite type value from PostgreSQL to PHP array From 3a3f47be22d4697948dffba5974357be5e433fe2 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 6 Sep 2023 12:08:00 +0700 Subject: [PATCH 23/34] Update according to #297 --- src/ColumnSchema.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 6f0b0636..6c496463 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -212,7 +212,7 @@ private function phpTypecastValue(mixed $value): mixed private function phpTypecastComposite(mixed $value): array|null { if (is_string($value)) { - $value = $this->getCompositeParser()->parse($value); + $value = (new CompositeParser())->parse($value); } if (!is_iterable($value)) { @@ -306,12 +306,4 @@ public function getColumns(): array|null { return $this->columns; } - - /** - * Creates instance of `CompositeParser`. - */ - private function getCompositeParser(): CompositeParser - { - return new CompositeParser(); - } } From 0f872e334c40c106ba4cafca4557f7b1fb12edc8 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 6 Sep 2023 12:57:38 +0700 Subject: [PATCH 24/34] Improve `dbTypecastArray()` --- src/ColumnSchema.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 6c496463..2995cd05 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -99,21 +99,24 @@ public function dbTypecast(mixed $value): mixed * @param mixed $value The array or iterable object. * @param int $dimension The array dimension. Should be more than 0. * - * @return array Converted values. + * @return array|null Converted values. */ - private function dbTypecastArray(mixed $value, int $dimension): array + private function dbTypecastArray(mixed $value, int $dimension): array|null { - $items = []; - if (!is_iterable($value)) { - return $items; + return []; } - /** @psalm-var mixed $val */ - foreach ($value as $val) { - if ($dimension > 1) { + $items = []; + + if ($dimension > 1) { + /** @psalm-var mixed $val */ + foreach ($value as $val) { $items[] = $this->dbTypecastArray($val, $dimension - 1); - } else { + } + } else { + /** @psalm-var mixed $val */ + foreach ($value as $val) { /** @psalm-suppress MixedAssignment */ $items[] = $this->dbTypecastValue($val); } From 1a06c70f0782fcc67d136902a40d170c6ae1ec37 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 6 Sep 2023 13:06:05 +0700 Subject: [PATCH 25/34] Add test for excessive elements --- src/Composite/CompositeExpression.php | 2 +- tests/Provider/CompositeTypeProvider.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Composite/CompositeExpression.php b/src/Composite/CompositeExpression.php index 7908e43f..1455be41 100644 --- a/src/Composite/CompositeExpression.php +++ b/src/Composite/CompositeExpression.php @@ -68,7 +68,7 @@ public function getValue(): mixed * Sorted values according to the order of composite type columns, * indexed keys are replaced with column names, * missing elements are filled in with default values, - * redundant elements are removed. + * excessive elements are removed. */ public function getNormalizedValue(): mixed { diff --git a/tests/Provider/CompositeTypeProvider.php b/tests/Provider/CompositeTypeProvider.php index c189cba2..7ad63577 100644 --- a/tests/Provider/CompositeTypeProvider.php +++ b/tests/Provider/CompositeTypeProvider.php @@ -318,6 +318,11 @@ public static function normolizedValues() ['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'], From feb58a127162ab2d24fe5bcfb22acbf048780a24 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 6 Sep 2023 13:08:44 +0700 Subject: [PATCH 26/34] Add line to CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index adf917ca..cfd6419b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Enh #301: Refactor `JsonExpressionBuilder` (@Tigrov) - Enh #300: Refactor `ArrayExpressionBuilder` (@Tigrov) - Enh #302: Refactor `ColumnSchema` (@Tigrov) +- Enh #303: Support Composite types (@Tigrov) - Bug #302: Fix incorrect convert string value for BIT type (@Tigrov) - Bug #309: Fix retrieving sequence name from default value (@Tigrov) From edb4f17b9b630595c9f4085d249bc08b3b56851d Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 6 Sep 2023 13:16:49 +0700 Subject: [PATCH 27/34] Rename $fieldName to $compositeColumnName --- src/Schema.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Schema.php b/src/Schema.php index dd6c5234..671d4a61 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -829,8 +829,8 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface /** @psalm-var array|null $defaultValue */ $defaultValue = $column->getDefaultValue(); if (is_array($defaultValue)) { - foreach ((array) $column->getColumns() as $fieldName => $compositeColumn) { - $compositeColumn->defaultValue($defaultValue[$fieldName] ?? null); + foreach ((array) $column->getColumns() as $compositeColumnName => $compositeColumn) { + $compositeColumn->defaultValue($defaultValue[$compositeColumnName] ?? null); } } } From 90e9dbf280b03c58b23b9c5e63eb8b728eb51c6c Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 6 Sep 2023 13:27:36 +0700 Subject: [PATCH 28/34] Double array of null values --- src/ColumnSchema.php | 4 ++++ tests/ColumnSchemaTest.php | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 2995cd05..5ca62518 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -103,6 +103,10 @@ public function dbTypecast(mixed $value): mixed */ private function dbTypecastArray(mixed $value, int $dimension): array|null { + if ($value === null) { + return null; + } + if (!is_iterable($value)) { return []; } diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index d92aa391..02d7712e 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -266,6 +266,13 @@ public function testCompositeType(): void 'For scalar value returns empty array' ); + $priceArray2 = $tableSchema->getColumn('price_array2'); + $this->assertEquals( + [null, null], + $priceArray2->phpTypecast([null, null]), + 'Double array of null values' + ); + $db->close(); } } From 12cd867a589df41274bf5855e20e7e09511c3da9 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 6 Sep 2023 13:36:02 +0700 Subject: [PATCH 29/34] Update test --- tests/ColumnSchemaTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 02d7712e..85e79467 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -268,8 +268,8 @@ public function testCompositeType(): void $priceArray2 = $tableSchema->getColumn('price_array2'); $this->assertEquals( - [null, null], - $priceArray2->phpTypecast([null, null]), + new ArrayExpression([null, null], 'currency_money_composite', 2), + $priceArray2->dbTypecast([null, null]), 'Double array of null values' ); From c06f2b17e0aa3198742b9914d91f6985b997885a Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Tue, 14 Nov 2023 22:31:57 +0700 Subject: [PATCH 30/34] Apply suggestions from code review Co-authored-by: Sergei Predvoditelev --- CHANGELOG.md | 2 +- src/Builder/CompositeExpressionBuilder.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5429771e..042ac731 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - Enh #300: Refactor `ArrayExpressionBuilder` (@Tigrov) - Enh #301: Refactor `JsonExpressionBuilder` (@Tigrov) - Enh #302: Refactor `ColumnSchema` (@Tigrov) -- Enh #303: Support Composite types (@Tigrov) +- Enh #303: Support composite types (@Tigrov) - Enh #321: Move methods from `Command` to `AbstractPdoCommand` class (@Tigrov) - Bug #302: Fix incorrect convert string value for BIT type (@Tigrov) - Bug #309: Fix retrieving sequence name from default value (@Tigrov) diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php index f6f27f16..9f29919a 100644 --- a/src/Builder/CompositeExpressionBuilder.php +++ b/src/Builder/CompositeExpressionBuilder.php @@ -26,7 +26,7 @@ public function __construct(private QueryBuilderInterface $queryBuilder) } /** - * The Method builds the raw SQL from the expression that won't be additionally escaped or quoted. + * The method builds the raw SQL from the expression that won't be additionally escaped or quoted. * * @param CompositeExpression $expression The expression build. * @param array $params The binding parameters. From da586d2867f1c8e3edeeac9b3087a9d926e89b03 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Mon, 8 Jan 2024 10:25:33 +0700 Subject: [PATCH 31/34] Psalm suppress `MixedAssignment` --- psalm.xml | 3 +++ src/ColumnSchema.php | 11 +---------- src/Composite/CompositeExpression.php | 15 +++++---------- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/psalm.xml b/psalm.xml index 23bfcce1..54a52e17 100644 --- a/psalm.xml +++ b/psalm.xml @@ -14,4 +14,7 @@ + + + diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 5ca62518..dbdfaeb3 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -114,14 +114,11 @@ private function dbTypecastArray(mixed $value, int $dimension): array|null $items = []; if ($dimension > 1) { - /** @psalm-var mixed $val */ foreach ($value as $val) { $items[] = $this->dbTypecastArray($val, $dimension - 1); } } else { - /** @psalm-var mixed $val */ foreach ($value as $val) { - /** @psalm-suppress MixedAssignment */ $items[] = $this->dbTypecastValue($val); } } @@ -178,7 +175,6 @@ public function phpTypecast(mixed $value): mixed } array_walk_recursive($value, function (mixed &$val) { - /** @psalm-var mixed $val */ $val = $this->phpTypecastValue($val); }); @@ -230,19 +226,14 @@ private function phpTypecastComposite(mixed $value): array|null $columns = (array) $this->columns; $columnNames = array_keys($columns); - /** - * @psalm-var int|string $columnName - * @psalm-var mixed $item - */ + /** @psalm-var int|string $columnName */ foreach ($value as $columnName => $item) { $columnName = $columnNames[$columnName] ?? $columnName; if (isset($columns[$columnName])) { - /** @psalm-var mixed $item */ $item = $columns[$columnName]->phpTypecast($item); } - /** @psalm-suppress MixedAssignment */ $fields[$columnName] = $item; } diff --git a/src/Composite/CompositeExpression.php b/src/Composite/CompositeExpression.php index 1455be41..811c2975 100644 --- a/src/Composite/CompositeExpression.php +++ b/src/Composite/CompositeExpression.php @@ -85,16 +85,11 @@ public function getNormalizedValue(): mixed } foreach ($columnsNames as $i => $columnsName) { - if (array_key_exists($columnsName, $value)) { - /** @psalm-suppress MixedAssignment */ - $normalized[$columnsName] = $value[$columnsName]; - } elseif (array_key_exists($i, $value)) { - /** @psalm-suppress MixedAssignment */ - $normalized[$columnsName] = $value[$i]; - } else { - /** @psalm-suppress MixedAssignment */ - $normalized[$columnsName] = $this->columns[$columnsName]->getDefaultValue(); - } + $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; From 77fa5abf34fbfb8d7bdc1e7437db9bd9cef00166 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Mon, 8 Jan 2024 10:26:36 +0700 Subject: [PATCH 32/34] Remove `null` type from `ColumnSchema::$columns` --- src/Builder/CompositeExpressionBuilder.php | 2 +- src/ColumnSchema.php | 18 +++++++++--------- src/Composite/CompositeExpression.php | 10 +++++----- src/Schema.php | 2 +- tests/ColumnSchemaTest.php | 2 +- tests/CompositeExpressionTest.php | 6 ++---- tests/Provider/CompositeTypeProvider.php | 2 +- 7 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php index 9f29919a..7b26cdd1 100644 --- a/src/Builder/CompositeExpressionBuilder.php +++ b/src/Builder/CompositeExpressionBuilder.php @@ -83,7 +83,7 @@ private function buildPlaceholders(CompositeExpression $expression, array &$para return $placeholders; } - $columns = (array) $expression->getColumns(); + $columns = $expression->getColumns(); /** * @psalm-var int|string $columnName diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index dbdfaeb3..44e48e4b 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -62,10 +62,10 @@ final class ColumnSchema extends AbstractColumnSchema private string|null $sequenceName = null; /** - * @var ColumnSchemaInterface[]|null Columns metadata of the composite type. - * @psalm-var array|null + * @var ColumnSchemaInterface[] Columns metadata of the composite type. + * @psalm-var array */ - private array|null $columns = null; + private array $columns = []; /** * Converts the input value according to {@see type} and {@see dbType} for use in a db query. @@ -223,7 +223,7 @@ private function phpTypecastComposite(mixed $value): array|null } $fields = []; - $columns = (array) $this->columns; + $columns = $this->columns; $columnNames = array_keys($columns); /** @psalm-var int|string $columnName */ @@ -287,10 +287,10 @@ public function sequenceName(string|null $sequenceName): void /** * Set columns of the composite type. * - * @param ColumnSchemaInterface[]|null $columns The metadata of the composite type columns. - * @psalm-param array|null $columns + * @param ColumnSchemaInterface[] $columns The metadata of the composite type columns. + * @psalm-param array $columns */ - public function columns(array|null $columns): void + public function columns(array $columns): void { $this->columns = $columns; } @@ -298,9 +298,9 @@ public function columns(array|null $columns): void /** * Get the metadata of the composite type columns. * - * @return ColumnSchemaInterface[]|null + * @return ColumnSchemaInterface[] */ - public function getColumns(): array|null + public function getColumns(): array { return $this->columns; } diff --git a/src/Composite/CompositeExpression.php b/src/Composite/CompositeExpression.php index 811c2975..56ba9e0b 100644 --- a/src/Composite/CompositeExpression.php +++ b/src/Composite/CompositeExpression.php @@ -22,13 +22,13 @@ class CompositeExpression implements ExpressionInterface { /** - * @param ColumnSchemaInterface[]|null $columns - * @psalm-param array|null $columns + * @param ColumnSchemaInterface[] $columns + * @psalm-param array $columns */ public function __construct( private mixed $value, private string|null $type = null, - private array|null $columns = null, + private array $columns = [], ) { } @@ -48,9 +48,9 @@ public function getType(): string|null /** * The composite type columns that are used for value normalization and type casting. * - * @return ColumnSchemaInterface[]|null + * @return ColumnSchemaInterface[] */ - public function getColumns(): array|null + public function getColumns(): array { return $this->columns; } diff --git a/src/Schema.php b/src/Schema.php index 61733b51..04ac8efa 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -829,7 +829,7 @@ protected function loadColumnSchema(array $info): ColumnSchemaInterface /** @psalm-var array|null $defaultValue */ $defaultValue = $column->getDefaultValue(); if (is_array($defaultValue)) { - foreach ((array) $column->getColumns() as $compositeColumnName => $compositeColumn) { + foreach ($column->getColumns() as $compositeColumnName => $compositeColumn) { $compositeColumn->defaultValue($defaultValue[$compositeColumnName] ?? null); } } diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 85e79467..451cf0d2 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -256,7 +256,7 @@ public function testCompositeType(): void $priceCol = $tableSchema->getColumn('price_col'); $this->assertNull($priceCol->phpTypecast(1), 'For scalar value returns `null`'); - $priceCol->columns(null); + $priceCol->columns([]); $this->assertSame([5, 'USD'], $priceCol->phpTypecast([5, 'USD']), 'No type casting for empty columns'); $priceArray = $tableSchema->getColumn('price_array'); diff --git a/tests/CompositeExpressionTest.php b/tests/CompositeExpressionTest.php index 6a856aec..846c4253 100644 --- a/tests/CompositeExpressionTest.php +++ b/tests/CompositeExpressionTest.php @@ -15,10 +15,8 @@ final class CompositeExpressionTest extends TestCase { use TestTrait; - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CompositeTypeProvider::normolizedValues - */ - public function testGetNormalizedValue(mixed $value, mixed $expected, array|null $columns): void + /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CompositeTypeProvider::normolizedValues */ + public function testGetNormalizedValue(mixed $value, mixed $expected, array $columns): void { $compositeExpression = new CompositeExpression($value, 'currency_money_composite', $columns); $this->assertSame($expected, $compositeExpression->getNormalizedValue()); diff --git a/tests/Provider/CompositeTypeProvider.php b/tests/Provider/CompositeTypeProvider.php index 7ad63577..a19ff986 100644 --- a/tests/Provider/CompositeTypeProvider.php +++ b/tests/Provider/CompositeTypeProvider.php @@ -356,7 +356,7 @@ public static function normolizedValues() 'Do not normalize with empty columns' => [ [10.0], [10.0], - null, + [], ], ]; } From 0f6c122508b92e4437680dbb47ec0346b664cb36 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Mon, 8 Jan 2024 10:35:16 +0700 Subject: [PATCH 33/34] Update CHANGELOG.md, improve --- CHANGELOG.md | 4 ++-- src/ColumnSchema.php | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9ede644..9c203395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.2.1 under development - Enh #324: Change property `Schema::$typeMap` to constant `Schema::TYPE_MAP` (@Tigrov) +- Enh #303: Support composite types (@Tigrov) ## 1.2.0 November 12, 2023 @@ -10,7 +11,6 @@ - Enh #300: Refactor `ArrayExpressionBuilder` (@Tigrov) - Enh #301: Refactor `JsonExpressionBuilder` (@Tigrov) - Enh #302: Refactor `ColumnSchema` (@Tigrov) -- Enh #303: Support composite types (@Tigrov) - Enh #321: Move methods from `Command` to `AbstractPdoCommand` class (@Tigrov) - Bug #302: Fix incorrect convert string value for BIT type (@Tigrov) - Bug #309: Fix retrieving sequence name from default value (@Tigrov) @@ -26,7 +26,7 @@ - Enh #294: Refactoring of `Schema::normalizeDefaultValue()` method (@Tigrov) - Bug #287: Fix `bit` type (@Tigrov) - Bug #295: Fix multiline and single quote in default string value, add support for PostgreSQL 9.4 parentheses around negative numeric default values (@Tigrov) -- Bug #296: Prevent posible issues with array default values `('{one,two}'::text[])::varchar[]`, remove `ArrayParser::parseString()` (@Tigrov) +- Bug #296: Prevent possible issues with array default values `('{one,two}'::text[])::varchar[]`, remove `ArrayParser::parseString()` (@Tigrov) ## 1.0.0 April 12, 2023 diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index 44e48e4b..374f6543 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -223,15 +223,14 @@ private function phpTypecastComposite(mixed $value): array|null } $fields = []; - $columns = $this->columns; - $columnNames = array_keys($columns); + $columnNames = array_keys($this->columns); /** @psalm-var int|string $columnName */ foreach ($value as $columnName => $item) { $columnName = $columnNames[$columnName] ?? $columnName; - if (isset($columns[$columnName])) { - $item = $columns[$columnName]->phpTypecast($item); + if (isset($this->columns[$columnName])) { + $item = $this->columns[$columnName]->phpTypecast($item); } $fields[$columnName] = $item; From a5e1dbbb66f184cbb775024d75e04d9fa79f1b1c Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 9 Jan 2024 15:15:11 +0700 Subject: [PATCH 34/34] Remove `@psalm-var mixed` annotations --- src/Builder/CompositeExpressionBuilder.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Builder/CompositeExpressionBuilder.php b/src/Builder/CompositeExpressionBuilder.php index 7b26cdd1..5a531b12 100644 --- a/src/Builder/CompositeExpressionBuilder.php +++ b/src/Builder/CompositeExpressionBuilder.php @@ -40,7 +40,6 @@ public function __construct(private QueryBuilderInterface $queryBuilder) */ public function build(ExpressionInterface $expression, array &$params = []): string { - /** @psalm-var mixed $value */ $value = $expression->getValue(); if (empty($value)) { @@ -74,24 +73,18 @@ public function build(ExpressionInterface $expression, array &$params = []): str */ private function buildPlaceholders(CompositeExpression $expression, array &$params): array { - $placeholders = []; - - /** @psalm-var mixed $value */ $value = $expression->getNormalizedValue(); if (!is_iterable($value)) { - return $placeholders; + return []; } + $placeholders = []; $columns = $expression->getColumns(); - /** - * @psalm-var int|string $columnName - * @psalm-var mixed $item - */ + /** @psalm-var int|string $columnName */ foreach ($value as $columnName => $item) { if (isset($columns[$columnName])) { - /** @psalm-var mixed $item */ $item = $columns[$columnName]->dbTypecast($item); }