From 49c3a169d13801cea197eb79623a2ebf8797436f Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Tue, 1 Oct 2024 16:15:39 +0700 Subject: [PATCH 1/8] Refactor `ColumnSchemaInterface` (#881) --- .github/workflows/db-mssql.yml | 6 +- CHANGELOG.md | 1 + UPGRADE.md | 8 ++ src/Schema/Column/AbstractColumnSchema.php | 95 +++++++++++-- src/Schema/Column/ColumnBuilder.php | 12 +- src/Schema/Column/ColumnSchemaInterface.php | 126 +++++++++++++++--- src/Syntax/ColumnDefinitionParser.php | 1 - tests/Common/CommonSchemaTest.php | 20 +-- tests/Db/Schema/ColumnSchemaTest.php | 64 ++++++++- tests/Provider/ColumnBuilderProvider.php | 2 +- .../ColumnDefinitionParserProvider.php | 8 +- tests/Provider/ColumnSchemaProvider.php | 13 ++ 12 files changed, 299 insertions(+), 57 deletions(-) diff --git a/.github/workflows/db-mssql.yml b/.github/workflows/db-mssql.yml index 1f22d0d2a..ad0c8cbe6 100644 --- a/.github/workflows/db-mssql.yml +++ b/.github/workflows/db-mssql.yml @@ -27,7 +27,7 @@ jobs: CURRENT_PACKAGE: db-mssql EXTENSIONS: pdo, pdo_sqlsrv-5.10.1 - runs-on: ubuntu-latest + runs-on: ${{ matrix.mssql.os || 'ubuntu-latest' }} strategy: matrix: @@ -43,7 +43,9 @@ jobs: include: - php: 8.3 - mssql: { server: 2017-latest } + mssql: + server: 2017-latest + os: ubuntu-20.04 - php: 8.3 mssql: server: 2019-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 44ce8fb13..bc2e06569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - Enh #875: Ignore "Packets out of order..." warnings in `AbstractPdoCommand::internalExecute()` method (@Tigrov) - Enh #877: Separate column type constants (@Tigrov) - Enh #878: Realize `ColumnBuilder` class (@Tigrov) +- Enh #881: Refactor `ColumnSchemaInterface` and `AbstractColumnSchema` (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/UPGRADE.md b/UPGRADE.md index 6fe3a7c57..3eaab5f31 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -67,6 +67,14 @@ and the following changes were made: - `getName()` method can return `string` or `null`; - `getPhpType()` method must return `string` PHP type of the column which used for generating related model properties; - `name(string|null $name)` method is added; +- `check(string|null $check)` method is added; +- `getCheck()` method is added; +- `reference(ForeignKeyConstraint|null $reference)` method is added; +- `getReference()` method is added; +- `notNull(bool $notNull = true)` method is added; +- `isNotNull()` method is added; +- `unique(bool $unique = true)` method is added; +- `isUnique()` method is added; - `load(array $info)` method is added; - constructor of `AbstractColumnSchema` class is changed to `__construct(string $type, string|null $phpType = null)`; - added method chaining. diff --git a/src/Schema/Column/AbstractColumnSchema.php b/src/Schema/Column/AbstractColumnSchema.php index 8281163a5..a6b4ef8a0 100644 --- a/src/Schema/Column/AbstractColumnSchema.php +++ b/src/Schema/Column/AbstractColumnSchema.php @@ -7,12 +7,14 @@ use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Constant\PhpType; +use Yiisoft\Db\Constraint\ForeignKeyConstraint; + use function is_array; /** * Represents the metadata of a column in a database table. * - * It provides information about the column's name, type, size, precision, and other details. + * It provides information about the column's type, size, scale, and other details. * * The `ColumnSchema` class is used to store and retrieve metadata about a column in a database table. * @@ -25,10 +27,9 @@ * use Yiisoft\Db\Schema\ColumnSchema; * * $column = (new IntegerColumnSchema()) - * ->name('id') - * ->allowNull(false) - * ->dbType('int(11)') - * ->type('integer') + * ->notNull() + * ->dbType('int') + * ->size(11) * ->defaultValue(0) * ->autoIncrement() * ->primaryKey(); @@ -36,8 +37,8 @@ */ abstract class AbstractColumnSchema implements ColumnSchemaInterface { - private bool $allowNull = false; private bool $autoIncrement = false; + private string|null $check = null; private string|null $comment = null; private bool $computed = false; private string|null $dbType = null; @@ -46,9 +47,11 @@ abstract class AbstractColumnSchema implements ColumnSchemaInterface private string|null $extra = null; private bool $isPrimaryKey = false; private string|null $name = null; - private int|null $precision = null; + private bool $notNull = false; + private ForeignKeyConstraint|null $reference = null; private int|null $scale = null; private int|null $size = null; + private bool $unique = false; private bool $unsigned = false; /** @@ -59,9 +62,12 @@ public function __construct( ) { } + /** + * @deprecated Use {@see notNull()} instead. Will be removed in version 2.0. + */ public function allowNull(bool $allowNull = true): static { - $this->allowNull = $allowNull; + $this->notNull(!$allowNull); return $this; } @@ -71,6 +77,12 @@ public function autoIncrement(bool $autoIncrement = true): static return $this; } + public function check(string|null $check): static + { + $this->check = $check; + return $this; + } + public function comment(string|null $comment): static { $this->comment = $comment; @@ -107,6 +119,11 @@ public function extra(string|null $extra): static return $this; } + public function getCheck(): string|null + { + return $this->check; + } + public function getComment(): string|null { return $this->comment; @@ -132,14 +149,20 @@ public function getExtra(): string|null return $this->extra; } + /** + * @deprecated Will be removed in version 2.0. + */ public function getName(): string|null { return $this->name; } + /** + * @deprecated Use {@see getSize()} instead. Will be removed in version 2.0. + */ public function getPrecision(): int|null { - return $this->precision; + return $this->getSize(); } public function getPhpType(): string @@ -147,6 +170,11 @@ public function getPhpType(): string return PhpType::MIXED; } + public function getReference(): ForeignKeyConstraint|null + { + return $this->reference; + } + public function getScale(): int|null { return $this->scale; @@ -162,9 +190,12 @@ public function getType(): string return $this->type; } + /** + * @deprecated Use {@see isNotNull()} instead. Will be removed in version 2.0. + */ public function isAllowNull(): bool { - return $this->allowNull; + return !$this->isNotNull(); } public function isAutoIncrement(): bool @@ -177,11 +208,21 @@ public function isComputed(): bool return $this->computed; } + public function isNotNull(): bool + { + return $this->notNull; + } + public function isPrimaryKey(): bool { return $this->isPrimaryKey; } + public function isUnique(): bool + { + return $this->unique; + } + public function isUnsigned(): bool { return $this->unsigned; @@ -192,11 +233,13 @@ public function load(array $info): static foreach ($info as $key => $value) { /** * @psalm-suppress PossiblyInvalidCast - * @psalm-suppress RiskyCast + * @psalm-suppress InvalidCast + * @psalm-suppress DeprecatedMethod */ match ($key) { 'allow_null' => $this->allowNull((bool) $value), 'auto_increment' => $this->autoIncrement((bool) $value), + 'check' => $this->check($value !== null ? (string) $value : null), 'comment' => $this->comment($value !== null ? (string) $value : null), 'computed' => $this->computed((bool) $value), 'db_type' => $this->dbType($value !== null ? (string) $value : null), @@ -204,10 +247,13 @@ public function load(array $info): static 'enum_values' => $this->enumValues(is_array($value) ? $value : null), 'extra' => $this->extra($value !== null ? (string) $value : null), 'name' => $this->name($value !== null ? (string) $value : null), + 'not_null' => $this->notNull((bool) $value), 'primary_key' => $this->primaryKey((bool) $value), 'precision' => $this->precision($value !== null ? (int) $value : null), + 'reference' => $this->reference($value instanceof ForeignKeyConstraint ? $value : null), 'scale' => $this->scale($value !== null ? (int) $value : null), 'size' => $this->size($value !== null ? (int) $value : null), + 'unique' => $this->unique((bool) $value), 'unsigned' => $this->unsigned((bool) $value), default => null, }; @@ -216,24 +262,41 @@ public function load(array $info): static return $this; } + /** + * @deprecated Will be removed in version 2.0. + */ public function name(string|null $name): static { $this->name = $name; return $this; } - public function precision(int|null $precision): static + public function notNull(bool $notNull = true): static { - $this->precision = $precision; + $this->notNull = $notNull; return $this; } + /** + * @deprecated Use {@see size()} instead. Will be removed in version 2.0. + */ + public function precision(int|null $precision): static + { + return $this->size($precision); + } + public function primaryKey(bool $isPrimaryKey = true): static { $this->isPrimaryKey = $isPrimaryKey; return $this; } + public function reference(ForeignKeyConstraint|null $reference): static + { + $this->reference = $reference; + return $this; + } + public function scale(int|null $scale): static { $this->scale = $scale; @@ -252,6 +315,12 @@ public function type(string $type): static return $this; } + public function unique(bool $unique = true): static + { + $this->unique = $unique; + return $this; + } + public function unsigned(bool $unsigned = true): static { $this->unsigned = $unsigned; diff --git a/src/Schema/Column/ColumnBuilder.php b/src/Schema/Column/ColumnBuilder.php index 0f89b3f6e..ababbb70d 100644 --- a/src/Schema/Column/ColumnBuilder.php +++ b/src/Schema/Column/ColumnBuilder.php @@ -21,8 +21,7 @@ public static function primaryKey(bool $autoIncrement = true): ColumnSchemaInter { return static::integer() ->primaryKey() - ->autoIncrement($autoIncrement) - ->allowNull(false); + ->autoIncrement($autoIncrement); } /** @@ -32,8 +31,7 @@ public static function smallPrimaryKey(bool $autoIncrement = true): ColumnSchema { return static::smallint() ->primaryKey() - ->autoIncrement($autoIncrement) - ->allowNull(false); + ->autoIncrement($autoIncrement); } /** @@ -43,8 +41,7 @@ public static function bigPrimaryKey(bool $autoIncrement = true): ColumnSchemaIn { return static::bigint() ->primaryKey() - ->autoIncrement($autoIncrement) - ->allowNull(false); + ->autoIncrement($autoIncrement); } /** @@ -54,8 +51,7 @@ public static function uuidPrimaryKey(bool $autoIncrement = false): ColumnSchema { return static::uuid() ->primaryKey() - ->autoIncrement($autoIncrement) - ->allowNull(false); + ->autoIncrement($autoIncrement); } // Abstract type column builders diff --git a/src/Schema/Column/ColumnSchemaInterface.php b/src/Schema/Column/ColumnSchemaInterface.php index 99a743928..9e55230ee 100644 --- a/src/Schema/Column/ColumnSchemaInterface.php +++ b/src/Schema/Column/ColumnSchemaInterface.php @@ -6,14 +6,15 @@ use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Constant\PhpType; +use Yiisoft\Db\Constraint\ForeignKeyConstraint; /** * This interface defines a set of methods that must be implemented by a class that represents the column schema of a * database table column. * * @psalm-type ColumnInfo = array{ - * allow_null?: bool|string|null, * auto_increment?: bool|string, + * check?: string|null, * comment?: string|null, * computed?: bool|string, * db_type?: string|null, @@ -21,13 +22,14 @@ * enum_values?: array|null, * extra?: string|null, * primary_key?: bool|string, - * name?: string|null, - * precision?: int|string|null, + * not_null?: bool|string, + * reference?: ForeignKeyConstraint|null, * scale?: int|string|null, * schema?: string|null, * size?: int|string|null, * table?: string|null, * type?: ColumnType::*, + * unique?: bool|string, * unsigned?: bool|string, * ... * } @@ -41,9 +43,11 @@ interface ColumnSchemaInterface * * ```php * $columns = [ - * 'description' => $this->text()->allowNull(), + * 'description' => ColumnBuilder::text()->allowNull(), * ]; * ``` + * + * @deprecated Use {@see notNull()} instead. Will be removed in version 2.0. */ public function allowNull(bool $allowNull = true): static; @@ -56,12 +60,23 @@ public function allowNull(bool $allowNull = true): static; * * ```php * $columns = [ - * 'id' => $this->primaryKey()->autoIncrement(), + * 'id' => ColumnBuilder::primaryKey()->autoIncrement(), * ]; * ``` */ public function autoIncrement(bool $autoIncrement = true): static; + /** + * The check constraint for the column to specify an expression that must be true for each row in the table. + * + * ```php + * $columns = [ + * 'age' => ColumnBuilder::integer()->check('age > 0'), + * ]; + * ``` + */ + public function check(string|null $check): static; + /** * The comment for a column in a database table. * @@ -69,7 +84,7 @@ public function autoIncrement(bool $autoIncrement = true): static; * * ```php * $columns = [ - * 'description' => $this->text()->comment('Description of the product'), + * 'description' => ColumnBuilder::text()->comment('Description of the product'), * ]; * ``` */ @@ -82,7 +97,7 @@ public function comment(string|null $comment): static; * * ```php * $columns = [ - * 'description' => $this->text()->computed(true), + * 'full_name' => ColumnBuilder::text()->computed(true), * ]; * ``` */ @@ -97,7 +112,7 @@ public function computed(bool $computed = true): static; * * ```php * $columns = [ - * 'description' => $this->text()->dbType('text'), + * 'description' => ColumnBuilder::text()->dbType('text'), * ]; * ``` */ @@ -120,7 +135,7 @@ public function dbTypecast(mixed $value): mixed; * * ```php * $columns = [ - * 'description' => $this->text()->defaultValue('Description of the product'), + * 'description' => ColumnBuilder::text()->defaultValue('Description of the product'), * ]; * ``` */ @@ -131,7 +146,7 @@ public function defaultValue(mixed $defaultValue): static; * * ```php * $columns = [ - * 'status' => $this->string(16)->enumValues(['active', 'inactive']), + * 'status' => ColumnBuilder::string(16)->enumValues(['active', 'inactive']), * ]; * ``` */ @@ -145,12 +160,19 @@ public function enumValues(array|null $enumValues): static; * * ```php * $columns = [ - * 'description' => $this->text()->extra('ON UPDATE CURRENT_TIMESTAMP'), + * 'updated_at' => ColumnBuilder::integer()->extra('ON UPDATE CURRENT_TIMESTAMP'), * ]; * ``` */ public function extra(string|null $extra): static; + /** + * Returns the check constraint for the column. + * + * @see check() + */ + public function getCheck(): string|null; + /** * @return string|null The comment of the column. * @@ -192,6 +214,8 @@ public function getExtra(): string|null; /** * @return string|null The name of the column. + * + * @deprecated Will be removed in version 2.0. */ public function getName(): string|null; @@ -199,6 +223,8 @@ public function getName(): string|null; * @return int|null The precision of the column. * * @see precision() + * + * @deprecated Use {@see getSize()} instead. Will be removed in version 2.0. */ public function getPrecision(): int|null; @@ -210,6 +236,13 @@ public function getPrecision(): int|null; */ public function getPhpType(): string; + /** + * Returns the reference to the foreign key constraint. + * + * @see reference() + */ + public function getReference(): ForeignKeyConstraint|null; + /** * @return int|null The scale of the column. * @@ -236,6 +269,8 @@ public function getType(): string; * Whether this column is nullable. * * @see allowNull() + * + * @deprecated Use {@see isNotNull()} instead. Will be removed in version 2.0. */ public function isAllowNull(): bool; @@ -255,6 +290,13 @@ public function isAutoIncrement(): bool; */ public function isComputed(): bool; + /** + * Whether this column is not nullable. + * + * @see notNull() + */ + public function isNotNull(): bool; + /** * Whether this column is a part of primary key. * @@ -262,6 +304,13 @@ public function isComputed(): bool; */ public function isPrimaryKey(): bool; + /** + * Whether this column has a unique index. + * + * @see unique() + */ + public function isUnique(): bool; + /** * Whether this column is unsigned. This is only meaningful when {@see type} is `tinyint`, `smallint`, `integer` * or `bigint`. @@ -282,12 +331,25 @@ public function load(array $info): static; * * ```php * $columns = [ - * 'id' => $this->primaryKey()->name('id'), + * 'id' => ColumnBuilder::primaryKey()->name('id'), * ]; * ``` + * + * @deprecated Will be removed in version 2.0. */ public function name(string|null $name): static; + /** + * Whether the column is not nullable. + * + * ```php + * $columns = [ + * 'description' => ColumnBuilder::text()->notNull(), + * ]; + * ``` + */ + public function notNull(bool $notNull = true): static; + /** * Converts the input value according to {@see phpType} after retrieval from the database. * @@ -301,8 +363,10 @@ public function phpTypecast(mixed $value): mixed; * * ```php * $columns = [ - * 'price' => $this->decimal(10, 2)->precision(10), + * 'price' => ColumnBuilder::decimal(10, 2)->precision(10), * ]; + * + * @deprecated Use {@see size()} instead. Will be removed in version 2.0. */ public function precision(int|null $precision): static; @@ -311,19 +375,34 @@ public function precision(int|null $precision): static; * * ```php * $columns = [ - * 'id' => $this->primaryKey(true), + * 'id' => ColumnBuilder::primaryKey(), * ]; * ``` */ public function primaryKey(bool $isPrimaryKey = true): static; + /** + * The reference to the foreign key constraint. + * + * ```php + * $reference = new ForeignKeyConstraint(); + * $reference->foreignTableName('user'); + * $reference->foreignColumnNames(['id']); + * + * $columns = [ + * 'user_id' => ColumnBuilder::integer()->reference($reference), + * ]; + * ``` + */ + public function reference(ForeignKeyConstraint|null $reference): static; + /** * The scale is the number of digits to the right of the decimal point and is only meaningful when {@see type} is * `decimal`. * * ```php * $columns = [ - * 'price' => $this->decimal(10, 2)->scale(2), + * 'price' => ColumnBuilder::decimal(10, 2)->scale(2), * ]; * ``` */ @@ -336,7 +415,7 @@ public function scale(int|null $scale): static; * * ```php * $columns = [ - * 'name' => $this->string()->size(255), + * 'name' => ColumnBuilder::string()->size(255), * ]; * ``` */ @@ -347,20 +426,31 @@ public function size(int|null $size): static; * * ```php * $columns = [ - * 'description' => $this->text()->type('text'), + * 'description' => ColumnBuilder::text()->type('text'), * ]; * * @psalm-param ColumnType::* $type */ public function type(string $type): static; + /** + * Whether the column has a unique index. + * + * ```php + * $columns = [ + * 'username' => ColumnBuilder::string()->unique(), + * ]; + * ``` + */ + public function unique(bool $unique = true): static; + /** * Whether the column type is an unsigned integer. * It's a data type that can only represent positive whole numbers only. * * ```php * $columns = [ - * 'age' => $this->integer()->unsigned(), + * 'age' => ColumnBuilder::integer()->unsigned(), * ]; * ``` */ diff --git a/src/Syntax/ColumnDefinitionParser.php b/src/Syntax/ColumnDefinitionParser.php index a81f79e15..7c66953d0 100644 --- a/src/Syntax/ColumnDefinitionParser.php +++ b/src/Syntax/ColumnDefinitionParser.php @@ -86,7 +86,6 @@ private function sizeInfo(string $size): array $info = [ 'size' => (int) $values[0], - 'precision' => (int) $values[0], ]; if (isset($values[1])) { diff --git a/tests/Common/CommonSchemaTest.php b/tests/Common/CommonSchemaTest.php index 1fad7be35..3f2ffd6fe 100644 --- a/tests/Common/CommonSchemaTest.php +++ b/tests/Common/CommonSchemaTest.php @@ -858,9 +858,9 @@ protected function columnSchema(array $columns, string $table): void ); $this->assertSame($expected['type'], $column->getType(), "type of column $name does not match."); $this->assertSame( - $expected['allowNull'], - $column->isAllowNull(), - "allowNull of column $name does not match." + $expected['notNull'], + $column->isNotNull(), + "notNull of column $name does not match." ); $this->assertSame( $expected['autoIncrement'], @@ -873,11 +873,6 @@ protected function columnSchema(array $columns, string $table): void "enumValues of column $name does not match." ); $this->assertSame($expected['size'], $column->getSize(), "size of column $name does not match."); - $this->assertSame( - $expected['precision'], - $column->getPrecision(), - "precision of column $name does not match." - ); $this->assertSame($expected['scale'], $column->getScale(), "scale of column $name does not match."); @@ -899,6 +894,14 @@ protected function columnSchema(array $columns, string $table): void ); } + if (isset($expected['unique'])) { + $this->assertSame( + $expected['unique'], + $column->isUnique(), + "unique of column $name does not match" + ); + } + if (isset($expected['unsigned'])) { $this->assertSame( $expected['unsigned'], @@ -929,7 +932,6 @@ protected function columnSchema(array $columns, string $table): void 'phpType' => $arrayColumn->getPhpType(), 'enumValues' => $arrayColumn->getEnumValues(), 'size' => $arrayColumn->getSize(), - 'precision' => $arrayColumn->getPrecision(), 'scale' => $arrayColumn->getScale(), ], "array column of column $name does not match" diff --git a/tests/Db/Schema/ColumnSchemaTest.php b/tests/Db/Schema/ColumnSchemaTest.php index 65e7277cf..71852b1a3 100644 --- a/tests/Db/Schema/ColumnSchemaTest.php +++ b/tests/Db/Schema/ColumnSchemaTest.php @@ -5,6 +5,7 @@ namespace Yiisoft\Db\Tests\Db\Schema; use PHPUnit\Framework\TestCase; +use Yiisoft\Db\Constraint\ForeignKeyConstraint; use Yiisoft\Db\Tests\Support\Stub\ColumnSchema; /** @@ -18,7 +19,7 @@ public function testAllowNull(): void { $column = new ColumnSchema(); - $this->assertFalse($column->isAllowNull()); + $this->assertTrue($column->isAllowNull()); $this->assertSame($column, $column->allowNull()); $this->assertTrue($column->isAllowNull()); @@ -48,6 +49,19 @@ public function testAutoIncrement(): void $this->assertTrue($column->isAutoIncrement()); } + public function testCheck(): void + { + $column = new ColumnSchema(); + + $this->assertNull($column->getCheck()); + $this->assertSame($column, $column->check('age > 0')); + $this->assertSame('age > 0', $column->getCheck()); + + $column->check(null); + + $this->assertNull($column->getCheck()); + } + public function testComment(): void { $column = new ColumnSchema(); @@ -153,6 +167,23 @@ public function testName(): void $this->assertSame('', $column->getName()); } + public function testNotNull(): void + { + $column = new ColumnSchema(); + + $this->assertFalse($column->isNotNull()); + $this->assertSame($column, $column->notNull()); + $this->assertTrue($column->isNotNull()); + + $column->notNull(false); + + $this->assertFalse($column->isNotNull()); + + $column->notNull(true); + + $this->assertTrue($column->isNotNull()); + } + public function testPrecision(): void { $column = new ColumnSchema(); @@ -183,6 +214,20 @@ public function testPrimaryKey(): void $this->assertTrue($column->isPrimaryKey()); } + public function testReference(): void + { + $column = new ColumnSchema(); + $fk = new ForeignKeyConstraint(); + + $this->assertNull($column->getReference()); + $this->assertSame($column, $column->reference($fk)); + $this->assertSame($fk, $column->getReference()); + + $column->reference(null); + + $this->assertNull($column->getReference()); + } + public function testScale(): void { $column = new ColumnSchema(); @@ -222,6 +267,23 @@ public function testType(): void $this->assertSame('', $column->getType()); } + public function testUnique(): void + { + $column = new ColumnSchema(); + + $this->assertFalse($column->isUnique()); + $this->assertSame($column, $column->unique()); + $this->assertTrue($column->isUnique()); + + $column->unique(false); + + $this->assertFalse($column->isUnique()); + + $column->unique(true); + + $this->assertTrue($column->isUnique()); + } + public function testUnsigned(): void { $column = new ColumnSchema(); diff --git a/tests/Provider/ColumnBuilderProvider.php b/tests/Provider/ColumnBuilderProvider.php index 7deff7338..ea4d997f9 100644 --- a/tests/Provider/ColumnBuilderProvider.php +++ b/tests/Provider/ColumnBuilderProvider.php @@ -23,9 +23,9 @@ class ColumnBuilderProvider 'getExtra' => null, 'getScale' => null, 'getSize' => null, - 'isAllowNull' => false, 'isAutoIncrement' => false, 'isComputed' => false, + 'isNotNull' => false, 'isPrimaryKey' => false, 'isUnsigned' => false, ]; diff --git a/tests/Provider/ColumnDefinitionParserProvider.php b/tests/Provider/ColumnDefinitionParserProvider.php index 7d2b4cabc..2ad24ab29 100644 --- a/tests/Provider/ColumnDefinitionParserProvider.php +++ b/tests/Provider/ColumnDefinitionParserProvider.php @@ -11,11 +11,11 @@ public static function parse(): array return [ ['', ['db_type' => '']], ['int', ['db_type' => 'int']], - ['int(10)', ['db_type' => 'int', 'size' => 10, 'precision' => 10]], + ['int(10)', ['db_type' => 'int', 'size' => 10]], ['int UNSIGNED', ['db_type' => 'int', 'unsigned' => true]], - ['int(10) UNSIGNED', ['db_type' => 'int', 'size' => 10, 'precision' => 10, 'unsigned' => true]], - ['int(10) UNSIGNED NOT NULL', ['db_type' => 'int', 'size' => 10, 'precision' => 10, 'unsigned' => true, 'extra' => 'NOT NULL']], - ['int(10) NOT NULL', ['db_type' => 'int', 'size' => 10, 'precision' => 10, 'extra' => 'NOT NULL']], + ['int(10) UNSIGNED', ['db_type' => 'int', 'size' => 10, 'unsigned' => true]], + ['int(10) UNSIGNED NOT NULL', ['db_type' => 'int', 'size' => 10, 'unsigned' => true, 'extra' => 'NOT NULL']], + ['int(10) NOT NULL', ['db_type' => 'int', 'size' => 10, 'extra' => 'NOT NULL']], ['text NOT NULL', ['db_type' => 'text', 'extra' => 'NOT NULL']], ["enum('a','b','c')", ['db_type' => 'enum', 'enum_values' => ['a', 'b', 'c']]], ["enum('a','b','c') NOT NULL", ['db_type' => 'enum', 'enum_values' => ['a', 'b', 'c'], 'extra' => 'NOT NULL']], diff --git a/tests/Provider/ColumnSchemaProvider.php b/tests/Provider/ColumnSchemaProvider.php index 83bf35b71..9bcbe4bca 100644 --- a/tests/Provider/ColumnSchemaProvider.php +++ b/tests/Provider/ColumnSchemaProvider.php @@ -8,6 +8,7 @@ use stdClass; use Yiisoft\Db\Command\Param; use Yiisoft\Db\Constant\ColumnType; +use Yiisoft\Db\Constraint\ForeignKeyConstraint; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Constant\PhpType; @@ -256,6 +257,8 @@ public static function load(): array ['auto_increment', false, 'isAutoIncrement', false], ['auto_increment', '1', 'isAutoIncrement', true], ['auto_increment', '0', 'isAutoIncrement', false], + ['check', 'age > 0', 'getCheck', 'age > 0'], + ['check', null, 'getCheck', null], ['comment', 'Lorem ipsum', 'getComment', 'Lorem ipsum'], ['comment', null, 'getComment', null], ['computed', true, 'isComputed', true], @@ -272,16 +275,26 @@ public static function load(): array ['extra', null, 'getExtra', null], ['name', 'name', 'getName', 'name'], ['name', null, 'getName', null], + ['not_null', true, 'isNotNull', true], + ['not_null', false, 'isNotNull', false], + ['not_null', '1', 'isNotNull', true], + ['not_null', '0', 'isNotNull', false], ['precision', 10, 'getPrecision', 10], ['precision', null, 'getPrecision', null], ['primary_key', true, 'isPrimaryKey', true], ['primary_key', false, 'isPrimaryKey', false], ['primary_key', '1', 'isPrimaryKey', true], ['primary_key', '0', 'isPrimaryKey', false], + ['reference', $fk = new ForeignKeyConstraint(), 'getReference', $fk], + ['reference', null, 'getReference', null], ['scale', 2, 'getScale', 2], ['scale', null, 'getScale', null], ['size', 255, 'getSize', 255], ['size', null, 'getSize', null], + ['unique', true, 'isUnique', true], + ['unique', false, 'isUnique', false], + ['unique', '1', 'isUnique', true], + ['unique', '0', 'isUnique', false], ['unsigned', true, 'isUnsigned', true], ['unsigned', false, 'isUnsigned', false], ['unsigned', '1', 'isUnsigned', true], From 42e1168abfbc0d94705b6bdb43fea7b5d29e911d Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Tue, 8 Oct 2024 18:37:58 +0700 Subject: [PATCH 2/8] Move `ArrayColumnSchema` and `StructuredColumnSchema` from `db-pgsql` (#882) --- CHANGELOG.md | 3 +- UPGRADE.md | 2 + src/Expression/ArrayExpression.php | 2 +- src/Expression/Expression.php | 2 +- src/Expression/JsonExpression.php | 2 +- src/Expression/StructuredExpression.php | 126 +++++++++++++ src/Schema/Column/AbstractColumnFactory.php | 3 + src/Schema/Column/ArrayColumnSchema.php | 175 ++++++++++++++++++ src/Schema/Column/ColumnBuilder.php | 26 +++ src/Schema/Column/ColumnSchemaInterface.php | 3 + src/Schema/Column/StructuredColumnSchema.php | 109 +++++++++++ src/Syntax/ParserToArrayInterface.php | 20 ++ tests/AbstractColumnFactoryTest.php | 12 +- .../Expression/StructuredExpressionTest.php | 40 ++++ tests/Db/Schema/ColumnSchemaTest.php | 132 +++++++++++++ tests/Provider/ColumnBuilderProvider.php | 119 +++++++----- tests/Provider/ColumnFactoryProvider.php | 2 + tests/Provider/ColumnSchemaProvider.php | 121 ++++++++++++ tests/Provider/StructuredTypeProvider.php | 68 +++++++ 19 files changed, 914 insertions(+), 53 deletions(-) create mode 100644 src/Expression/StructuredExpression.php create mode 100644 src/Schema/Column/ArrayColumnSchema.php create mode 100644 src/Schema/Column/StructuredColumnSchema.php create mode 100644 src/Syntax/ParserToArrayInterface.php create mode 100644 tests/Db/Expression/StructuredExpressionTest.php create mode 100644 tests/Provider/StructuredTypeProvider.php diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2e06569..dcbc294f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,8 @@ - Enh #875: Ignore "Packets out of order..." warnings in `AbstractPdoCommand::internalExecute()` method (@Tigrov) - Enh #877: Separate column type constants (@Tigrov) - Enh #878: Realize `ColumnBuilder` class (@Tigrov) -- Enh #881: Refactor `ColumnSchemaInterface` and `AbstractColumnSchema` (@Tigrov) +- Enh #881: Refactor `ColumnSchemaInterface` and `AbstractColumnSchema` (@Tigrov) +- End #882: Move `ArrayColumnSchema` and `StructuredColumnSchema` classes from `db-pgsql` package (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/UPGRADE.md b/UPGRADE.md index 3eaab5f31..cb859fd69 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -97,6 +97,8 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace - `DoubleColumnSchema` for columns with fractional number type (float, double, decimal, money); - `StringColumnSchema` for columns with string or datetime type (char, string, text, datetime, timestamp, date, time); - `BinaryColumnSchema` for columns with binary type; +- `ArrayColumnSchema` for columns with array type; +- `StructuredColumnSchema` for columns with structured type (composite type in PostgreSQL); - `JsonColumnSchema` for columns with json type. ### New methods diff --git a/src/Expression/ArrayExpression.php b/src/Expression/ArrayExpression.php index 52e1d6788..7303e4e21 100644 --- a/src/Expression/ArrayExpression.php +++ b/src/Expression/ArrayExpression.php @@ -29,7 +29,7 @@ * @template-implements ArrayAccess * @template-implements IteratorAggregate */ -class ArrayExpression implements ExpressionInterface, ArrayAccess, Countable, IteratorAggregate +final class ArrayExpression implements ExpressionInterface, ArrayAccess, Countable, IteratorAggregate { public function __construct(private mixed $value = [], private string|null $type = null, private int $dimension = 1) { diff --git a/src/Expression/Expression.php b/src/Expression/Expression.php index 174f66399..374e202b2 100644 --- a/src/Expression/Expression.php +++ b/src/Expression/Expression.php @@ -26,7 +26,7 @@ * * @psalm-import-type ParamsType from ConnectionInterface */ -class Expression implements ExpressionInterface, Stringable +final class Expression implements ExpressionInterface, Stringable { /** * @psalm-param ParamsType $params diff --git a/src/Expression/JsonExpression.php b/src/Expression/JsonExpression.php index 884279ae4..71688d687 100644 --- a/src/Expression/JsonExpression.php +++ b/src/Expression/JsonExpression.php @@ -17,7 +17,7 @@ * new JsonExpression(['a' => 1, 'b' => 2]); // will be encoded to '{"a": 1, "b": 2}' * ``` */ -class JsonExpression implements ExpressionInterface, JsonSerializable +final class JsonExpression implements ExpressionInterface, JsonSerializable { public function __construct(protected mixed $value, private string|null $type = null) { diff --git a/src/Expression/StructuredExpression.php b/src/Expression/StructuredExpression.php new file mode 100644 index 000000000..ffbc6efc6 --- /dev/null +++ b/src/Expression/StructuredExpression.php @@ -0,0 +1,126 @@ + 10, 'currency_code' => 'USD']); + * ``` + * + * Will be encoded to `ROW(10, USD)` in PostgreSQL. + */ +final class StructuredExpression implements ExpressionInterface +{ + /** + * @param array|object $value The content of the structured type. It can be represented as + * - an associative `array` of column names and values; + * - an indexed `array` of column values in the order of structured type columns; + * - an `iterable` object that can be converted to an `array` using `iterator_to_array()`; + * - an `object` that can be converted to an `array` using `get_object_vars()`; + * - an `ExpressionInterface` object that represents a SQL expression. + * @param string|null $type The structured database type name. Defaults to `null` which means the type is not + * explicitly specified. Note that in the case where a type is not specified explicitly and DBMS cannot guess it + * from the context, SQL error will be raised. + * @param ColumnSchemaInterface[] $columns The structured type columns that are used for value normalization and type + * casting. + * + * @psalm-param array $columns + */ + public function __construct( + private array|object $value, + private string|null $type = null, + private array $columns = [], + ) { + } + + /** + * The structured type name. + * + * Defaults to `null` which means the type is not explicitly specified. + * + * Note that in the case where a type is not specified explicitly and DBMS cannot guess it from the context, + * SQL error will be raised. + */ + public function getType(): string|null + { + return $this->type; + } + + /** + * The structured type columns that are used for value normalization and type casting. + * + * @return ColumnSchemaInterface[] + */ + public function getColumns(): array + { + return $this->columns; + } + + /** + * The content of the structured type. It can be represented as + * - an associative `array` of column names and values; + * - an indexed `array` of column values in the order of structured type columns; + * - an `iterable` object that can be converted to an `array` using `iterator_to_array()`; + * - an `object` that can be converted to an `array` using `get_object_vars()`; + * - an `ExpressionInterface` object that represents a SQL expression. + */ + public function getValue(): array|object + { + return $this->value; + } + + /** + * Returns the normalized value of the structured type, where: + * - values sorted according to the order of structured type columns; + * - indexed keys are replaced with column names; + * - missing elements are filled in with default values; + * - excessive elements are removed. + * + * If the structured type columns are not specified or the value is an `ExpressionInterface` object, + * it will be returned as is. + */ + public function getNormalizedValue(): array|object + { + $value = $this->value; + + if (empty($this->columns) || $value instanceof ExpressionInterface) { + return $value; + } + + if (is_object($value)) { + $value = $value instanceof Traversable + ? iterator_to_array($value) + : get_object_vars($value); + } + + $normalized = []; + $columnsNames = array_keys($this->columns); + + foreach ($columnsNames as $i => $columnsName) { + $normalized[$columnsName] = match (true) { + array_key_exists($columnsName, $value) => $value[$columnsName], + array_key_exists($i, $value) => $value[$i], + default => $this->columns[$columnsName]->getDefaultValue(), + }; + } + + return $normalized; + } +} diff --git a/src/Schema/Column/AbstractColumnFactory.php b/src/Schema/Column/AbstractColumnFactory.php index 6cb1329f0..194d5c567 100644 --- a/src/Schema/Column/AbstractColumnFactory.php +++ b/src/Schema/Column/AbstractColumnFactory.php @@ -102,6 +102,7 @@ public function fromType(string $type, array $info = []): ColumnSchemaInterface ColumnType::FLOAT => new DoubleColumnSchema($type), ColumnType::DOUBLE => new DoubleColumnSchema($type), ColumnType::BINARY => new BinaryColumnSchema($type), + ColumnType::STRUCTURED => new StructuredColumnSchema($type), ColumnType::JSON => new JsonColumnSchema($type), default => new StringColumnSchema($type), }; @@ -162,6 +163,8 @@ protected function isType(string $type): bool ColumnType::TIMESTAMP, ColumnType::DATE, ColumnType::TIME, + ColumnType::ARRAY, + ColumnType::STRUCTURED, ColumnType::JSON => true, default => false, }; diff --git a/src/Schema/Column/ArrayColumnSchema.php b/src/Schema/Column/ArrayColumnSchema.php new file mode 100644 index 000000000..1f93a5d45 --- /dev/null +++ b/src/Schema/Column/ArrayColumnSchema.php @@ -0,0 +1,175 @@ +column = $column; + return $this; + } + + /** + * @return ColumnSchemaInterface the column of an array item. + */ + public function getColumn(): ColumnSchemaInterface + { + if ($this->column === null) { + $this->column = new StringColumnSchema(); + $this->column->dbType($this->getDbType()); + $this->column->enumValues($this->getEnumValues()); + $this->column->scale($this->getScale()); + $this->column->size($this->getSize()); + } + + return $this->column; + } + + /** + * Set dimension of an array, must be greater than 0. + */ + public function dimension(int $dimension): static + { + $this->dimension = $dimension; + return $this; + } + + /** + * @return int the dimension of the array. + */ + public function getDimension(): int + { + return $this->dimension; + } + + public function getPhpType(): string + { + return PhpType::ARRAY; + } + + public function dbTypecast(mixed $value): ExpressionInterface|null + { + if ($value === null || $value instanceof ExpressionInterface) { + return $value; + } + + if ($this->dimension === 1 && is_array($value)) { + $value = array_map($this->getColumn()->dbTypecast(...), $value); + } else { + $value = $this->dbTypecastArray($value, $this->dimension); + } + + return new ArrayExpression($value, $this->getDbType() ?? $this->getColumn()->getDbType(), $this->dimension); + } + + public function phpTypecast(mixed $value): array|null + { + if (is_string($value)) { + $value = $this->getParser()->parse($value); + } + + if (!is_array($value)) { + return null; + } + + $column = $this->getColumn(); + + if ($column->getType() === ColumnType::STRING) { + return $value; + } + + if ($this->dimension === 1 && $column->getType() !== ColumnType::JSON) { + return array_map($column->phpTypecast(...), $value); + } + + array_walk_recursive($value, function (string|null &$val) use ($column): void { + $val = $column->phpTypecast($val); + }); + + return $value; + } + + /** + * Recursively converts array values for use in a db query. + * + * @param mixed $value The array or iterable object. + * @param int $dimension The array dimension. Should be more than 0. + * + * @return array|null Converted values. + */ + protected function dbTypecastArray(mixed $value, int $dimension): array|null + { + if ($value === null) { + return null; + } + + if (!is_iterable($value)) { + return []; + } + + if ($dimension <= 1) { + return array_map( + $this->getColumn()->dbTypecast(...), + $value instanceof Traversable + ? iterator_to_array($value, false) + : $value + ); + } + + $items = []; + + foreach ($value as $val) { + $items[] = $this->dbTypecastArray($val, $dimension - 1); + } + + return $items; + } +} diff --git a/src/Schema/Column/ColumnBuilder.php b/src/Schema/Column/ColumnBuilder.php index ababbb70d..6dfb22722 100644 --- a/src/Schema/Column/ColumnBuilder.php +++ b/src/Schema/Column/ColumnBuilder.php @@ -227,6 +227,32 @@ public static function time(int|null $size = 0): ColumnSchemaInterface ->size($size); } + /** + * Builds a column with the abstract type `array`. + * + * @param ColumnSchemaInterface|null $column The column schema of the array elements. + */ + public static function array(ColumnSchemaInterface|null $column = null): ColumnSchemaInterface + { + return (new ArrayColumnSchema(ColumnType::ARRAY)) + ->column($column); + } + + /** + * Builds a column with the abstract type `structured`. + * + * @param string|null $dbType The DB type of the column. + * @param ColumnSchemaInterface[] $columns The columns (name -> instance) that the structured column should contain. + * + * @psalm-param array $columns + */ + public static function structured(string|null $dbType = null, array $columns = []): ColumnSchemaInterface + { + return (new StructuredColumnSchema(ColumnType::STRUCTURED)) + ->dbType($dbType) + ->columns($columns); + } + /** * Builds a column with the abstract type `json`. */ diff --git a/src/Schema/Column/ColumnSchemaInterface.php b/src/Schema/Column/ColumnSchemaInterface.php index 9e55230ee..4c5ff4388 100644 --- a/src/Schema/Column/ColumnSchemaInterface.php +++ b/src/Schema/Column/ColumnSchemaInterface.php @@ -15,10 +15,13 @@ * @psalm-type ColumnInfo = array{ * auto_increment?: bool|string, * check?: string|null, + * column?: ColumnSchemaInterface|null, + * columns?: array, * comment?: string|null, * computed?: bool|string, * db_type?: string|null, * default_value?: mixed, + * dimension?: int|string, * enum_values?: array|null, * extra?: string|null, * primary_key?: bool|string, diff --git a/src/Schema/Column/StructuredColumnSchema.php b/src/Schema/Column/StructuredColumnSchema.php new file mode 100644 index 000000000..241132989 --- /dev/null +++ b/src/Schema/Column/StructuredColumnSchema.php @@ -0,0 +1,109 @@ + + */ + private array $columns = []; + + /** + * Returns the parser for the column value. + */ + protected function getParser(): ParserToArrayInterface + { + throw new NotSupportedException(__METHOD__ . '() is not supported. Use concrete DBMS implementation.'); + } + + /** + * @psalm-param ColumnType::* $type + */ + public function __construct( + string $type = ColumnType::STRUCTURED, + ) { + parent::__construct($type); + } + + /** + * Set columns of the structured type. + * + * @param ColumnSchemaInterface[] $columns The metadata of the structured type columns. + * @psalm-param array $columns + */ + public function columns(array $columns): static + { + $this->columns = $columns; + return $this; + } + + /** + * Get the metadata of the structured type columns. + * + * @return ColumnSchemaInterface[] + */ + public function getColumns(): array + { + return $this->columns; + } + + public function getPhpType(): string + { + return PhpType::ARRAY; + } + + public function dbTypecast(mixed $value): mixed + { + if ($value === null || $value instanceof ExpressionInterface) { + return $value; + } + + /** @psalm-suppress MixedArgument */ + return new StructuredExpression($value, $this->getDbType(), $this->columns); + } + + public function phpTypecast(mixed $value): array|null + { + if (is_string($value)) { + $value = $this->getParser()->parse($value); + } + + if (!is_array($value)) { + return null; + } + + if (empty($this->columns)) { + return $value; + } + + $fields = []; + $columnNames = array_keys($this->columns); + + /** @psalm-var int|string $columnName */ + foreach ($value as $columnName => $item) { + $columnName = $columnNames[$columnName] ?? $columnName; + + if (isset($this->columns[$columnName])) { + $fields[$columnName] = $this->columns[$columnName]->phpTypecast($item); + } else { + $fields[$columnName] = $item; + } + } + + return $fields; + } +} diff --git a/src/Syntax/ParserToArrayInterface.php b/src/Syntax/ParserToArrayInterface.php new file mode 100644 index 000000000..1b30bbb8f --- /dev/null +++ b/src/Syntax/ParserToArrayInterface.php @@ -0,0 +1,20 @@ +assertInstanceOf($expectedInstanceOf, $column); $this->assertSame($expectedType, $column->getType()); $this->assertSame($dbType, $column->getDbType()); + + $db->close(); } /** @@ -51,6 +53,8 @@ public function testFromDefinition( foreach ($columnMethodResults as $method => $result) { $this->assertSame($result, $column->$method()); } + + $db->close(); } /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnFactoryProvider::pseudoTypes */ @@ -74,8 +78,10 @@ public function testFromPseudoType( ); foreach ($columnMethodResults as $method => $result) { - $this->assertSame($result, $column->$method()); + $this->assertEquals($result, $column->$method()); } + + $db->close(); } /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnFactoryProvider::types */ @@ -88,6 +94,8 @@ public function testFromType(string $type, string $expectedType, string $expecte $this->assertInstanceOf($expectedInstanceOf, $column); $this->assertSame($expectedType, $column->getType()); + + $db->close(); } public function testFromDefinitionWithExtra(): void @@ -101,5 +109,7 @@ public function testFromDefinitionWithExtra(): void $this->assertSame('char', $column->getType()); $this->assertSame(1, $column->getSize()); $this->assertSame('NOT NULL UNIQUE', $column->getExtra()); + + $db->close(); } } diff --git a/tests/Db/Expression/StructuredExpressionTest.php b/tests/Db/Expression/StructuredExpressionTest.php new file mode 100644 index 000000000..2c04ac6e6 --- /dev/null +++ b/tests/Db/Expression/StructuredExpressionTest.php @@ -0,0 +1,40 @@ + ColumnBuilder::money(10, 2), + 'currency' => ColumnBuilder::char(3), + ]; + + $expression = new StructuredExpression([5, 'USD'], 'currency_money_structured', $columns); + + $this->assertSame([5, 'USD'], $expression->getValue()); + $this->assertSame('currency_money_structured', $expression->getType()); + $this->assertSame($columns, $expression->getColumns()); + } + + /** @dataProvider \Yiisoft\Db\Tests\Provider\StructuredTypeProvider::normolizedValues */ + public function testGetNormalizedValue(mixed $value, mixed $expected, array $columns): void + { + $expression = new StructuredExpression($value, 'currency_money_structured', $columns); + + $this->assertSame($expected, $expression->getNormalizedValue()); + } +} diff --git a/tests/Db/Schema/ColumnSchemaTest.php b/tests/Db/Schema/ColumnSchemaTest.php index 71852b1a3..e76a1fef7 100644 --- a/tests/Db/Schema/ColumnSchemaTest.php +++ b/tests/Db/Schema/ColumnSchemaTest.php @@ -4,8 +4,19 @@ namespace Yiisoft\Db\Tests\Db\Schema; +use ArrayIterator; use PHPUnit\Framework\TestCase; use Yiisoft\Db\Constraint\ForeignKeyConstraint; +use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Expression\ArrayExpression; +use Yiisoft\Db\Expression\Expression; +use Yiisoft\Db\Expression\StructuredExpression; +use Yiisoft\Db\Schema\Column\ArrayColumnSchema; +use Yiisoft\Db\Schema\Column\ColumnBuilder; +use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; +use Yiisoft\Db\Schema\Column\IntegerColumnSchema; +use Yiisoft\Db\Schema\Column\StringColumnSchema; +use Yiisoft\Db\Schema\Column\StructuredColumnSchema; use Yiisoft\Db\Tests\Support\Stub\ColumnSchema; /** @@ -300,4 +311,125 @@ public function testUnsigned(): void $this->assertTrue($column->isUnsigned()); } + + public function testArrayColumnGetColumn(): void + { + $arrayCol = new ArrayColumnSchema(); + $intCol = new IntegerColumnSchema(); + + $this->assertInstanceOf(StringColumnSchema::class, $arrayCol->getColumn()); + $this->assertSame($arrayCol, $arrayCol->column($intCol)); + $this->assertSame($intCol, $arrayCol->getColumn()); + + $arrayCol->column(null); + + $this->assertInstanceOf(StringColumnSchema::class, $arrayCol->getColumn()); + } + + public function testArrayColumnGetDimension(): void + { + $arrayCol = new ArrayColumnSchema(); + + $this->assertSame(1, $arrayCol->getDimension()); + + $arrayCol->dimension(2); + $this->assertSame(2, $arrayCol->getDimension()); + } + + /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnSchemaProvider::dbTypecastArrayColumns */ + public function testArrayColumnDbTypecast(ColumnSchemaInterface $column, array $values): void + { + $arrayCol = ColumnBuilder::array($column); + + foreach ($values as [$dimension, $expected, $value]) { + $arrayCol->dimension($dimension); + $dbValue = $arrayCol->dbTypecast($value); + + $this->assertInstanceOf(ArrayExpression::class, $dbValue); + $this->assertSame($dimension, $dbValue->getDimension()); + + $this->assertEquals($expected, $dbValue->getValue()); + } + } + + public function testArrayColumnDbTypecastSimple() + { + $arrayCol = new ArrayColumnSchema(); + + $this->assertNull($arrayCol->dbTypecast(null)); + $this->assertEquals(new ArrayExpression([]), $arrayCol->dbTypecast('')); + $this->assertEquals(new ArrayExpression([1, 2, 3]), $arrayCol->dbTypecast(new ArrayIterator([1, 2, 3]))); + $this->assertSame($expression = new Expression('expression'), $arrayCol->dbTypecast($expression)); + } + + public function testArrayColumnPhpTypecast() + { + $arrayCol = new ArrayColumnSchema(); + + $this->assertNull($arrayCol->phpTypecast(null)); + $this->assertNull($arrayCol->phpTypecast(1)); + $this->assertSame([], $arrayCol->phpTypecast([])); + $this->assertSame(['1', '2', '3'], $arrayCol->phpTypecast(['1', '2', '3'])); + + $this->expectException(NotSupportedException::class); + $this->expectExceptionMessage( + 'Yiisoft\Db\Schema\Column\ArrayColumnSchema::getParser() is not supported. Use concrete DBMS implementation.' + ); + + $arrayCol->phpTypecast('{1,2,3}'); + } + + public function testStructuredColumnGetColumns(): void + { + $structuredCol = new StructuredColumnSchema(); + $columns = [ + 'value' => ColumnBuilder::money(), + 'currency_code' => ColumnBuilder::char(3), + ]; + + $this->assertSame([], $structuredCol->getColumns()); + $this->assertSame($structuredCol, $structuredCol->columns($columns)); + $this->assertSame($columns, $structuredCol->getColumns()); + } + + public function testStructuredColumnDbTypecast(): void + { + $structuredCol = new StructuredColumnSchema(); + $expression = new Expression('expression'); + $structuredExpression = new StructuredExpression(['value' => 1, 'currency_code' => 'USD']); + + $this->assertNull($structuredCol->dbTypecast(null)); + $this->assertSame($expression, $structuredCol->dbTypecast($expression)); + $this->assertSame($structuredExpression, $structuredCol->dbTypecast($structuredExpression)); + $this->assertEquals($structuredExpression, $structuredCol->dbTypecast(['value' => 1, 'currency_code' => 'USD'])); + } + + public function testStructuredColumnPhpTypecast(): void + { + $structuredCol = new StructuredColumnSchema(); + $columns = [ + 'int' => ColumnBuilder::integer(), + 'bool' => ColumnBuilder::boolean(), + ]; + + $this->assertNull($structuredCol->phpTypecast(null)); + $this->assertNull($structuredCol->phpTypecast(1)); + $this->assertSame( + ['int' => '1', 'bool' => '1'], + $structuredCol->phpTypecast(['int' => '1', 'bool' => '1']) + ); + + $structuredCol->columns($columns); + $this->assertSame( + ['int' => 1, 'bool' => true], + $structuredCol->phpTypecast(['int' => '1', 'bool' => '1']) + ); + + $this->expectException(NotSupportedException::class); + $this->expectExceptionMessage( + 'Yiisoft\Db\Schema\Column\StructuredColumnSchema::getParser() is not supported. Use concrete DBMS implementation.' + ); + + $structuredCol->phpTypecast('(1,true)'); + } } diff --git a/tests/Provider/ColumnBuilderProvider.php b/tests/Provider/ColumnBuilderProvider.php index ea4d997f9..0572bc219 100644 --- a/tests/Provider/ColumnBuilderProvider.php +++ b/tests/Provider/ColumnBuilderProvider.php @@ -5,13 +5,16 @@ namespace Yiisoft\Db\Tests\Provider; use Yiisoft\Db\Constant\ColumnType; +use Yiisoft\Db\Schema\Column\ArrayColumnSchema; use Yiisoft\Db\Schema\Column\BinaryColumnSchema; use Yiisoft\Db\Schema\Column\BitColumnSchema; use Yiisoft\Db\Schema\Column\BooleanColumnSchema; +use Yiisoft\Db\Schema\Column\ColumnBuilder; use Yiisoft\Db\Schema\Column\DoubleColumnSchema; use Yiisoft\Db\Schema\Column\IntegerColumnSchema; use Yiisoft\Db\Schema\Column\JsonColumnSchema; use Yiisoft\Db\Schema\Column\StringColumnSchema; +use Yiisoft\Db\Schema\Column\StructuredColumnSchema; class ColumnBuilderProvider { @@ -32,56 +35,76 @@ class ColumnBuilderProvider public static function buildingMethods(): array { + $column = ColumnBuilder::string(); + $columns = ['value' => ColumnBuilder::money(), 'currency_code' => ColumnBuilder::char(3)]; + return [ // building method, args, expected instance of, expected type, expected column method results - ['primaryKey', [], IntegerColumnSchema::class, ColumnType::INTEGER, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], - ['primaryKey', [false], IntegerColumnSchema::class, ColumnType::INTEGER, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], - ['smallPrimaryKey', [], IntegerColumnSchema::class, ColumnType::SMALLINT, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], - ['smallPrimaryKey', [false], IntegerColumnSchema::class, ColumnType::SMALLINT, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], - ['bigPrimaryKey', [], IntegerColumnSchema::class, ColumnType::BIGINT, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], - ['bigPrimaryKey', [false], IntegerColumnSchema::class, ColumnType::BIGINT, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], - ['uuidPrimaryKey', [], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], - ['uuidPrimaryKey', [true], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], - ['boolean', [], BooleanColumnSchema::class, ColumnType::BOOLEAN], - ['bit', [], BitColumnSchema::class, ColumnType::BIT], - ['bit', [1], BitColumnSchema::class, ColumnType::BIT, ['getSize' => 1]], - ['tinyint', [], IntegerColumnSchema::class, ColumnType::TINYINT], - ['tinyint', [1], IntegerColumnSchema::class, ColumnType::TINYINT, ['getSize' => 1]], - ['smallint', [], IntegerColumnSchema::class, ColumnType::SMALLINT], - ['smallint', [1], IntegerColumnSchema::class, ColumnType::SMALLINT, ['getSize' => 1]], - ['integer', [], IntegerColumnSchema::class, ColumnType::INTEGER], - ['integer', [1], IntegerColumnSchema::class, ColumnType::INTEGER, ['getSize' => 1]], - ['bigint', [], IntegerColumnSchema::class, ColumnType::BIGINT], - ['bigint', [1], IntegerColumnSchema::class, ColumnType::BIGINT, ['getSize' => 1]], - ['float', [], DoubleColumnSchema::class, ColumnType::FLOAT], - ['float', [8], DoubleColumnSchema::class, ColumnType::FLOAT, ['getSize' => 8]], - ['float', [8, 2], DoubleColumnSchema::class, ColumnType::FLOAT, ['getSize' => 8, 'getScale' => 2]], - ['double', [], DoubleColumnSchema::class, ColumnType::DOUBLE], - ['double', [8], DoubleColumnSchema::class, ColumnType::DOUBLE, ['getSize' => 8]], - ['double', [8, 2], DoubleColumnSchema::class, ColumnType::DOUBLE, ['getSize' => 8, 'getScale' => 2]], - ['decimal', [], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 10, 'getScale' => 0]], - ['decimal', [8], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 8, 'getScale' => 0]], - ['decimal', [8, 2], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 8, 'getScale' => 2]], - ['money', [], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 19, 'getScale' => 4]], - ['money', [8], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 8, 'getScale' => 4]], - ['money', [8, 2], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 8, 'getScale' => 2]], - ['char', [], StringColumnSchema::class, ColumnType::CHAR, ['getSize' => 1]], - ['char', [100], StringColumnSchema::class, ColumnType::CHAR, ['getSize' => 100]], - ['string', [], StringColumnSchema::class, ColumnType::STRING, ['getSize' => 255]], - ['string', [100], StringColumnSchema::class, ColumnType::STRING, ['getSize' => 100]], - ['text', [], StringColumnSchema::class, ColumnType::TEXT], - ['text', [5000], StringColumnSchema::class, ColumnType::TEXT, ['getSize' => 5000]], - ['binary', [], BinaryColumnSchema::class, ColumnType::BINARY], - ['binary', [8], BinaryColumnSchema::class, ColumnType::BINARY, ['getSize' => 8]], - ['uuid', [], StringColumnSchema::class, ColumnType::UUID], - ['datetime', [], StringColumnSchema::class, ColumnType::DATETIME, ['getSize' => 0]], - ['datetime', [3], StringColumnSchema::class, ColumnType::DATETIME, ['getSize' => 3]], - ['timestamp', [], StringColumnSchema::class, ColumnType::TIMESTAMP, ['getSize' => 0]], - ['timestamp', [3], StringColumnSchema::class, ColumnType::TIMESTAMP, ['getSize' => 3]], - ['date', [], StringColumnSchema::class, ColumnType::DATE], - ['time', [], StringColumnSchema::class, ColumnType::TIME, ['getSize' => 0]], - ['time', [3], StringColumnSchema::class, ColumnType::TIME, ['getSize' => 3]], - ['json', [], JsonColumnSchema::class, ColumnType::JSON], + 'primaryKey()' => ['primaryKey', [], IntegerColumnSchema::class, ColumnType::INTEGER, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], + 'primaryKey(false)' => ['primaryKey', [false], IntegerColumnSchema::class, ColumnType::INTEGER, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], + 'smallPrimaryKey()' => ['smallPrimaryKey', [], IntegerColumnSchema::class, ColumnType::SMALLINT, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], + 'smallPrimaryKey(false)' => ['smallPrimaryKey', [false], IntegerColumnSchema::class, ColumnType::SMALLINT, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], + 'bigPrimaryKey()' => ['bigPrimaryKey', [], IntegerColumnSchema::class, ColumnType::BIGINT, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], + 'bigPrimaryKey(false)' => ['bigPrimaryKey', [false], IntegerColumnSchema::class, ColumnType::BIGINT, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], + 'uuidPrimaryKey()' => ['uuidPrimaryKey', [], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], + 'uuidPrimaryKey(true)' => ['uuidPrimaryKey', [true], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], + 'boolean()' => ['boolean', [], BooleanColumnSchema::class, ColumnType::BOOLEAN], + 'bit()' => ['bit', [], BitColumnSchema::class, ColumnType::BIT], + 'bit(1)' => ['bit', [1], BitColumnSchema::class, ColumnType::BIT, ['getSize' => 1]], + 'tinyint()' => ['tinyint', [], IntegerColumnSchema::class, ColumnType::TINYINT], + 'tinyint(1)' => ['tinyint', [1], IntegerColumnSchema::class, ColumnType::TINYINT, ['getSize' => 1]], + 'smallint()' => ['smallint', [], IntegerColumnSchema::class, ColumnType::SMALLINT], + 'smallint(1)' => ['smallint', [1], IntegerColumnSchema::class, ColumnType::SMALLINT, ['getSize' => 1]], + 'integer()' => ['integer', [], IntegerColumnSchema::class, ColumnType::INTEGER], + 'integer(1)' => ['integer', [1], IntegerColumnSchema::class, ColumnType::INTEGER, ['getSize' => 1]], + 'bigint()' => ['bigint', [], IntegerColumnSchema::class, ColumnType::BIGINT], + 'bigint(1)' => ['bigint', [1], IntegerColumnSchema::class, ColumnType::BIGINT, ['getSize' => 1]], + 'float()' => ['float', [], DoubleColumnSchema::class, ColumnType::FLOAT], + 'float(8)' => ['float', [8], DoubleColumnSchema::class, ColumnType::FLOAT, ['getSize' => 8]], + 'float(8,2)' => ['float', [8, 2], DoubleColumnSchema::class, ColumnType::FLOAT, ['getSize' => 8, 'getScale' => 2]], + 'double()' => ['double', [], DoubleColumnSchema::class, ColumnType::DOUBLE], + 'double(8)' => ['double', [8], DoubleColumnSchema::class, ColumnType::DOUBLE, ['getSize' => 8]], + 'double(8,2)' => ['double', [8, 2], DoubleColumnSchema::class, ColumnType::DOUBLE, ['getSize' => 8, 'getScale' => 2]], + 'decimal()' => ['decimal', [], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 10, 'getScale' => 0]], + 'decimal(8)' => ['decimal', [8], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 8, 'getScale' => 0]], + 'decimal(8,2)' => ['decimal', [8, 2], DoubleColumnSchema::class, ColumnType::DECIMAL, ['getSize' => 8, 'getScale' => 2]], + 'money()' => ['money', [], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 19, 'getScale' => 4]], + 'money(8)' => ['money', [8], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 8, 'getScale' => 4]], + 'money(8,2)' => ['money', [8, 2], DoubleColumnSchema::class, ColumnType::MONEY, ['getSize' => 8, 'getScale' => 2]], + 'char()' => ['char', [], StringColumnSchema::class, ColumnType::CHAR, ['getSize' => 1]], + 'char(100)' => ['char', [100], StringColumnSchema::class, ColumnType::CHAR, ['getSize' => 100]], + 'string()' => ['string', [], StringColumnSchema::class, ColumnType::STRING, ['getSize' => 255]], + 'string(100)' => ['string', [100], StringColumnSchema::class, ColumnType::STRING, ['getSize' => 100]], + 'text()' => ['text', [], StringColumnSchema::class, ColumnType::TEXT], + 'text(5000)' => ['text', [5000], StringColumnSchema::class, ColumnType::TEXT, ['getSize' => 5000]], + 'binary()' => ['binary', [], BinaryColumnSchema::class, ColumnType::BINARY], + 'binary(8)' => ['binary', [8], BinaryColumnSchema::class, ColumnType::BINARY, ['getSize' => 8]], + 'uuid()' => ['uuid', [], StringColumnSchema::class, ColumnType::UUID], + 'datetime()' => ['datetime', [], StringColumnSchema::class, ColumnType::DATETIME, ['getSize' => 0]], + 'datetime(3)' => ['datetime', [3], StringColumnSchema::class, ColumnType::DATETIME, ['getSize' => 3]], + 'timestamp()' => ['timestamp', [], StringColumnSchema::class, ColumnType::TIMESTAMP, ['getSize' => 0]], + 'timestamp(3)' => ['timestamp', [3], StringColumnSchema::class, ColumnType::TIMESTAMP, ['getSize' => 3]], + 'date()' => ['date', [], StringColumnSchema::class, ColumnType::DATE], + 'time()' => ['time', [], StringColumnSchema::class, ColumnType::TIME, ['getSize' => 0]], + 'time(3)' => ['time', [3], StringColumnSchema::class, ColumnType::TIME, ['getSize' => 3]], + 'array()' => ['array', [], ArrayColumnSchema::class, ColumnType::ARRAY], + 'array($column)' => ['array', [$column], ArrayColumnSchema::class, ColumnType::ARRAY, ['getColumn' => $column]], + 'structured()' => ['structured', [], StructuredColumnSchema::class, ColumnType::STRUCTURED], + "structured('money_currency')" => [ + 'structured', + ['money_currency'], + StructuredColumnSchema::class, + ColumnType::STRUCTURED, + ['getDbType' => 'money_currency'], + ], + "structured('money_currency',\$columns)" => [ + 'structured', + ['money_currency', $columns], + StructuredColumnSchema::class, + ColumnType::STRUCTURED, + ['getDbType' => 'money_currency', 'getColumns' => $columns], + ], + 'json()' => ['json', [], JsonColumnSchema::class, ColumnType::JSON], ]; } } diff --git a/tests/Provider/ColumnFactoryProvider.php b/tests/Provider/ColumnFactoryProvider.php index cdb647b19..efd7106b7 100644 --- a/tests/Provider/ColumnFactoryProvider.php +++ b/tests/Provider/ColumnFactoryProvider.php @@ -13,6 +13,7 @@ use Yiisoft\Db\Schema\Column\IntegerColumnSchema; use Yiisoft\Db\Schema\Column\JsonColumnSchema; use Yiisoft\Db\Schema\Column\StringColumnSchema; +use Yiisoft\Db\Schema\Column\StructuredColumnSchema; class ColumnFactoryProvider { @@ -64,6 +65,7 @@ public static function types(): array 'timestamp' => [ColumnType::TIMESTAMP, ColumnType::TIMESTAMP, StringColumnSchema::class], 'time' => [ColumnType::TIME, ColumnType::TIME, StringColumnSchema::class], 'date' => [ColumnType::DATE, ColumnType::DATE, StringColumnSchema::class], + 'structured' => [ColumnType::STRUCTURED, ColumnType::STRUCTURED, StructuredColumnSchema::class], 'json' => [ColumnType::JSON, ColumnType::JSON, JsonColumnSchema::class], ]; } diff --git a/tests/Provider/ColumnSchemaProvider.php b/tests/Provider/ColumnSchemaProvider.php index 9bcbe4bca..6f00d19b2 100644 --- a/tests/Provider/ColumnSchemaProvider.php +++ b/tests/Provider/ColumnSchemaProvider.php @@ -12,14 +12,18 @@ use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Constant\PhpType; +use Yiisoft\Db\Expression\StructuredExpression; +use Yiisoft\Db\Schema\Column\ArrayColumnSchema; use Yiisoft\Db\Schema\Column\BigIntColumnSchema; use Yiisoft\Db\Schema\Column\BinaryColumnSchema; use Yiisoft\Db\Schema\Column\BitColumnSchema; use Yiisoft\Db\Schema\Column\BooleanColumnSchema; +use Yiisoft\Db\Schema\Column\ColumnBuilder; use Yiisoft\Db\Schema\Column\DoubleColumnSchema; use Yiisoft\Db\Schema\Column\IntegerColumnSchema; use Yiisoft\Db\Schema\Column\JsonColumnSchema; use Yiisoft\Db\Schema\Column\StringColumnSchema; +use Yiisoft\Db\Schema\Column\StructuredColumnSchema; use function fopen; @@ -36,6 +40,8 @@ public static function predefinedTypes(): array 'binary' => [BinaryColumnSchema::class, ColumnType::BINARY, PhpType::MIXED], 'bit' => [BitColumnSchema::class, ColumnType::BIT, PhpType::INT], 'boolean' => [BooleanColumnSchema::class, ColumnType::BOOLEAN, PhpType::BOOL], + 'array' => [ArrayColumnSchema::class, ColumnType::ARRAY, PhpType::ARRAY], + 'structured' => [StructuredColumnSchema::class, ColumnType::STRUCTURED, PhpType::ARRAY], 'json' => [JsonColumnSchema::class, ColumnType::JSON, PhpType::MIXED], ]; } @@ -245,6 +251,121 @@ public static function phpTypecastColumns(): array ]; } + public static function dbTypecastArrayColumns() + { + return [ + // [column, values] + ColumnType::BOOLEAN => [ + ColumnBuilder::boolean(), + [ + // [dimension, expected, typecast value] + [1, [true, true, true, false, false, false, null], [true, 1, '1', false, 0, '0', null]], + [2, [[true, true, true, false, false, false, null]], [[true, 1, '1', false, 0, '0', null]]], + ], + ], + ColumnType::BIT => [ + ColumnBuilder::bit(), + [ + [1, [0b1011, 1001, null], [0b1011, '1001', null]], + [2, [[0b1011, 1001, null]], [[0b1011, '1001', null]]], + ], + ], + ColumnType::INTEGER => [ + ColumnBuilder::integer(), + [ + [1, [1, 2, 3, null], [1, 2.0, '3', null]], + [2, [[1, 2], [3], null], [[1, 2.0], ['3'], null]], + [2, [null, null], [null, null]], + ], + ], + ColumnType::BIGINT => [ + new BigIntColumnSchema(), + [ + [1, ['1', '2', '3', '9223372036854775807'], [1, 2.0, '3', '9223372036854775807']], + [2, [['1', '2'], ['3'], ['9223372036854775807']], [[1, 2.0], ['3'], ['9223372036854775807']]], + ], + ], + ColumnType::DOUBLE => [ + ColumnBuilder::double(), + [ + [1, [1.0, 2.2, 3.3, null], [1, 2.2, '3.3', null]], + [2, [[1.0, 2.2], [3.3, null]], [[1, 2.2], ['3.3', null]]], + ], + ], + ColumnType::STRING => [ + ColumnBuilder::string(), + [ + [1, ['1', '2', '1', '0', '', null], [1, '2', true, false, '', null]], + [2, [['1', '2', '1', '0'], [''], [null]], [[1, '2', true, false], [''], [null]]], + ], + ], + ColumnType::BINARY => [ + ColumnBuilder::binary(), + [ + [1, [ + '1', + new Param("\x10", PDO::PARAM_LOB), + $resource = fopen('php://memory', 'rb'), + null, + ], [1, "\x10", $resource, null]], + [2, [[ + '1', + new Param("\x10", PDO::PARAM_LOB), + $resource = fopen('php://memory', 'rb'), + null, + ]], [[1, "\x10", $resource, null]]], + ], + ], + ColumnType::JSON => [ + ColumnBuilder::json(), + [ + [1, [ + new JsonExpression([1, 2, 3]), + new JsonExpression(['key' => 'value']), + new JsonExpression(['key' => 'value']), + null, + ], [[1, 2, 3], ['key' => 'value'], new JsonExpression(['key' => 'value']), null]], + [2, [ + [ + new JsonExpression([1, 2, 3]), + new JsonExpression(['key' => 'value']), + new JsonExpression(['key' => 'value']), + null, + ], + null, + ], [[[1, 2, 3], ['key' => 'value'], new JsonExpression(['key' => 'value']), null], null]], + ], + ], + ColumnType::STRUCTURED => [ + ColumnBuilder::structured('structured_type'), + [ + [ + 1, + [ + new StructuredExpression(['value' => 10, 'currency' => 'USD'], 'structured_type'), + null, + ], + [ + ['value' => 10, 'currency' => 'USD'], + null, + ], + ], + [ + 2, + [[ + new StructuredExpression(['value' => 10, 'currency' => 'USD'], 'structured_type'), + null, + ]], + [[ + ['value' => 10, 'currency' => 'USD'], + null, + ]], + ], + ], + ], + ]; + } + public static function load(): array { return [ diff --git a/tests/Provider/StructuredTypeProvider.php b/tests/Provider/StructuredTypeProvider.php new file mode 100644 index 000000000..3123897cb --- /dev/null +++ b/tests/Provider/StructuredTypeProvider.php @@ -0,0 +1,68 @@ + ColumnBuilder::money(10, 2)->defaultValue(5.0), + 'currency_code' => ColumnBuilder::char(3)->defaultValue('USD'), + ]; + + return [ + 'Sort according to `$columns` order' => [ + ['currency_code' => 'USD', 'value' => 10.0], + ['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'], + $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 an iterable object' => [ + new TraversableObject([10.0]), + ['value' => 10.0, 'currency_code' => 'USD'], + $price5UsdColumns, + ], + 'Fill default values for an object' => [ + (object) ['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 expressions' => [ + $expression = new Expression('(5,USD)'), + $expression, + $price5UsdColumns, + ], + 'Do not normalize with empty columns' => [ + [10.0], + [10.0], + [], + ], + ]; + } +} From 05d359c8bfb04cad6723964dce35dd591b835263 Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Mon, 14 Oct 2024 02:31:00 +0700 Subject: [PATCH 3/8] Fix MSSQL tests (#886) --- .github/workflows/active-record.yml | 7 ++++++- .github/workflows/db-mssql.yml | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/active-record.yml b/.github/workflows/active-record.yml index f4ed03395..0886fc101 100644 --- a/.github/workflows/active-record.yml +++ b/.github/workflows/active-record.yml @@ -20,7 +20,7 @@ jobs: env: COMPOSER_ROOT_VERSION: dev-master - EXTENSIONS: pdo, pdo_mysql, pdo_oci, pdo_pgsql, pdo_sqlite, pdo_sqlsrv-5.10.1 + EXTENSIONS: pdo, pdo_mysql, pdo_oci, pdo_pgsql, pdo_sqlite, pdo_sqlsrv-5.12 runs-on: ${{ matrix.os }} @@ -77,6 +77,11 @@ jobs: options: --name=mssql --health-cmd="/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1'" --health-interval=10s --health-timeout=5s --health-retries=3 steps: + - name: Install ODBC driver. + run: | + sudo curl https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list + sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 + - name: Checkout. uses: actions/checkout@v3 diff --git a/.github/workflows/db-mssql.yml b/.github/workflows/db-mssql.yml index ad0c8cbe6..07bbd4f1b 100644 --- a/.github/workflows/db-mssql.yml +++ b/.github/workflows/db-mssql.yml @@ -25,7 +25,7 @@ jobs: env: COMPOSER_ROOT_VERSION: dev-master CURRENT_PACKAGE: db-mssql - EXTENSIONS: pdo, pdo_sqlsrv-5.10.1 + EXTENSIONS: pdo, pdo_sqlsrv-5.12 runs-on: ${{ matrix.mssql.os || 'ubuntu-latest' }} @@ -64,6 +64,11 @@ jobs: options: --name=mssql --health-cmd="/opt/mssql-tools${{ matrix.mssql.odbc-version }}/bin/sqlcmd ${{ matrix.mssql.flag }} -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1'" --health-interval=10s --health-timeout=5s --health-retries=3 steps: + - name: Install ODBC driver. + run: | + sudo curl https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list + sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 + - name: Checkout. uses: actions/checkout@v3 From b9c2ce90f77cdc39e941bb0aeece5dccabfd6029 Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Mon, 14 Oct 2024 14:49:30 +0700 Subject: [PATCH 4/8] Refactor `AbstractDsn` class (#885) --- CHANGELOG.md | 1 + src/Connection/AbstractDsn.php | 38 +++++++++++++++++----------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcbc294f5..c7e82cd77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - Enh #878: Realize `ColumnBuilder` class (@Tigrov) - Enh #881: Refactor `ColumnSchemaInterface` and `AbstractColumnSchema` (@Tigrov) - End #882: Move `ArrayColumnSchema` and `StructuredColumnSchema` classes from `db-pgsql` package (@Tigrov) +- Enh #885: Refactor `AbstractDsn` class (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/src/Connection/AbstractDsn.php b/src/Connection/AbstractDsn.php index f8895c097..8689a04f2 100644 --- a/src/Connection/AbstractDsn.php +++ b/src/Connection/AbstractDsn.php @@ -6,8 +6,6 @@ use Stringable; -use function implode; - /** * It's typically used to parse a DSN string, which is a string that has all the necessary information to connect * to a database, such as the database driver, hostname, database name, port, and options. @@ -17,37 +15,37 @@ abstract class AbstractDsn implements DsnInterface, Stringable { /** - * @psalm-param string[] $options + * @param string $driver The database driver name. + * @param string $host The database host name or IP address. + * @param string|null $databaseName The database name to connect to. + * @param string|null $port The database port. Null if isn't set. + * @param string[] $options The database connection options. Default value to an empty array. + * + * @psalm-param array $options */ public function __construct( - private string $driver, - private string $host, - private string|null $databaseName = null, - private string|null $port = null, - private array $options = [] + private readonly string $driver, + private readonly string $host, + private readonly string|null $databaseName = null, + private readonly string|null $port = null, + private readonly array $options = [] ) { } public function asString(): string { - $dsn = "$this->driver:" . "host=$this->host"; + $dsn = "$this->driver:host=$this->host"; if ($this->databaseName !== null && $this->databaseName !== '') { - $dsn .= ';' . "dbname=$this->databaseName"; + $dsn .= ";dbname=$this->databaseName"; } if ($this->port !== null) { - $dsn .= ';' . "port=$this->port"; + $dsn .= ";port=$this->port"; } - $parts = []; - foreach ($this->options as $key => $value) { - $parts[] = "$key=$value"; - } - - if (!empty($parts)) { - $dsn .= ';' . implode(';', $parts); + $dsn .= ";$key=$value"; } return $dsn; @@ -86,7 +84,9 @@ public function getHost(): string } /** - * @return array The database connection options. Default value to an empty array. + * @return string[] The database connection options. Default value to an empty array. + * + * @psalm-return array */ public function getOptions(): array { From 8abb33afdff026ba19a8b130eb6062553e0680d2 Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Tue, 15 Oct 2024 13:11:02 +0700 Subject: [PATCH 5/8] Add `ColumnDefinitionBuilder` (#883) --- CHANGELOG.md | 11 +- UPGRADE.md | 3 +- src/Connection/ConnectionInterface.php | 6 - src/Debug/ConnectionInterfaceProxy.php | 6 - .../AbstractColumnDefinitionBuilder.php | 329 ++++++++++++++++++ src/QueryBuilder/AbstractDDLQueryBuilder.php | 3 + src/QueryBuilder/AbstractQueryBuilder.php | 15 +- .../ColumnDefinitionBuilderInterface.php | 19 + src/QueryBuilder/QueryBuilderInterface.php | 13 + src/Schema/Column/AbstractColumnFactory.php | 2 +- src/Schema/Column/ColumnBuilder.php | 4 +- src/Schema/SchemaInterface.php | 8 + tests/AbstractColumnFactoryTest.php | 10 +- tests/AbstractQueryBuilderTest.php | 10 + tests/Common/CommonColumnSchemaTest.php | 2 +- tests/Common/CommonPdoConnectionTest.php | 2 - tests/Db/Schema/ColumnSchemaTest.php | 2 +- tests/Provider/ColumnBuilderProvider.php | 4 +- tests/Provider/ColumnFactoryProvider.php | 2 +- tests/Provider/QueryBuilderProvider.php | 131 +++++++ .../Support/Stub/ColumnDefinitionBuilder.php | 69 ++++ tests/Support/Stub/ColumnFactory.php | 3 +- tests/Support/Stub/Connection.php | 6 - tests/Support/Stub/QueryBuilder.php | 5 +- tests/Support/Stub/Schema.php | 6 + 25 files changed, 629 insertions(+), 42 deletions(-) create mode 100644 src/QueryBuilder/AbstractColumnDefinitionBuilder.php create mode 100644 src/QueryBuilder/ColumnDefinitionBuilderInterface.php create mode 100644 tests/Support/Stub/ColumnDefinitionBuilder.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c7e82cd77..cd645674b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - Enh #806: Build `Expression` instances inside `Expression::$params` when build a query using `QueryBuilder` (@Tigrov) - Enh #766: Allow `ColumnInterface` as column type. (@Tigrov) - Bug #828: Fix `float` type when use `AbstractCommand::getRawSql()` method (@Tigrov) -- Enh #752: Implement `ColumnSchemaInterface` classes according to the data type of database table columns +- New #752: Implement `ColumnSchemaInterface` classes according to the data type of database table columns for type casting performance (@Tigrov) - Enh #829: Rename `batchInsert()` to `insertBatch()` in `DMLQueryBuilderInterface` and `CommandInterface` and change parameters from `$table, $columns, $rows` to `$table, $rows, $columns = []` (@Tigrov) @@ -29,18 +29,19 @@ - Chg #846: Remove `SchemaInterface::isReadQuery()` and `AbstractSchema::isReadQuery()` methods (@Tigrov) - Chg #847: Remove `SchemaInterface::getRawTableName()` and `AbstractSchema::getRawTableName()` methods (@Tigrov) - Enh #852: Add method chaining for column classes (@Tigrov) -- Enh #855: Add array and JSON overlaps conditions (@Tigrov) -- Enh #860: Add `bit` abstract type (@Tigrov) +- New #855: Add array and JSON overlaps conditions (@Tigrov) +- New #860: Add `bit` abstract type (@Tigrov) - Enh #862: Refactor PHP type of `ColumnSchemaInterface` instances (@Tigrov) - Enh #865: Raise minimum PHP version to `^8.1` with minor refactoring (@Tigrov, @vjik) - Enh #798: Allow `QueryInterface::one()` and `QueryInterface::all()` to return objects (@darkdef, @Tigrov) - Enh #872: Use `#[\SensitiveParameter]` attribute to mark sensitive parameters (@heap-s) -- Enh #864: Realize column factory (@Tigrov) +- New #864: Realize column factory (@Tigrov) - Enh #875: Ignore "Packets out of order..." warnings in `AbstractPdoCommand::internalExecute()` method (@Tigrov) - Enh #877: Separate column type constants (@Tigrov) - Enh #878: Realize `ColumnBuilder` class (@Tigrov) - Enh #881: Refactor `ColumnSchemaInterface` and `AbstractColumnSchema` (@Tigrov) -- End #882: Move `ArrayColumnSchema` and `StructuredColumnSchema` classes from `db-pgsql` package (@Tigrov) +- New #882: Move `ArrayColumnSchema` and `StructuredColumnSchema` classes from `db-pgsql` package (@Tigrov) +- New #883: Add `ColumnDefinitionBuilder` class and `QueryBuilderInterface::buildColumnDefinition()` method (@Tigrov) - Enh #885: Refactor `AbstractDsn` class (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/UPGRADE.md b/UPGRADE.md index cb859fd69..42496e1f1 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -104,7 +104,8 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace ### New methods - `QuoterInterface::getRawTableName()` - returns the raw table name without quotes; -- `ConnectionInterface::getColumnFactory()` - returns the column factory object for concrete DBMS. +- `SchemaInterface::getColumnFactory()` - returns the column factory object for concrete DBMS; +- `QueryBuilderInterface::buildColumnDefinition()` - builds column definition for `CREATE TABLE` statement. ### Remove methods diff --git a/src/Connection/ConnectionInterface.php b/src/Connection/ConnectionInterface.php index 55e24a83c..a8aad3d1c 100644 --- a/src/Connection/ConnectionInterface.php +++ b/src/Connection/ConnectionInterface.php @@ -14,7 +14,6 @@ use Yiisoft\Db\Query\BatchQueryResultInterface; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; -use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Schema\TableSchemaInterface; @@ -86,11 +85,6 @@ public function createTransaction(): TransactionInterface; */ public function close(): void; - /** - * Returns the column factory for creating column instances. - */ - public function getColumnFactory(): ColumnFactoryInterface; - /** * Returns the name of the DB driver for the current `dsn`. * diff --git a/src/Debug/ConnectionInterfaceProxy.php b/src/Debug/ConnectionInterfaceProxy.php index 802392be8..3944c3f6f 100644 --- a/src/Debug/ConnectionInterfaceProxy.php +++ b/src/Debug/ConnectionInterfaceProxy.php @@ -10,7 +10,6 @@ use Yiisoft\Db\Query\BatchQueryResultInterface; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; -use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Schema\TableSchemaInterface; @@ -63,11 +62,6 @@ public function close(): void $this->connection->close(); } - public function getColumnFactory(): ColumnFactoryInterface - { - return $this->connection->getColumnFactory(); - } - public function getLastInsertID(string $sequenceName = null): string { return $this->connection->getLastInsertID($sequenceName); diff --git a/src/QueryBuilder/AbstractColumnDefinitionBuilder.php b/src/QueryBuilder/AbstractColumnDefinitionBuilder.php new file mode 100644 index 000000000..6682de87d --- /dev/null +++ b/src/QueryBuilder/AbstractColumnDefinitionBuilder.php @@ -0,0 +1,329 @@ +buildType($column) + . $this->buildUnsigned($column) + . $this->buildNotNull($column) + . $this->buildPrimaryKey($column) + . $this->buildAutoIncrement($column) + . $this->buildUnique($column) + . $this->buildDefault($column) + . $this->buildComment($column) + . $this->buildCheck($column) + . $this->buildReferences($column) + . $this->buildExtra($column); + } + + /** + * Builds the auto increment clause for the column. + * + * @return string A string containing the {@see AUTO_INCREMENT_KEYWORD} keyword. + */ + protected function buildAutoIncrement(ColumnSchemaInterface $column): string + { + if (empty(static::AUTO_INCREMENT_KEYWORD) || !$column->isAutoIncrement()) { + return ''; + } + + return match ($column->getType()) { + ColumnType::TINYINT, + ColumnType::SMALLINT, + ColumnType::INTEGER, + ColumnType::BIGINT => ' ' . static::AUTO_INCREMENT_KEYWORD, + default => '', + }; + } + + /** + * Builds the check constraint for the column. + * + * @return string A string containing the CHECK constraint. + */ + protected function buildCheck(ColumnSchemaInterface $column): string + { + $check = $column->getCheck(); + + return !empty($check) ? " CHECK ($check)" : ''; + } + + /** + * Builds the comment clause for the column. Default is empty string. + * + * @return string A string containing the COMMENT keyword and the comment itself. + */ + protected function buildComment(ColumnSchemaInterface $column): string + { + return ''; + } + + /** + * Builds the default value specification for the column. + * + * @return string A string containing the DEFAULT keyword and the default value. + */ + protected function buildDefault(ColumnSchemaInterface $column): string + { + if (!empty(static::GENERATE_UUID_EXPRESSION) + && $column->getType() === ColumnType::UUID + && $column->isAutoIncrement() + && $column->getDefaultValue() === null + ) { + return ' DEFAULT ' . static::GENERATE_UUID_EXPRESSION; + } + + if ($column->isAutoIncrement() && $column->getType() !== ColumnType::UUID + || $column->getDefaultValue() === null + ) { + return ''; + } + + $defaultValue = $this->buildDefaultValue($column); + + if ($defaultValue === null) { + return ''; + } + + return " DEFAULT $defaultValue"; + } + + /** + * Return the default value for the column. + * + * @return string|null string with default value of column. + */ + protected function buildDefaultValue(ColumnSchemaInterface $column): string|null + { + $value = $column->dbTypecast($column->getDefaultValue()); + + if ($value === null) { + return null; + } + + if ($value instanceof ExpressionInterface) { + return $this->queryBuilder->buildExpression($value); + } + + /** @var string */ + return match (gettype($value)) { + GettypeResult::INTEGER => (string) $value, + GettypeResult::DOUBLE => (string) $value, + GettypeResult::BOOLEAN => $value ? 'TRUE' : 'FALSE', + default => $this->queryBuilder->quoter()->quoteValue((string) $value), + }; + } + + /** + * Builds the custom string that's appended to column definition. + * + * @return string A string containing the custom SQL fragment appended to column definition. + */ + protected function buildExtra(ColumnSchemaInterface $column): string + { + $extra = $column->getExtra(); + + return !empty($extra) ? " $extra" : ''; + } + + /** + * Builds the not null constraint for the column. + * + * @return string A string 'NOT NULL' if {@see ColumnSchemaInterface::isNotNull()} is `true` + * or an empty string otherwise. + */ + protected function buildNotNull(ColumnSchemaInterface $column): string + { + return $column->isNotNull() ? ' NOT NULL' : ''; + } + + /** + * Builds the primary key clause for column. + * + * @return string A string containing the PRIMARY KEY keyword. + */ + protected function buildPrimaryKey(ColumnSchemaInterface $column): string + { + return $column->isPrimaryKey() ? ' PRIMARY KEY' : ''; + } + + /** + * Builds the references clause for the column. + */ + protected function buildReferences(ColumnSchemaInterface $column): string + { + $reference = $this->buildReferenceDefinition($column); + + if ($reference === null) { + return ''; + } + + return " REFERENCES $reference"; + } + + /** + * Builds the reference definition for the column. + */ + protected function buildReferenceDefinition(ColumnSchemaInterface $column): string|null + { + $reference = $column->getReference(); + $table = $reference?->getForeignTableName(); + + if ($table === null) { + return null; + } + + $quoter = $this->queryBuilder->quoter(); + $schema = $reference?->getForeignSchemaName(); + + $sql = $quoter->quoteTableName($table); + + if ($schema !== null) { + $sql = $quoter->quoteTableName($schema) . '.' . $sql; + } + + $columns = $reference?->getForeignColumnNames(); + + if (!empty($columns)) { + $sql .= ' (' . $this->queryBuilder->buildColumns($columns) . ')'; + } + + if (null !== $onDelete = $reference?->getOnDelete()) { + $sql .= $this->buildOnDelete($onDelete); + } + + if (null !== $onUpdate = $reference?->getOnUpdate()) { + $sql .= $this->buildOnUpdate($onUpdate); + } + + return $sql; + } + + /** + * Builds the ON DELETE clause for the column reference. + */ + protected function buildOnDelete(string $onDelete): string + { + return " ON DELETE $onDelete"; + } + + /** + * Builds the ON UPDATE clause for the column reference. + */ + protected function buildOnUpdate(string $onUpdate): string + { + return " ON UPDATE $onUpdate"; + } + + /** + * Builds the type definition for the column. For example: `varchar(128)` or `decimal(10,2)`. + * + * @return string A string containing the column type definition. + */ + protected function buildType(ColumnSchemaInterface $column): string + { + $dbType = $column->getDbType(); + + if ($dbType === null) { + $dbType = $this->getDbType($column); + } + + if (empty($dbType) + || $dbType[-1] === ')' + || !in_array(strtolower($dbType), static::TYPES_WITH_SIZE, true) + ) { + return $dbType; + } + + $size = $column->getSize(); + + if ($size === null) { + return $dbType; + } + + $scale = $column->getScale(); + + if ($scale === null || !in_array(strtolower($dbType), static::TYPES_WITH_SCALE, true)) { + return "$dbType($size)"; + } + + return "$dbType($size,$scale)"; + } + + /** + * Builds the unique constraint for the column. + * + * @return string A string 'UNIQUE' if {@see ColumnSchemaInterface::isUnique()} is true + * or an empty string otherwise. + */ + protected function buildUnique(ColumnSchemaInterface $column): string + { + if ($column->isPrimaryKey()) { + return ''; + } + + return $column->isUnique() ? ' UNIQUE' : ''; + } + + /** + * Builds the unsigned string for the column. + * + * @return string A string containing the UNSIGNED keyword. + */ + protected function buildUnsigned(ColumnSchemaInterface $column): string + { + return $column->isUnsigned() ? ' UNSIGNED' : ''; + } +} diff --git a/src/QueryBuilder/AbstractDDLQueryBuilder.php b/src/QueryBuilder/AbstractDDLQueryBuilder.php index ce387a8ae..7365d5967 100644 --- a/src/QueryBuilder/AbstractDDLQueryBuilder.php +++ b/src/QueryBuilder/AbstractDDLQueryBuilder.php @@ -41,6 +41,7 @@ public function addCheck(string $table, string $name, string $expression): strin public function addColumn(string $table, string $column, ColumnInterface|string $type): string { + /** @psalm-suppress DeprecatedMethod */ return 'ALTER TABLE ' . $this->quoter->quoteTableName($table) . ' ADD ' @@ -138,6 +139,7 @@ public function alterColumn( string $column, ColumnInterface|string $type ): string { + /** @psalm-suppress DeprecatedMethod */ return 'ALTER TABLE ' . $this->quoter->quoteTableName($table) . ' CHANGE ' @@ -171,6 +173,7 @@ public function createTable(string $table, array $columns, string $options = nul foreach ($columns as $name => $type) { if (is_string($name)) { + /** @psalm-suppress DeprecatedMethod */ $cols[] = "\t" . $this->quoter->quoteColumnName($name) . ' ' diff --git a/src/QueryBuilder/AbstractQueryBuilder.php b/src/QueryBuilder/AbstractQueryBuilder.php index 4123e7cc0..a8a4a9703 100644 --- a/src/QueryBuilder/AbstractQueryBuilder.php +++ b/src/QueryBuilder/AbstractQueryBuilder.php @@ -10,10 +10,12 @@ use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\Condition\Interface\ConditionInterface; use Yiisoft\Db\Schema\Builder\ColumnInterface; +use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Schema\SchemaInterface; use function count; +use function is_string; use function preg_match; use function preg_replace; @@ -48,7 +50,8 @@ public function __construct( private SchemaInterface $schema, private AbstractDDLQueryBuilder $ddlBuilder, private AbstractDMLQueryBuilder $dmlBuilder, - private AbstractDQLQueryBuilder $dqlBuilder + private AbstractDQLQueryBuilder $dqlBuilder, + private AbstractColumnDefinitionBuilder $columnDefinitionBuilder, ) { } @@ -151,6 +154,15 @@ public function build(QueryInterface $query, array $params = []): array return $this->dqlBuilder->build($query, $params); } + public function buildColumnDefinition(ColumnSchemaInterface|string $column): string + { + if (is_string($column)) { + $column = $this->schema->getColumnFactory()->fromDefinition($column); + } + + return $this->columnDefinitionBuilder->build($column); + } + public function buildColumns(array|string $columns): string { return $this->dqlBuilder->buildColumns($columns); @@ -322,6 +334,7 @@ public function dropView(string $viewName): string return $this->ddlBuilder->dropView($viewName); } + /** @deprecated Use {@see buildColumnDefinition()}. Will be removed in version 2.0. */ public function getColumnType(ColumnInterface|string $type): string { if ($type instanceof ColumnInterface) { diff --git a/src/QueryBuilder/ColumnDefinitionBuilderInterface.php b/src/QueryBuilder/ColumnDefinitionBuilderInterface.php new file mode 100644 index 000000000..332613149 --- /dev/null +++ b/src/QueryBuilder/ColumnDefinitionBuilderInterface.php @@ -0,0 +1,19 @@ + ColumnBuilder::bigPrimaryKey()->load($info), PseudoType::UBIGPK => ColumnBuilder::bigPrimaryKey()->unsigned()->load($info), PseudoType::UUID_PK => ColumnBuilder::uuidPrimaryKey()->load($info), - PseudoType::UUID_PK_SEQ => ColumnBuilder::uuidPrimaryKey(true)->load($info), + PseudoType::UUID_PK_SEQ => ColumnBuilder::uuidPrimaryKey()->load($info), }; } diff --git a/src/Schema/Column/ColumnBuilder.php b/src/Schema/Column/ColumnBuilder.php index 6dfb22722..9160fffd3 100644 --- a/src/Schema/Column/ColumnBuilder.php +++ b/src/Schema/Column/ColumnBuilder.php @@ -47,7 +47,7 @@ public static function bigPrimaryKey(bool $autoIncrement = true): ColumnSchemaIn /** * Builds a column as an `uuid` primary key. */ - public static function uuidPrimaryKey(bool $autoIncrement = false): ColumnSchemaInterface + public static function uuidPrimaryKey(bool $autoIncrement = true): ColumnSchemaInterface { return static::uuid() ->primaryKey() @@ -65,6 +65,8 @@ public static function boolean(): ColumnSchemaInterface /** * Builds a column with the abstract type `bit`. + * + * @param int|null $size The number of bits that the column can store. */ public static function bit(int|null $size = null): ColumnSchemaInterface { diff --git a/src/Schema/SchemaInterface.php b/src/Schema/SchemaInterface.php index 72a40aa47..3e774b8a8 100644 --- a/src/Schema/SchemaInterface.php +++ b/src/Schema/SchemaInterface.php @@ -13,6 +13,7 @@ use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Schema\Builder\ColumnInterface; +use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; /** * Represents the schema for a database table. @@ -279,9 +280,16 @@ interface SchemaInterface extends ConstraintSchemaInterface /** * @psalm-param string[]|int[]|int|string|null $length + * + * @deprecated Use {@see ColumnBuilder} instead. Will be removed in 2.0. */ public function createColumn(string $type, array|int|string $length = null): ColumnInterface; + /** + * Returns the column factory for creating column instances. + */ + public function getColumnFactory(): ColumnFactoryInterface; + /** * @return string|null The default schema name. */ diff --git a/tests/AbstractColumnFactoryTest.php b/tests/AbstractColumnFactoryTest.php index cce700a6a..908f4b88e 100644 --- a/tests/AbstractColumnFactoryTest.php +++ b/tests/AbstractColumnFactoryTest.php @@ -17,7 +17,7 @@ abstract class AbstractColumnFactoryTest extends TestCase public function testFromDbType(string $dbType, string $expectedType, string $expectedInstanceOf): void { $db = $this->getConnection(); - $columnFactory = $db->getColumnFactory(); + $columnFactory = $db->getSchema()->getColumnFactory(); $column = $columnFactory->fromDbType($dbType); @@ -38,7 +38,7 @@ public function testFromDefinition( array $expectedMethodResults = [] ): void { $db = $this->getConnection(); - $columnFactory = $db->getColumnFactory(); + $columnFactory = $db->getSchema()->getColumnFactory(); $column = $columnFactory->fromDefinition($definition); @@ -65,7 +65,7 @@ public function testFromPseudoType( array $expectedMethodResults = [] ): void { $db = $this->getConnection(); - $columnFactory = $db->getColumnFactory(); + $columnFactory = $db->getSchema()->getColumnFactory(); $column = $columnFactory->fromPseudoType($pseudoType); @@ -88,7 +88,7 @@ public function testFromPseudoType( public function testFromType(string $type, string $expectedType, string $expectedInstanceOf): void { $db = $this->getConnection(); - $columnFactory = $db->getColumnFactory(); + $columnFactory = $db->getSchema()->getColumnFactory(); $column = $columnFactory->fromType($type); @@ -101,7 +101,7 @@ public function testFromType(string $type, string $expectedType, string $expecte public function testFromDefinitionWithExtra(): void { $db = $this->getConnection(); - $columnFactory = $db->getColumnFactory(); + $columnFactory = $db->getSchema()->getColumnFactory(); $column = $columnFactory->fromDefinition('char(1) NOT NULL', ['extra' => 'UNIQUE']); diff --git a/tests/AbstractQueryBuilderTest.php b/tests/AbstractQueryBuilderTest.php index c1fc66be9..91c73468c 100644 --- a/tests/AbstractQueryBuilderTest.php +++ b/tests/AbstractQueryBuilderTest.php @@ -26,6 +26,7 @@ use Yiisoft\Db\QueryBuilder\Condition\JsonOverlapsCondition; use Yiisoft\Db\QueryBuilder\Condition\SimpleCondition; use Yiisoft\Db\Schema\Builder\ColumnInterface; +use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Tests\Support\Assert; use Yiisoft\Db\Tests\Support\DbHelper; @@ -2402,4 +2403,13 @@ public function testOverrideParameters2(): void $command->getRawSql() ); } + + /** @dataProvider \Yiisoft\Db\Tests\Provider\QueryBuilderProvider::buildColumnDefinition() */ + public function testBuildColumnDefinition(string $expected, ColumnSchemaInterface|string $column): void + { + $db = $this->getConnection(); + $qb = $db->getQueryBuilder(); + + $this->assertSame($expected, $qb->buildColumnDefinition($column)); + } } diff --git a/tests/Common/CommonColumnSchemaTest.php b/tests/Common/CommonColumnSchemaTest.php index c9ffc454c..274f1b66b 100644 --- a/tests/Common/CommonColumnSchemaTest.php +++ b/tests/Common/CommonColumnSchemaTest.php @@ -36,7 +36,7 @@ public function testDbTypecastColumns(string $className, array $values) /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnSchemaProvider::phpTypecastColumns */ public function testPhpTypecastColumns(string $className, array $values) { - $column = new $className('column_name'); + $column = new $className(); foreach ($values as [$expected, $value]) { $this->assertSame($expected, $column->phpTypecast($value)); diff --git a/tests/Common/CommonPdoConnectionTest.php b/tests/Common/CommonPdoConnectionTest.php index 756616e9c..88b101987 100644 --- a/tests/Common/CommonPdoConnectionTest.php +++ b/tests/Common/CommonPdoConnectionTest.php @@ -311,7 +311,6 @@ public function testTransactionRollbackTransactionOnLevel(): void 'createTransaction', 'createCommand', 'close', - 'getColumnFactory', 'getDriverName', 'getLastInsertID', 'getQueryBuilder', @@ -346,7 +345,6 @@ public function testGetActivePdo(): void $db = $this->getMockBuilder(AbstractPdoConnection::class)->onlyMethods([ 'createCommand', 'createTransaction', - 'getColumnFactory', 'getPdo', 'getQueryBuilder', 'getQuoter', diff --git a/tests/Db/Schema/ColumnSchemaTest.php b/tests/Db/Schema/ColumnSchemaTest.php index e76a1fef7..449646517 100644 --- a/tests/Db/Schema/ColumnSchemaTest.php +++ b/tests/Db/Schema/ColumnSchemaTest.php @@ -307,7 +307,7 @@ public function testUnsigned(): void $this->assertFalse($column->isUnsigned()); - $column->unsigned(true); + $column->unsigned(); $this->assertTrue($column->isUnsigned()); } diff --git a/tests/Provider/ColumnBuilderProvider.php b/tests/Provider/ColumnBuilderProvider.php index 0572bc219..b147df30d 100644 --- a/tests/Provider/ColumnBuilderProvider.php +++ b/tests/Provider/ColumnBuilderProvider.php @@ -46,8 +46,8 @@ public static function buildingMethods(): array 'smallPrimaryKey(false)' => ['smallPrimaryKey', [false], IntegerColumnSchema::class, ColumnType::SMALLINT, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], 'bigPrimaryKey()' => ['bigPrimaryKey', [], IntegerColumnSchema::class, ColumnType::BIGINT, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], 'bigPrimaryKey(false)' => ['bigPrimaryKey', [false], IntegerColumnSchema::class, ColumnType::BIGINT, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], - 'uuidPrimaryKey()' => ['uuidPrimaryKey', [], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], - 'uuidPrimaryKey(true)' => ['uuidPrimaryKey', [true], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], + 'uuidPrimaryKey()' => ['uuidPrimaryKey', [], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], + 'uuidPrimaryKey(false)' => ['uuidPrimaryKey', [false], StringColumnSchema::class, ColumnType::UUID, ['isPrimaryKey' => true, 'isAutoIncrement' => false]], 'boolean()' => ['boolean', [], BooleanColumnSchema::class, ColumnType::BOOLEAN], 'bit()' => ['bit', [], BitColumnSchema::class, ColumnType::BIT], 'bit(1)' => ['bit', [1], BitColumnSchema::class, ColumnType::BIT, ['getSize' => 1]], diff --git a/tests/Provider/ColumnFactoryProvider.php b/tests/Provider/ColumnFactoryProvider.php index efd7106b7..7b7f7f778 100644 --- a/tests/Provider/ColumnFactoryProvider.php +++ b/tests/Provider/ColumnFactoryProvider.php @@ -38,7 +38,7 @@ public static function pseudoTypes(): array 'upk' => [PseudoType::UPK, ColumnType::INTEGER, IntegerColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true, 'isUnsigned' => true]], 'bigpk' => [PseudoType::BIGPK, ColumnType::BIGINT, IntegerColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], 'ubigpk' => [PseudoType::UBIGPK, ColumnType::BIGINT, IntegerColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true, 'isUnsigned' => true]], - 'uuid_pk' => [PseudoType::UUID_PK, ColumnType::UUID, StringColumnSchema::class, ['isPrimaryKey' => true]], + 'uuid_pk' => [PseudoType::UUID_PK, ColumnType::UUID, StringColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], 'uuid_pk_seq' => [PseudoType::UUID_PK_SEQ, ColumnType::UUID, StringColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], ]; } diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index de73e3110..85df631c5 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -8,6 +8,8 @@ use Yiisoft\Db\Command\DataType; use Yiisoft\Db\Command\Param; use Yiisoft\Db\Constant\ColumnType; +use Yiisoft\Db\Constant\PseudoType; +use Yiisoft\Db\Constraint\ForeignKeyConstraint; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Query\Query; @@ -15,6 +17,7 @@ use Yiisoft\Db\QueryBuilder\Condition\InCondition; use Yiisoft\Db\QueryBuilder\Condition\LikeCondition; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; +use Yiisoft\Db\Schema\Column\ColumnBuilder; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Tests\Support\DbHelper; use Yiisoft\Db\Tests\Support\Stub\Column; @@ -1562,4 +1565,132 @@ public static function overlapsCondition(): array 'query expression' => [(new Query(static::getDb()))->select(new JsonExpression([0,1,2,7])), 1], ]; } + + public static function buildColumnDefinition(): array + { + $reference = new ForeignKeyConstraint(); + $reference->foreignColumnNames(['id']); + $reference->foreignTableName('ref_table'); + $reference->onDelete('CASCADE'); + $reference->onUpdate('CASCADE'); + + $referenceWithSchema = clone $reference; + $referenceWithSchema->foreignSchemaName('ref_schema'); + + return [ + PseudoType::PK => ['integer PRIMARY KEY AUTO_INCREMENT', PseudoType::PK], + PseudoType::UPK => ['integer UNSIGNED PRIMARY KEY AUTO_INCREMENT', PseudoType::UPK], + PseudoType::BIGPK => ['bigint PRIMARY KEY AUTO_INCREMENT', PseudoType::BIGPK], + PseudoType::UBIGPK => ['bigint UNSIGNED PRIMARY KEY AUTO_INCREMENT', PseudoType::UBIGPK], + PseudoType::UUID_PK => ['uuid PRIMARY KEY DEFAULT uuid()', PseudoType::UUID_PK], + PseudoType::UUID_PK_SEQ => ['uuid PRIMARY KEY DEFAULT uuid()', PseudoType::UUID_PK_SEQ], + 'STRING' => ['varchar', ColumnType::STRING], + 'STRING(100)' => ['varchar(100)', ColumnType::STRING . '(100)'], + + 'primaryKey()' => ['integer PRIMARY KEY AUTO_INCREMENT', ColumnBuilder::primaryKey()], + 'primaryKey(false)' => ['integer PRIMARY KEY', ColumnBuilder::primaryKey(false)], + 'smallPrimaryKey()' => ['smallint PRIMARY KEY AUTO_INCREMENT', ColumnBuilder::smallPrimaryKey()], + 'smallPrimaryKey(false)' => ['smallint PRIMARY KEY', ColumnBuilder::smallPrimaryKey(false)], + 'bigPrimaryKey()' => ['bigint PRIMARY KEY AUTO_INCREMENT', ColumnBuilder::bigPrimaryKey()], + 'bigPrimaryKey(false)' => ['bigint PRIMARY KEY', ColumnBuilder::bigPrimaryKey(false)], + 'uuidPrimaryKey()' => ['uuid PRIMARY KEY DEFAULT uuid()', ColumnBuilder::uuidPrimaryKey()], + 'uuidPrimaryKey(false)' => ['uuid PRIMARY KEY', ColumnBuilder::uuidPrimaryKey(false)], + + 'boolean()' => ['boolean', ColumnBuilder::boolean()], + 'boolean(100)' => ['boolean', ColumnBuilder::boolean()->size(100)], + 'bit()' => ['bit', ColumnBuilder::bit()], + 'bit(1)' => ['bit(1)', ColumnBuilder::bit(1)], + 'bit(8)' => ['bit(8)', ColumnBuilder::bit(8)], + 'bit(1000)' => ['bit(1000)', ColumnBuilder::bit(1000)], + 'tinyint()' => ['tinyint', ColumnBuilder::tinyint()], + 'tinyint(2)' => ['tinyint(2)', ColumnBuilder::tinyint(2)], + 'smallint()' => ['smallint', ColumnBuilder::smallint()], + 'smallint(4)' => ['smallint(4)', ColumnBuilder::smallint(4)], + 'integer()' => ['integer', ColumnBuilder::integer()], + 'integer(8)' => ['integer(8)', ColumnBuilder::integer(8)], + 'bigint()' => ['bigint', ColumnBuilder::bigint()], + 'bigint(15)' => ['bigint(15)', ColumnBuilder::bigint(15)], + 'float()' => ['float', ColumnBuilder::float()], + 'float(10)' => ['float(10)', ColumnBuilder::float(10)], + 'float(10,2)' => ['float(10,2)', ColumnBuilder::float(10, 2)], + 'double()' => ['double', ColumnBuilder::double()], + 'double(10)' => ['double(10)', ColumnBuilder::double(10)], + 'double(10,2)' => ['double(10,2)', ColumnBuilder::double(10, 2)], + 'decimal()' => ['decimal(10,0)', ColumnBuilder::decimal()], + 'decimal(5)' => ['decimal(5,0)', ColumnBuilder::decimal(5)], + 'decimal(5,2)' => ['decimal(5,2)', ColumnBuilder::decimal(5, 2)], + 'decimal(null)' => ['decimal', ColumnBuilder::decimal(null)], + 'money()' => ['money', ColumnBuilder::money()], + 'money(10)' => ['money', ColumnBuilder::money(10)], + 'money(10,2)' => ['money', ColumnBuilder::money(10, 2)], + 'money(null)' => ['money', ColumnBuilder::money(null)], + 'char()' => ['char(1)', ColumnBuilder::char()], + 'char(10)' => ['char(10)', ColumnBuilder::char(10)], + 'char(null)' => ['char', ColumnBuilder::char(null)], + 'string()' => ['varchar(255)', ColumnBuilder::string()], + 'string(100)' => ['varchar(100)', ColumnBuilder::string(100)], + 'string(null)' => ['varchar', ColumnBuilder::string(null)], + 'text()' => ['text', ColumnBuilder::text()], + 'text(1000)' => ['text(1000)', ColumnBuilder::text(1000)], + 'binary()' => ['binary', ColumnBuilder::binary()], + 'binary(1000)' => ['binary(1000)', ColumnBuilder::binary(1000)], + 'uuid()' => ['uuid', ColumnBuilder::uuid()], + 'datetime()' => ['datetime(0)', ColumnBuilder::datetime()], + 'datetime(6)' => ['datetime(6)', ColumnBuilder::datetime(6)], + 'datetime(null)' => ['datetime', ColumnBuilder::datetime(null)], + 'timestamp()' => ['timestamp(0)', ColumnBuilder::timestamp()], + 'timestamp(6)' => ['timestamp(6)', ColumnBuilder::timestamp(6)], + 'timestamp(null)' => ['timestamp', ColumnBuilder::timestamp(null)], + 'date()' => ['date', ColumnBuilder::date()], + 'date(100)' => ['date', ColumnBuilder::date()->size(100)], + 'time()' => ['time(0)', ColumnBuilder::time()], + 'time(6)' => ['time(6)', ColumnBuilder::time(6)], + 'time(null)' => ['time', ColumnBuilder::time(null)], + 'array()' => ['json', ColumnBuilder::array()], + 'structured()' => ['json', ColumnBuilder::structured()], + "structured('structured_type')" => ['structured_type', ColumnBuilder::structured('structured_type')], + 'json()' => ['json', ColumnBuilder::json()], + 'json(100)' => ['json', ColumnBuilder::json()->size(100)], + + "enum('a','b','c')" => ["enum('a','b','c')", ColumnBuilder::string()->dbType("enum('a','b','c')")], + "extra('bar')" => ['varchar(255) bar', ColumnBuilder::string()->extra('bar')], + "extra('')" => ['varchar(255)', ColumnBuilder::string()->extra('')], + "check('value > 5')" => ['varchar(255) CHECK (value > 5)', ColumnBuilder::string()->check('value > 5')], + "check('')" => ['varchar(255)', ColumnBuilder::string()->check('')], + 'check(null)' => ['varchar(255)', ColumnBuilder::string()->check(null)], + "comment('comment')" => ['varchar(255)', ColumnBuilder::string()->comment('comment')], + "comment('')" => ['varchar(255)', ColumnBuilder::string()->comment('')], + 'comment(null)' => ['varchar(255)', ColumnBuilder::string()->comment(null)], + "defaultValue('value')" => ["varchar(255) DEFAULT 'value'", ColumnBuilder::string()->defaultValue('value')], + "defaultValue('')" => ["varchar(255) DEFAULT ''", ColumnBuilder::string()->defaultValue('')], + 'defaultValue(null)' => ['varchar(255)', ColumnBuilder::string()->defaultValue(null)], + 'defaultValue($expression)' => ['varchar(255) DEFAULT expression', ColumnBuilder::string()->defaultValue(new Expression('expression'))], + "integer()->defaultValue('')" => ['integer', ColumnBuilder::integer()->defaultValue('')], + 'notNull()' => ['varchar(255) NOT NULL', ColumnBuilder::string()->notNull()], + 'integer()->primaryKey()' => ['integer PRIMARY KEY', ColumnBuilder::integer()->primaryKey()], + 'size(10)' => ['varchar(10)', ColumnBuilder::string()->size(10)], + 'unique()' => ['varchar(255) UNIQUE', ColumnBuilder::string()->unique()], + 'unsigned()' => ['integer UNSIGNED', ColumnBuilder::integer()->unsigned()], + 'scale(2)' => ['decimal(10,2)', ColumnBuilder::decimal()->scale(2)], + 'integer(8)->scale(2)' => ['integer(8)', ColumnBuilder::integer(8)->scale(2)], + 'reference($reference)' => [ + DbHelper::replaceQuotes( + <<reference($reference), + ], + 'reference($referenceWithSchema)' => [ + DbHelper::replaceQuotes( + <<reference($referenceWithSchema), + ], + ]; + } } diff --git a/tests/Support/Stub/ColumnDefinitionBuilder.php b/tests/Support/Stub/ColumnDefinitionBuilder.php new file mode 100644 index 000000000..199943ef6 --- /dev/null +++ b/tests/Support/Stub/ColumnDefinitionBuilder.php @@ -0,0 +1,69 @@ +getType()) { + ColumnType::BOOLEAN => 'boolean', + ColumnType::BIT => 'bit', + ColumnType::TINYINT => 'tinyint', + ColumnType::SMALLINT => 'smallint', + ColumnType::INTEGER => 'integer', + ColumnType::BIGINT => 'bigint', + ColumnType::FLOAT => 'float', + ColumnType::DOUBLE => 'double', + ColumnType::DECIMAL => 'decimal', + ColumnType::MONEY => 'money', + ColumnType::CHAR => 'char', + ColumnType::STRING => 'varchar', + ColumnType::TEXT => 'text', + ColumnType::BINARY => 'binary', + ColumnType::UUID => 'uuid', + ColumnType::DATETIME => 'datetime', + ColumnType::TIMESTAMP => 'timestamp', + ColumnType::DATE => 'date', + ColumnType::TIME => 'time', + ColumnType::ARRAY => 'json', + ColumnType::STRUCTURED => 'json', + ColumnType::JSON => 'json', + default => 'varchar', + }; + } +} diff --git a/tests/Support/Stub/ColumnFactory.php b/tests/Support/Stub/ColumnFactory.php index bd6d4f2bc..2adf21f23 100644 --- a/tests/Support/Stub/ColumnFactory.php +++ b/tests/Support/Stub/ColumnFactory.php @@ -16,6 +16,7 @@ protected function getType(string $dbType, array $info = []): string protected function isDbType(string $dbType): bool { - return $this->isType($dbType); + return $dbType === 'varchar' + || $dbType !== 'string' && $this->isType($dbType); } } diff --git a/tests/Support/Stub/Connection.php b/tests/Support/Stub/Connection.php index ac616b0af..8b0cee145 100644 --- a/tests/Support/Stub/Connection.php +++ b/tests/Support/Stub/Connection.php @@ -8,7 +8,6 @@ use Yiisoft\Db\Command\CommandInterface; use Yiisoft\Db\Driver\Pdo\AbstractPdoConnection; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; -use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; use Yiisoft\Db\Schema\Quoter; use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Schema\SchemaInterface; @@ -43,11 +42,6 @@ public function createTransaction(): TransactionInterface return new Transaction($this); } - public function getColumnFactory(): ColumnFactoryInterface - { - return new ColumnFactory(); - } - public function getQueryBuilder(): QueryBuilderInterface { if ($this->queryBuilder === null) { diff --git a/tests/Support/Stub/QueryBuilder.php b/tests/Support/Stub/QueryBuilder.php index e78c90cdc..ad93dd8ce 100644 --- a/tests/Support/Stub/QueryBuilder.php +++ b/tests/Support/Stub/QueryBuilder.php @@ -14,8 +14,9 @@ public function __construct(QuoterInterface $quoter, SchemaInterface $schema) { $ddlBuilder = new DDLQueryBuilder($this, $quoter, $schema); $dmlBuilder = new DMLQueryBuilder($this, $quoter, $schema); - $dqlBuilder = new DQLQueryBuilder($this, $quoter, $schema); + $dqlBuilder = new DQLQueryBuilder($this, $quoter); + $columnDefinitionBuilder = new ColumnDefinitionBuilder($this); - parent::__construct($quoter, $schema, $ddlBuilder, $dmlBuilder, $dqlBuilder); + parent::__construct($quoter, $schema, $ddlBuilder, $dmlBuilder, $dqlBuilder, $columnDefinitionBuilder); } } diff --git a/tests/Support/Stub/Schema.php b/tests/Support/Stub/Schema.php index 554299ccb..62098a38a 100644 --- a/tests/Support/Stub/Schema.php +++ b/tests/Support/Stub/Schema.php @@ -8,6 +8,7 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Schema\AbstractSchema; use Yiisoft\Db\Schema\Builder\ColumnInterface; +use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; use Yiisoft\Db\Schema\TableSchemaInterface; /** @@ -22,6 +23,11 @@ public function createColumn(string $type, array|int|string $length = null): Col return new Column($type, $length); } + public function getColumnFactory(): ColumnFactoryInterface + { + return new ColumnFactory(); + } + public function findUniqueIndexes(TableSchemaInterface $table): array { throw new NotSupportedException(__METHOD__ . ' is not supported by this DBMS.'); From ee6f00ee6a014e34008c5b9ae3e599d2739219c7 Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Fri, 18 Oct 2024 15:03:12 +0700 Subject: [PATCH 6/8] Update DMLQueryBuilder::insertBatch() method (#889) --- CHANGELOG.md | 1 + src/QueryBuilder/AbstractDMLQueryBuilder.php | 4 ++-- tests/Provider/QueryBuilderProvider.php | 4 ++-- tests/Support/DbHelper.php | 14 +++++++++++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd645674b..5f7a5239d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ - New #882: Move `ArrayColumnSchema` and `StructuredColumnSchema` classes from `db-pgsql` package (@Tigrov) - New #883: Add `ColumnDefinitionBuilder` class and `QueryBuilderInterface::buildColumnDefinition()` method (@Tigrov) - Enh #885: Refactor `AbstractDsn` class (@Tigrov) +- Chg #889: Update `AbstractDMLQueryBuilder::insertBatch()` method (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/src/QueryBuilder/AbstractDMLQueryBuilder.php b/src/QueryBuilder/AbstractDMLQueryBuilder.php index 7097b0f4a..3b3905ec1 100644 --- a/src/QueryBuilder/AbstractDMLQueryBuilder.php +++ b/src/QueryBuilder/AbstractDMLQueryBuilder.php @@ -96,7 +96,7 @@ public function insertBatch(string $table, iterable $rows, array $columns = [], $query .= ' (' . implode(', ', $quotedColumnNames) . ')'; } - return $query . ' VALUES ' . implode(', ', $values); + return $query . ' VALUES (' . implode('), (', $values) . ')'; } public function delete(string $table, array|string $condition, array &$params): string @@ -209,7 +209,7 @@ protected function prepareBatchInsertValues(string $table, iterable $rows, array ++$i; } - $values[] = '(' . implode(', ', $placeholders) . ')'; + $values[] = implode(', ', $placeholders); } return $values; diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 85df631c5..63b22d580 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -172,7 +172,7 @@ public static function batchInsert(): array 'customer', [], ['address'], - '', + 'expected' => '', ], 'customer3' => [ 'customer', @@ -242,7 +242,7 @@ public static function batchInsert(): array } })(), [], - '', + 'expected' => '', ], 'empty columns and non-exists table' => [ 'non_exists_table', diff --git a/tests/Support/DbHelper.php b/tests/Support/DbHelper.php index 8aeab5d9c..18970d50f 100644 --- a/tests/Support/DbHelper.php +++ b/tests/Support/DbHelper.php @@ -21,7 +21,19 @@ final class DbHelper { public static function changeSqlForOracleBatchInsert(string &$str): void { - $str = str_replace('INSERT INTO', 'INSERT ALL INTO', $str) . ' SELECT 1 FROM SYS.DUAL'; + if (empty($str)) { + return; + } + + $str = str_replace( + ' VALUES (', + "\nSELECT ", + str_replace( + '), (', + " FROM DUAL UNION ALL\nSELECT ", + substr($str, 0, -1) + ) + ) . ' FROM DUAL'; } public static function getPsrCache(): CacheInterface From d04a9c4a26fbbcd36706405f5cf10ff0f5d631bc Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Fri, 18 Oct 2024 18:09:21 +0700 Subject: [PATCH 7/8] Remove `ColumnSchema::load()`, add properties to constructor (#890) --- CHANGELOG.md | 1 + UPGRADE.md | 4 +- src/Connection/AbstractDsn.php | 2 +- src/Schema/Column/AbstractColumnFactory.php | 89 ++++++++------ src/Schema/Column/AbstractColumnSchema.php | 116 +++++++++--------- src/Schema/Column/ArrayColumnSchema.php | 27 ++-- src/Schema/Column/BigIntColumnSchema.php | 12 +- src/Schema/Column/BinaryColumnSchema.php | 12 +- src/Schema/Column/BitColumnSchema.php | 12 +- src/Schema/Column/BooleanColumnSchema.php | 12 +- src/Schema/Column/ColumnBuilder.php | 61 +++------ src/Schema/Column/ColumnFactoryInterface.php | 40 ++++-- src/Schema/Column/ColumnSchemaInterface.php | 34 +---- src/Schema/Column/DoubleColumnSchema.php | 12 +- src/Schema/Column/IntegerColumnSchema.php | 12 +- src/Schema/Column/JsonColumnSchema.php | 12 +- src/Schema/Column/StringColumnSchema.php | 12 +- src/Schema/Column/StructuredColumnSchema.php | 17 ++- src/Syntax/ColumnDefinitionParser.php | 12 +- tests/Db/Schema/ColumnSchemaTest.php | 8 +- .../ColumnDefinitionParserProvider.php | 20 +-- tests/Provider/ColumnFactoryProvider.php | 2 +- tests/Provider/ColumnSchemaProvider.php | 44 ++----- tests/Support/Stub/ColumnSchema.php | 6 +- 24 files changed, 257 insertions(+), 322 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7a5239d..e11cbd3e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - New #883: Add `ColumnDefinitionBuilder` class and `QueryBuilderInterface::buildColumnDefinition()` method (@Tigrov) - Enh #885: Refactor `AbstractDsn` class (@Tigrov) - Chg #889: Update `AbstractDMLQueryBuilder::insertBatch()` method (@Tigrov) +- Enh #890: Add properties of `AbstractColumnSchema` class to constructor (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/UPGRADE.md b/UPGRADE.md index 42496e1f1..a22e6df49 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -75,8 +75,8 @@ and the following changes were made: - `isNotNull()` method is added; - `unique(bool $unique = true)` method is added; - `isUnique()` method is added; -- `load(array $info)` method is added; -- constructor of `AbstractColumnSchema` class is changed to `__construct(string $type, string|null $phpType = null)`; +- all `AbstractColumnSchema` class properties except `$type` moved to constructor; +- added `DEFAULT_TYPE` constant to `AbstractColumnSchema` class; - added method chaining. ### New classes with constants diff --git a/src/Connection/AbstractDsn.php b/src/Connection/AbstractDsn.php index 8689a04f2..42814f405 100644 --- a/src/Connection/AbstractDsn.php +++ b/src/Connection/AbstractDsn.php @@ -25,7 +25,7 @@ abstract class AbstractDsn implements DsnInterface, Stringable */ public function __construct( private readonly string $driver, - private readonly string $host, + private readonly string $host = '127.0.0.1', private readonly string|null $databaseName = null, private readonly string|null $port = null, private readonly array $options = [] diff --git a/src/Schema/Column/AbstractColumnFactory.php b/src/Schema/Column/AbstractColumnFactory.php index cbc659d8f..a58789c1c 100644 --- a/src/Schema/Column/AbstractColumnFactory.php +++ b/src/Schema/Column/AbstractColumnFactory.php @@ -13,8 +13,7 @@ /** * The default implementation of the {@see ColumnFactoryInterface}. * - * @psalm-import-type ColumnInfo from ColumnSchemaInterface - * @psalm-suppress MixedArgumentTypeCoercion + * @psalm-import-type ColumnInfo from ColumnFactoryInterface */ abstract class AbstractColumnFactory implements ColumnFactoryInterface { @@ -38,8 +37,10 @@ abstract protected function isDbType(string $dbType): bool; public function fromDbType(string $dbType, array $info = []): ColumnSchemaInterface { - $info['db_type'] = $dbType; + unset($info['dbType']); $type = $info['type'] ?? $this->getType($dbType, $info); + unset($info['type']); + $info['dbType'] = $dbType; return $this->fromType($type, $info); } @@ -52,62 +53,76 @@ public function fromDefinition(string $definition, array $info = []): ColumnSche $info['extra'] = $definitionInfo['extra'] . ' ' . $info['extra']; } - /** @var string $dbType */ - $dbType = $definitionInfo['db_type'] ?? ''; - unset($definitionInfo['db_type']); + /** @var string $type */ + $type = $definitionInfo['type'] ?? ''; + unset($definitionInfo['type']); $info += $definitionInfo; - if ($this->isDbType($dbType)) { - return $this->fromDbType($dbType, $info); + if ($this->isDbType($type)) { + unset($info['dbType']); + return $this->fromDbType($type, $info); } - if ($this->isType($dbType)) { - return $this->fromType($dbType, $info); + if ($this->isType($type)) { + unset($info['type']); + return $this->fromType($type, $info); } - if ($this->isPseudoType($dbType)) { - return $this->fromPseudoType($dbType, $info); + if ($this->isPseudoType($type)) { + return $this->fromPseudoType($type, $info); } - return $this->fromDbType($dbType, $info); + unset($info['dbType']); + return $this->fromDbType($type, $info); } + /** + * @psalm-suppress MixedArgument + * @psalm-suppress InvalidArgument + * @psalm-suppress InvalidNamedArgument + */ public function fromPseudoType(string $pseudoType, array $info = []): ColumnSchemaInterface { + $info['primaryKey'] = true; + $info['autoIncrement'] = true; + return match ($pseudoType) { - PseudoType::PK => ColumnBuilder::primaryKey()->load($info), - PseudoType::UPK => ColumnBuilder::primaryKey()->unsigned()->load($info), - PseudoType::BIGPK => ColumnBuilder::bigPrimaryKey()->load($info), - PseudoType::UBIGPK => ColumnBuilder::bigPrimaryKey()->unsigned()->load($info), - PseudoType::UUID_PK => ColumnBuilder::uuidPrimaryKey()->load($info), - PseudoType::UUID_PK_SEQ => ColumnBuilder::uuidPrimaryKey()->load($info), + PseudoType::PK => new IntegerColumnSchema(ColumnType::INTEGER, ...$info), + PseudoType::UPK => new IntegerColumnSchema(ColumnType::INTEGER, ...[...$info, 'unsigned' => true]), + PseudoType::BIGPK => PHP_INT_SIZE !== 8 + ? new BigIntColumnSchema(ColumnType::BIGINT, ...$info) + : new IntegerColumnSchema(ColumnType::BIGINT, ...$info), + PseudoType::UBIGPK => new BigIntColumnSchema(ColumnType::BIGINT, ...[...$info, 'unsigned' => true]), + PseudoType::UUID_PK => new StringColumnSchema(ColumnType::UUID, ...$info), + PseudoType::UUID_PK_SEQ => new StringColumnSchema(ColumnType::UUID, ...$info), }; } + /** + * @psalm-suppress InvalidNamedArgument + */ public function fromType(string $type, array $info = []): ColumnSchemaInterface { - $column = match ($type) { - ColumnType::BOOLEAN => new BooleanColumnSchema($type), - ColumnType::BIT => new BitColumnSchema($type), - ColumnType::TINYINT => new IntegerColumnSchema($type), - ColumnType::SMALLINT => new IntegerColumnSchema($type), + return match ($type) { + ColumnType::BOOLEAN => new BooleanColumnSchema($type, ...$info), + ColumnType::BIT => new BitColumnSchema($type, ...$info), + ColumnType::TINYINT => new IntegerColumnSchema($type, ...$info), + ColumnType::SMALLINT => new IntegerColumnSchema($type, ...$info), ColumnType::INTEGER => PHP_INT_SIZE !== 8 && !empty($info['unsigned']) - ? new BigIntColumnSchema($type) - : new IntegerColumnSchema($type), + ? new BigIntColumnSchema($type, ...$info) + : new IntegerColumnSchema($type, ...$info), ColumnType::BIGINT => PHP_INT_SIZE !== 8 || !empty($info['unsigned']) - ? new BigIntColumnSchema($type) - : new IntegerColumnSchema($type), - ColumnType::DECIMAL => new DoubleColumnSchema($type), - ColumnType::FLOAT => new DoubleColumnSchema($type), - ColumnType::DOUBLE => new DoubleColumnSchema($type), - ColumnType::BINARY => new BinaryColumnSchema($type), - ColumnType::STRUCTURED => new StructuredColumnSchema($type), - ColumnType::JSON => new JsonColumnSchema($type), - default => new StringColumnSchema($type), + ? new BigIntColumnSchema($type, ...$info) + : new IntegerColumnSchema($type, ...$info), + ColumnType::DECIMAL => new DoubleColumnSchema($type, ...$info), + ColumnType::FLOAT => new DoubleColumnSchema($type, ...$info), + ColumnType::DOUBLE => new DoubleColumnSchema($type, ...$info), + ColumnType::BINARY => new BinaryColumnSchema($type, ...$info), + ColumnType::STRUCTURED => new StructuredColumnSchema($type, ...$info), + ColumnType::JSON => new JsonColumnSchema($type, ...$info), + default => new StringColumnSchema($type, ...$info), }; - - return $column->load($info); } /** diff --git a/src/Schema/Column/AbstractColumnSchema.php b/src/Schema/Column/AbstractColumnSchema.php index a6b4ef8a0..377d0f952 100644 --- a/src/Schema/Column/AbstractColumnSchema.php +++ b/src/Schema/Column/AbstractColumnSchema.php @@ -9,7 +9,7 @@ use Yiisoft\Db\Constraint\ForeignKeyConstraint; -use function is_array; +use function property_exists; /** * Represents the metadata of a column in a database table. @@ -37,29 +37,69 @@ */ abstract class AbstractColumnSchema implements ColumnSchemaInterface { - private bool $autoIncrement = false; - private string|null $check = null; - private string|null $comment = null; - private bool $computed = false; - private string|null $dbType = null; - private mixed $defaultValue = null; - private array|null $enumValues = null; - private string|null $extra = null; - private bool $isPrimaryKey = false; - private string|null $name = null; - private bool $notNull = false; - private ForeignKeyConstraint|null $reference = null; - private int|null $scale = null; - private int|null $size = null; - private bool $unique = false; - private bool $unsigned = false; + /** + * @var string The default column abstract type + * @psalm-var ColumnType::* + */ + protected const DEFAULT_TYPE = ColumnType::STRING; + + /** + * @var string The column abstract type + * @psalm-var ColumnType::* + */ + private string $type; /** + * @param string|null $type The column's abstract type. + * @param bool $autoIncrement Whether the column is auto-incremental. + * @param string|null $check The check constraint for the column. + * @param string|null $comment The column's comment. + * @param bool $computed Whether the column is a computed column. + * @param string|null $dbType The column's database type. + * @param mixed $defaultValue The default value of the column. + * @param array|null $enumValues The list of possible values for an ENUM column. + * @param string|null $extra Any extra information that needs to be appended to the column's definition. + * @param bool $primaryKey Whether the column is a primary key. + * @param string|null $name The column's name. + * @param bool $notNull Whether the column is not nullable. + * @param ForeignKeyConstraint|null $reference The foreign key constraint. + * @param int|null $scale The number of digits to the right of the decimal point. + * @param int|null $size The column's size. + * @param bool $unique Whether the column is unique. + * @param bool $unsigned Whether the column is unsigned. + * @param mixed ...$args Additional arguments to be passed to the constructor. + * * @psalm-param ColumnType::* $type + * @psalm-param array $args */ public function __construct( - private string $type, + string|null $type = null, + private bool $autoIncrement = false, + private string|null $check = null, + private string|null $comment = null, + private bool $computed = false, + private string|null $dbType = null, + private mixed $defaultValue = null, + private array|null $enumValues = null, + private string|null $extra = null, + private bool $primaryKey = false, + private string|null $name = null, + private bool $notNull = false, + private ForeignKeyConstraint|null $reference = null, + private int|null $scale = null, + private int|null $size = null, + private bool $unique = false, + private bool $unsigned = false, + mixed ...$args, ) { + $this->type = $type ?? static::DEFAULT_TYPE; + + /** @var array $args */ + foreach ($args as $property => $value) { + if (property_exists($this, $property)) { + $this->$property = $value; + } + } } /** @@ -215,7 +255,7 @@ public function isNotNull(): bool public function isPrimaryKey(): bool { - return $this->isPrimaryKey; + return $this->primaryKey; } public function isUnique(): bool @@ -228,40 +268,6 @@ public function isUnsigned(): bool return $this->unsigned; } - public function load(array $info): static - { - foreach ($info as $key => $value) { - /** - * @psalm-suppress PossiblyInvalidCast - * @psalm-suppress InvalidCast - * @psalm-suppress DeprecatedMethod - */ - match ($key) { - 'allow_null' => $this->allowNull((bool) $value), - 'auto_increment' => $this->autoIncrement((bool) $value), - 'check' => $this->check($value !== null ? (string) $value : null), - 'comment' => $this->comment($value !== null ? (string) $value : null), - 'computed' => $this->computed((bool) $value), - 'db_type' => $this->dbType($value !== null ? (string) $value : null), - 'default_value' => $this->defaultValue($value), - 'enum_values' => $this->enumValues(is_array($value) ? $value : null), - 'extra' => $this->extra($value !== null ? (string) $value : null), - 'name' => $this->name($value !== null ? (string) $value : null), - 'not_null' => $this->notNull((bool) $value), - 'primary_key' => $this->primaryKey((bool) $value), - 'precision' => $this->precision($value !== null ? (int) $value : null), - 'reference' => $this->reference($value instanceof ForeignKeyConstraint ? $value : null), - 'scale' => $this->scale($value !== null ? (int) $value : null), - 'size' => $this->size($value !== null ? (int) $value : null), - 'unique' => $this->unique((bool) $value), - 'unsigned' => $this->unsigned((bool) $value), - default => null, - }; - } - - return $this; - } - /** * @deprecated Will be removed in version 2.0. */ @@ -285,9 +291,9 @@ public function precision(int|null $precision): static return $this->size($precision); } - public function primaryKey(bool $isPrimaryKey = true): static + public function primaryKey(bool $primaryKey = true): static { - $this->isPrimaryKey = $isPrimaryKey; + $this->primaryKey = $primaryKey; return $this; } diff --git a/src/Schema/Column/ArrayColumnSchema.php b/src/Schema/Column/ArrayColumnSchema.php index 1f93a5d45..7f60ea92f 100644 --- a/src/Schema/Column/ArrayColumnSchema.php +++ b/src/Schema/Column/ArrayColumnSchema.php @@ -19,17 +19,23 @@ use function is_string; use function iterator_to_array; +/** + * Represents the schema for an array column. + */ class ArrayColumnSchema extends AbstractColumnSchema { + protected const DEFAULT_TYPE = ColumnType::ARRAY; + /** * @var ColumnSchemaInterface|null The column of an array item. */ - private ColumnSchemaInterface|null $column = null; + protected ColumnSchemaInterface|null $column = null; /** * @var int The dimension of array, must be greater than 0. + * @psalm-var positive-int */ - private int $dimension = 1; + protected int $dimension = 1; /** * Returns the parser for the column value. @@ -39,15 +45,6 @@ protected function getParser(): ParserToArrayInterface throw new NotSupportedException(__METHOD__ . '() is not supported. Use concrete DBMS implementation.'); } - /** - * @psalm-param ColumnType::* $type - */ - public function __construct( - string $type = ColumnType::ARRAY, - ) { - parent::__construct($type); - } - /** * Set column of an array item. */ @@ -74,7 +71,9 @@ public function getColumn(): ColumnSchemaInterface } /** - * Set dimension of an array, must be greater than 0. + * Set dimension of an array, must be greater than + * + * @psalm-param positive-int $dimension */ public function dimension(int $dimension): static { @@ -84,6 +83,8 @@ public function dimension(int $dimension): static /** * @return int the dimension of the array. + * + * @psalm-return positive-int */ public function getDimension(): int { @@ -143,6 +144,8 @@ public function phpTypecast(mixed $value): array|null * @param mixed $value The array or iterable object. * @param int $dimension The array dimension. Should be more than 0. * + * @psalm-param positive-int $dimension + * * @return array|null Converted values. */ protected function dbTypecastArray(mixed $value, int $dimension): array|null diff --git a/src/Schema/Column/BigIntColumnSchema.php b/src/Schema/Column/BigIntColumnSchema.php index ebb9a9c41..30e0b6227 100644 --- a/src/Schema/Column/BigIntColumnSchema.php +++ b/src/Schema/Column/BigIntColumnSchema.php @@ -14,16 +14,12 @@ use const PHP_INT_MAX; use const PHP_INT_MIN; +/** + * Represents the metadata for a bigint column. + */ class BigIntColumnSchema extends AbstractColumnSchema { - /** - * @psalm-param ColumnType::* $type - */ - public function __construct( - string $type = ColumnType::BIGINT, - ) { - parent::__construct($type); - } + protected const DEFAULT_TYPE = ColumnType::BIGINT; public function dbTypecast(mixed $value): int|string|ExpressionInterface|null { diff --git a/src/Schema/Column/BinaryColumnSchema.php b/src/Schema/Column/BinaryColumnSchema.php index 2109d2e84..b3fc33e9a 100644 --- a/src/Schema/Column/BinaryColumnSchema.php +++ b/src/Schema/Column/BinaryColumnSchema.php @@ -12,16 +12,12 @@ use function gettype; +/** + * Represents the metadata for a binary column. + */ class BinaryColumnSchema extends AbstractColumnSchema { - /** - * @psalm-param ColumnType::* $type - */ - public function __construct( - string $type = ColumnType::BINARY, - ) { - parent::__construct($type); - } + protected const DEFAULT_TYPE = ColumnType::BINARY; public function dbTypecast(mixed $value): mixed { diff --git a/src/Schema/Column/BitColumnSchema.php b/src/Schema/Column/BitColumnSchema.php index 973a64635..a2cdc30d7 100644 --- a/src/Schema/Column/BitColumnSchema.php +++ b/src/Schema/Column/BitColumnSchema.php @@ -8,16 +8,12 @@ use Yiisoft\Db\Constant\PhpType; use Yiisoft\Db\Expression\ExpressionInterface; +/** + * Represents the metadata for a bit column. + */ class BitColumnSchema extends AbstractColumnSchema { - /** - * @psalm-param ColumnType::* $type - */ - public function __construct( - string $type = ColumnType::BIT, - ) { - parent::__construct($type); - } + protected const DEFAULT_TYPE = ColumnType::BIT; public function dbTypecast(mixed $value): int|string|ExpressionInterface|null { diff --git a/src/Schema/Column/BooleanColumnSchema.php b/src/Schema/Column/BooleanColumnSchema.php index 6ea80f9b5..4d60ba2dd 100644 --- a/src/Schema/Column/BooleanColumnSchema.php +++ b/src/Schema/Column/BooleanColumnSchema.php @@ -8,16 +8,12 @@ use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Constant\PhpType; +/** + * Represents the metadata for a boolean column. + */ class BooleanColumnSchema extends AbstractColumnSchema { - /** - * @psalm-param ColumnType::* $type - */ - public function __construct( - string $type = ColumnType::BOOLEAN, - ) { - parent::__construct($type); - } + protected const DEFAULT_TYPE = ColumnType::BOOLEAN; public function dbTypecast(mixed $value): bool|ExpressionInterface|null { diff --git a/src/Schema/Column/ColumnBuilder.php b/src/Schema/Column/ColumnBuilder.php index 9160fffd3..8b35f3a2a 100644 --- a/src/Schema/Column/ColumnBuilder.php +++ b/src/Schema/Column/ColumnBuilder.php @@ -9,7 +9,7 @@ /** * Column builder for database {@see ColumnSchemaInterface} instances. * - * @psalm-import-type ColumnInfo from ColumnSchemaInterface + * @psalm-import-type ColumnInfo from ColumnFactoryInterface */ class ColumnBuilder { @@ -70,8 +70,7 @@ public static function boolean(): ColumnSchemaInterface */ public static function bit(int|null $size = null): ColumnSchemaInterface { - return (new BitColumnSchema(ColumnType::BIT)) - ->size($size); + return new BitColumnSchema(ColumnType::BIT, size: $size); } /** @@ -79,8 +78,7 @@ public static function bit(int|null $size = null): ColumnSchemaInterface */ public static function tinyint(int|null $size = null): ColumnSchemaInterface { - return (new IntegerColumnSchema(ColumnType::TINYINT)) - ->size($size); + return new IntegerColumnSchema(ColumnType::TINYINT, size: $size); } /** @@ -88,8 +86,7 @@ public static function tinyint(int|null $size = null): ColumnSchemaInterface */ public static function smallint(int|null $size = null): ColumnSchemaInterface { - return (new IntegerColumnSchema(ColumnType::SMALLINT)) - ->size($size); + return new IntegerColumnSchema(ColumnType::SMALLINT, size: $size); } /** @@ -97,8 +94,7 @@ public static function smallint(int|null $size = null): ColumnSchemaInterface */ public static function integer(int|null $size = null): ColumnSchemaInterface { - return (new IntegerColumnSchema(ColumnType::INTEGER)) - ->size($size); + return new IntegerColumnSchema(ColumnType::INTEGER, size: $size); } /** @@ -106,8 +102,7 @@ public static function integer(int|null $size = null): ColumnSchemaInterface */ public static function bigint(int|null $size = null): ColumnSchemaInterface { - return (new IntegerColumnSchema(ColumnType::BIGINT)) - ->size($size); + return new IntegerColumnSchema(ColumnType::BIGINT, size: $size); } /** @@ -115,9 +110,7 @@ public static function bigint(int|null $size = null): ColumnSchemaInterface */ public static function float(int|null $size = null, int|null $scale = null): ColumnSchemaInterface { - return (new DoubleColumnSchema(ColumnType::FLOAT)) - ->size($size) - ->scale($scale); + return new DoubleColumnSchema(ColumnType::FLOAT, scale: $scale, size: $size); } /** @@ -125,9 +118,7 @@ public static function float(int|null $size = null, int|null $scale = null): Col */ public static function double(int|null $size = null, int|null $scale = null): ColumnSchemaInterface { - return (new DoubleColumnSchema(ColumnType::DOUBLE)) - ->size($size) - ->scale($scale); + return new DoubleColumnSchema(ColumnType::DOUBLE, scale: $scale, size: $size); } /** @@ -135,9 +126,7 @@ public static function double(int|null $size = null, int|null $scale = null): Co */ public static function decimal(int|null $size = 10, int|null $scale = 0): ColumnSchemaInterface { - return (new DoubleColumnSchema(ColumnType::DECIMAL)) - ->size($size) - ->scale($scale); + return new DoubleColumnSchema(ColumnType::DECIMAL, scale: $scale, size: $size); } /** @@ -145,9 +134,7 @@ public static function decimal(int|null $size = 10, int|null $scale = 0): Column */ public static function money(int|null $size = 19, int|null $scale = 4): ColumnSchemaInterface { - return (new DoubleColumnSchema(ColumnType::MONEY)) - ->size($size) - ->scale($scale); + return new DoubleColumnSchema(ColumnType::MONEY, scale: $scale, size: $size); } /** @@ -155,8 +142,7 @@ public static function money(int|null $size = 19, int|null $scale = 4): ColumnSc */ public static function char(int|null $size = 1): ColumnSchemaInterface { - return (new StringColumnSchema(ColumnType::CHAR)) - ->size($size); + return new StringColumnSchema(ColumnType::CHAR, size: $size); } /** @@ -164,8 +150,7 @@ public static function char(int|null $size = 1): ColumnSchemaInterface */ public static function string(int|null $size = 255): ColumnSchemaInterface { - return (new StringColumnSchema(ColumnType::STRING)) - ->size($size); + return new StringColumnSchema(ColumnType::STRING, size: $size); } /** @@ -173,8 +158,7 @@ public static function string(int|null $size = 255): ColumnSchemaInterface */ public static function text(int|null $size = null): ColumnSchemaInterface { - return (new StringColumnSchema(ColumnType::TEXT)) - ->size($size); + return new StringColumnSchema(ColumnType::TEXT, size: $size); } /** @@ -182,8 +166,7 @@ public static function text(int|null $size = null): ColumnSchemaInterface */ public static function binary(int|null $size = null): ColumnSchemaInterface { - return (new BinaryColumnSchema(ColumnType::BINARY)) - ->size($size); + return new BinaryColumnSchema(ColumnType::BINARY, size: $size); } /** @@ -199,8 +182,7 @@ public static function uuid(): ColumnSchemaInterface */ public static function datetime(int|null $size = 0): ColumnSchemaInterface { - return (new StringColumnSchema(ColumnType::DATETIME)) - ->size($size); + return new StringColumnSchema(ColumnType::DATETIME, size: $size); } /** @@ -208,8 +190,7 @@ public static function datetime(int|null $size = 0): ColumnSchemaInterface */ public static function timestamp(int|null $size = 0): ColumnSchemaInterface { - return (new StringColumnSchema(ColumnType::TIMESTAMP)) - ->size($size); + return new StringColumnSchema(ColumnType::TIMESTAMP, size: $size); } /** @@ -225,8 +206,7 @@ public static function date(): ColumnSchemaInterface */ public static function time(int|null $size = 0): ColumnSchemaInterface { - return (new StringColumnSchema(ColumnType::TIME)) - ->size($size); + return new StringColumnSchema(ColumnType::TIME, size: $size); } /** @@ -236,8 +216,7 @@ public static function time(int|null $size = 0): ColumnSchemaInterface */ public static function array(ColumnSchemaInterface|null $column = null): ColumnSchemaInterface { - return (new ArrayColumnSchema(ColumnType::ARRAY)) - ->column($column); + return new ArrayColumnSchema(ColumnType::ARRAY, column: $column); } /** @@ -250,9 +229,7 @@ public static function array(ColumnSchemaInterface|null $column = null): ColumnS */ public static function structured(string|null $dbType = null, array $columns = []): ColumnSchemaInterface { - return (new StructuredColumnSchema(ColumnType::STRUCTURED)) - ->dbType($dbType) - ->columns($columns); + return new StructuredColumnSchema(ColumnType::STRUCTURED, dbType: $dbType, columns: $columns); } /** diff --git a/src/Schema/Column/ColumnFactoryInterface.php b/src/Schema/Column/ColumnFactoryInterface.php index 61398e35d..57d9251ec 100644 --- a/src/Schema/Column/ColumnFactoryInterface.php +++ b/src/Schema/Column/ColumnFactoryInterface.php @@ -6,12 +6,36 @@ use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Constant\PseudoType; +use Yiisoft\Db\Constraint\ForeignKeyConstraint; /** * The interface must be implemented by a column factory class. It should create a column schema for a database column * type and initialize column information. * - * @psalm-import-type ColumnInfo from ColumnSchemaInterface + * @psalm-type ColumnInfo = array{ + * autoIncrement?: bool, + * check?: string|null, + * column?: ColumnSchemaInterface|null, + * columns?: array, + * comment?: string|null, + * computed?: bool, + * dbType?: string|null, + * defaultValue?: mixed, + * dimension?: positive-int, + * enumValues?: array|null, + * extra?: string|null, + * primaryKey?: bool, + * name?: string|null, + * notNull?: bool, + * reference?: ForeignKeyConstraint|null, + * scale?: int|null, + * schema?: string|null, + * size?: int|null, + * table?: string|null, + * type?: ColumnType::*, + * unique?: bool, + * unsigned?: bool, + * } */ interface ColumnFactoryInterface { @@ -19,9 +43,9 @@ interface ColumnFactoryInterface * Creates a column schema for a database column type and initializes column information. * * @param string $dbType The database column type. - * @param array $info The column information. + * @param array $info The column information. The set of parameters may be different for a specific DBMS. * - * @psalm-param ColumnInfo $info The set of parameters may be different for a specific DBMS. + * @psalm-param ColumnInfo $info */ public function fromDbType(string $dbType, array $info = []): ColumnSchemaInterface; @@ -32,9 +56,9 @@ public function fromDbType(string $dbType, array $info = []): ColumnSchemaInterf * For example, `varchar(255) NOT NULL` is a database type with a size and a NOT NULL constraint. * * @param string $definition The database column definition. - * @param array $info The column information. + * @param array $info The column information. The set of parameters may be different for a specific DBMS. * - * @psalm-param ColumnInfo $info The set of parameters may be different for a specific DBMS. + * @psalm-param ColumnInfo $info */ public function fromDefinition(string $definition, array $info = []): ColumnSchemaInterface; @@ -42,7 +66,7 @@ public function fromDefinition(string $definition, array $info = []): ColumnSche * Creates a column schema for a pseudo-type and initializes column information. * * @param string $pseudoType The pseudo-type. - * @param array $info The column information. + * @param array $info The column information. The set of parameters may be different for a specific DBMS. * * @return ColumnSchemaInterface The column schema. * @@ -55,10 +79,10 @@ public function fromPseudoType(string $pseudoType, array $info = []): ColumnSche * Creates a column schema for an abstract database type and initializes column information. * * @param string $type The abstract database type. - * @param array $info The column information. + * @param array $info The column information. The set of parameters may be different for a specific DBMS. * * @psalm-param ColumnType::* $type - * @psalm-param ColumnInfo $info The set of parameters may be different for a specific DBMS. + * @psalm-param ColumnInfo $info */ public function fromType(string $type, array $info = []): ColumnSchemaInterface; } diff --git a/src/Schema/Column/ColumnSchemaInterface.php b/src/Schema/Column/ColumnSchemaInterface.php index 4c5ff4388..ffaf24330 100644 --- a/src/Schema/Column/ColumnSchemaInterface.php +++ b/src/Schema/Column/ColumnSchemaInterface.php @@ -11,31 +11,6 @@ /** * This interface defines a set of methods that must be implemented by a class that represents the column schema of a * database table column. - * - * @psalm-type ColumnInfo = array{ - * auto_increment?: bool|string, - * check?: string|null, - * column?: ColumnSchemaInterface|null, - * columns?: array, - * comment?: string|null, - * computed?: bool|string, - * db_type?: string|null, - * default_value?: mixed, - * dimension?: int|string, - * enum_values?: array|null, - * extra?: string|null, - * primary_key?: bool|string, - * not_null?: bool|string, - * reference?: ForeignKeyConstraint|null, - * scale?: int|string|null, - * schema?: string|null, - * size?: int|string|null, - * table?: string|null, - * type?: ColumnType::*, - * unique?: bool|string, - * unsigned?: bool|string, - * ... - * } */ interface ColumnSchemaInterface { @@ -322,13 +297,6 @@ public function isUnique(): bool; */ public function isUnsigned(): bool; - /** - * Loads the column's schema information from an array. - * - * @psalm-param ColumnInfo $info - */ - public function load(array $info): static; - /** * Sets a name of the column. * @@ -382,7 +350,7 @@ public function precision(int|null $precision): static; * ]; * ``` */ - public function primaryKey(bool $isPrimaryKey = true): static; + public function primaryKey(bool $primaryKey = true): static; /** * The reference to the foreign key constraint. diff --git a/src/Schema/Column/DoubleColumnSchema.php b/src/Schema/Column/DoubleColumnSchema.php index a84031329..674e2abb0 100644 --- a/src/Schema/Column/DoubleColumnSchema.php +++ b/src/Schema/Column/DoubleColumnSchema.php @@ -10,16 +10,12 @@ use function is_float; +/** + * Represents the metadata for a double column. + */ class DoubleColumnSchema extends AbstractColumnSchema { - /** - * @psalm-param ColumnType::* $type - */ - public function __construct( - string $type = ColumnType::DOUBLE, - ) { - parent::__construct($type); - } + protected const DEFAULT_TYPE = ColumnType::DOUBLE; public function dbTypecast(mixed $value): float|ExpressionInterface|null { diff --git a/src/Schema/Column/IntegerColumnSchema.php b/src/Schema/Column/IntegerColumnSchema.php index 27cd92b58..30513ed80 100644 --- a/src/Schema/Column/IntegerColumnSchema.php +++ b/src/Schema/Column/IntegerColumnSchema.php @@ -10,16 +10,12 @@ use function is_int; +/** + * Represents the schema for an integer column. + */ class IntegerColumnSchema extends AbstractColumnSchema { - /** - * @psalm-param ColumnType::* $type - */ - public function __construct( - string $type = ColumnType::INTEGER, - ) { - parent::__construct($type); - } + protected const DEFAULT_TYPE = ColumnType::INTEGER; public function dbTypecast(mixed $value): int|ExpressionInterface|null { diff --git a/src/Schema/Column/JsonColumnSchema.php b/src/Schema/Column/JsonColumnSchema.php index cf4015351..9ddf626f3 100644 --- a/src/Schema/Column/JsonColumnSchema.php +++ b/src/Schema/Column/JsonColumnSchema.php @@ -11,16 +11,12 @@ use function is_string; use function json_decode; +/** + * Represents the schema for a json column. + */ class JsonColumnSchema extends AbstractColumnSchema { - /** - * @psalm-param ColumnType::* $type - */ - public function __construct( - string $type = ColumnType::JSON, - ) { - parent::__construct($type); - } + protected const DEFAULT_TYPE = ColumnType::JSON; public function dbTypecast(mixed $value): ExpressionInterface|null { diff --git a/src/Schema/Column/StringColumnSchema.php b/src/Schema/Column/StringColumnSchema.php index c2b6c6510..992abe7de 100644 --- a/src/Schema/Column/StringColumnSchema.php +++ b/src/Schema/Column/StringColumnSchema.php @@ -11,16 +11,12 @@ use function gettype; +/** + * Represents the metadata for a string column. + */ class StringColumnSchema extends AbstractColumnSchema { - /** - * @psalm-param ColumnType::* $type - */ - public function __construct( - string $type = ColumnType::STRING, - ) { - parent::__construct($type); - } + protected const DEFAULT_TYPE = ColumnType::STRING; public function dbTypecast(mixed $value): mixed { diff --git a/src/Schema/Column/StructuredColumnSchema.php b/src/Schema/Column/StructuredColumnSchema.php index 241132989..d71315d41 100644 --- a/src/Schema/Column/StructuredColumnSchema.php +++ b/src/Schema/Column/StructuredColumnSchema.php @@ -12,15 +12,21 @@ use Yiisoft\Db\Syntax\ParserToArrayInterface; use function array_keys; +use function is_array; use function is_string; +/** + * Represents the schema for a structured column. + */ class StructuredColumnSchema extends AbstractColumnSchema { + protected const DEFAULT_TYPE = ColumnType::STRUCTURED; + /** * @var ColumnSchemaInterface[] Columns metadata of the structured type. * @psalm-var array */ - private array $columns = []; + protected array $columns = []; /** * Returns the parser for the column value. @@ -30,15 +36,6 @@ protected function getParser(): ParserToArrayInterface throw new NotSupportedException(__METHOD__ . '() is not supported. Use concrete DBMS implementation.'); } - /** - * @psalm-param ColumnType::* $type - */ - public function __construct( - string $type = ColumnType::STRUCTURED, - ) { - parent::__construct($type); - } - /** * Set columns of the structured type. * diff --git a/src/Syntax/ColumnDefinitionParser.php b/src/Syntax/ColumnDefinitionParser.php index 7c66953d0..1800596bb 100644 --- a/src/Syntax/ColumnDefinitionParser.php +++ b/src/Syntax/ColumnDefinitionParser.php @@ -4,7 +4,7 @@ namespace Yiisoft\Db\Syntax; -use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; +use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; use function explode; use function preg_match; @@ -19,7 +19,7 @@ /** * Parses column definition string. For example, `string(255)` or `int unsigned`. * - * @psalm-import-type ColumnInfo from ColumnSchemaInterface + * @psalm-import-type ColumnInfo from ColumnFactoryInterface */ final class ColumnDefinitionParser { @@ -36,11 +36,11 @@ public function parse(string $definition): array { preg_match('/^(\w*)(?:\(([^)]+)\))?\s*/', $definition, $matches); - $dbType = strtolower($matches[1]); - $info = ['db_type' => $dbType]; + $type = strtolower($matches[1]); + $info = ['type' => $type]; if (isset($matches[2])) { - if ($dbType === 'enum') { + if ($type === 'enum') { $info += $this->enumInfo($matches[2]); } else { $info += $this->sizeInfo($matches[2]); @@ -57,7 +57,7 @@ private function enumInfo(string $values): array { preg_match_all("/'([^']*)'/", $values, $matches); - return ['enum_values' => $matches[1]]; + return ['enumValues' => $matches[1]]; } private function extraInfo(string $extra): array diff --git a/tests/Db/Schema/ColumnSchemaTest.php b/tests/Db/Schema/ColumnSchemaTest.php index 449646517..3f1539653 100644 --- a/tests/Db/Schema/ColumnSchemaTest.php +++ b/tests/Db/Schema/ColumnSchemaTest.php @@ -155,12 +155,10 @@ public function testExtra(): void $this->assertSame('', $column->getExtra()); } - /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnSchemaProvider::load */ - public function testLoad(string $parameter, mixed $value, string $method, mixed $expected): void + /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnSchemaProvider::construct */ + public function testConstruct(string $parameter, mixed $value, string $method, mixed $expected): void { - $column = new ColumnSchema(); - - $column->load([$parameter => $value]); + $column = new ColumnSchema(...[$parameter => $value]); $this->assertSame($expected, $column->$method()); } diff --git a/tests/Provider/ColumnDefinitionParserProvider.php b/tests/Provider/ColumnDefinitionParserProvider.php index 2ad24ab29..fe7b2eede 100644 --- a/tests/Provider/ColumnDefinitionParserProvider.php +++ b/tests/Provider/ColumnDefinitionParserProvider.php @@ -9,16 +9,16 @@ class ColumnDefinitionParserProvider public static function parse(): array { return [ - ['', ['db_type' => '']], - ['int', ['db_type' => 'int']], - ['int(10)', ['db_type' => 'int', 'size' => 10]], - ['int UNSIGNED', ['db_type' => 'int', 'unsigned' => true]], - ['int(10) UNSIGNED', ['db_type' => 'int', 'size' => 10, 'unsigned' => true]], - ['int(10) UNSIGNED NOT NULL', ['db_type' => 'int', 'size' => 10, 'unsigned' => true, 'extra' => 'NOT NULL']], - ['int(10) NOT NULL', ['db_type' => 'int', 'size' => 10, 'extra' => 'NOT NULL']], - ['text NOT NULL', ['db_type' => 'text', 'extra' => 'NOT NULL']], - ["enum('a','b','c')", ['db_type' => 'enum', 'enum_values' => ['a', 'b', 'c']]], - ["enum('a','b','c') NOT NULL", ['db_type' => 'enum', 'enum_values' => ['a', 'b', 'c'], 'extra' => 'NOT NULL']], + ['', ['type' => '']], + ['int', ['type' => 'int']], + ['int(10)', ['type' => 'int', 'size' => 10]], + ['int UNSIGNED', ['type' => 'int', 'unsigned' => true]], + ['int(10) UNSIGNED', ['type' => 'int', 'size' => 10, 'unsigned' => true]], + ['int(10) UNSIGNED NOT NULL', ['type' => 'int', 'size' => 10, 'unsigned' => true, 'extra' => 'NOT NULL']], + ['int(10) NOT NULL', ['type' => 'int', 'size' => 10, 'extra' => 'NOT NULL']], + ['text NOT NULL', ['type' => 'text', 'extra' => 'NOT NULL']], + ["enum('a','b','c')", ['type' => 'enum', 'enumValues' => ['a', 'b', 'c']]], + ["enum('a','b','c') NOT NULL", ['type' => 'enum', 'enumValues' => ['a', 'b', 'c'], 'extra' => 'NOT NULL']], ]; } } diff --git a/tests/Provider/ColumnFactoryProvider.php b/tests/Provider/ColumnFactoryProvider.php index 7b7f7f778..77ff08a8a 100644 --- a/tests/Provider/ColumnFactoryProvider.php +++ b/tests/Provider/ColumnFactoryProvider.php @@ -37,7 +37,7 @@ public static function pseudoTypes(): array 'pk' => [PseudoType::PK, ColumnType::INTEGER, IntegerColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], 'upk' => [PseudoType::UPK, ColumnType::INTEGER, IntegerColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true, 'isUnsigned' => true]], 'bigpk' => [PseudoType::BIGPK, ColumnType::BIGINT, IntegerColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], - 'ubigpk' => [PseudoType::UBIGPK, ColumnType::BIGINT, IntegerColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true, 'isUnsigned' => true]], + 'ubigpk' => [PseudoType::UBIGPK, ColumnType::BIGINT, BigIntColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true, 'isUnsigned' => true]], 'uuid_pk' => [PseudoType::UUID_PK, ColumnType::UUID, StringColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], 'uuid_pk_seq' => [PseudoType::UUID_PK_SEQ, ColumnType::UUID, StringColumnSchema::class, ['isPrimaryKey' => true, 'isAutoIncrement' => true]], ]; diff --git a/tests/Provider/ColumnSchemaProvider.php b/tests/Provider/ColumnSchemaProvider.php index 6f00d19b2..806f03457 100644 --- a/tests/Provider/ColumnSchemaProvider.php +++ b/tests/Provider/ColumnSchemaProvider.php @@ -366,46 +366,32 @@ public static function dbTypecastArrayColumns() ]; } - public static function load(): array + public static function construct(): array { return [ // parameter, value, method to get value, expected value - ['allow_null', true, 'isAllowNull', true], - ['allow_null', false, 'isAllowNull', false], - ['allow_null', '1', 'isAllowNull', true], - ['allow_null', '0', 'isAllowNull', false], - ['auto_increment', true, 'isAutoIncrement', true], - ['auto_increment', false, 'isAutoIncrement', false], - ['auto_increment', '1', 'isAutoIncrement', true], - ['auto_increment', '0', 'isAutoIncrement', false], + ['autoIncrement', true, 'isAutoIncrement', true], + ['autoIncrement', false, 'isAutoIncrement', false], ['check', 'age > 0', 'getCheck', 'age > 0'], ['check', null, 'getCheck', null], ['comment', 'Lorem ipsum', 'getComment', 'Lorem ipsum'], ['comment', null, 'getComment', null], ['computed', true, 'isComputed', true], ['computed', false, 'isComputed', false], - ['computed', '1', 'isComputed', true], - ['computed', '0', 'isComputed', false], - ['db_type', 'integer', 'getDbType', 'integer'], - ['db_type', null, 'getDbType', null], - ['default_value', 'default_value', 'getDefaultValue', 'default_value'], - ['default_value', null, 'getDefaultValue', null], - ['enum_values', ['value1', 'value2'], 'getEnumValues', ['value1', 'value2']], - ['enum_values', null, 'getEnumValues', null], + ['dbType', 'integer', 'getDbType', 'integer'], + ['dbType', null, 'getDbType', null], + ['defaultValue', 'default_value', 'getDefaultValue', 'default_value'], + ['defaultValue', null, 'getDefaultValue', null], + ['enumValues', ['value1', 'value2'], 'getEnumValues', ['value1', 'value2']], + ['enumValues', null, 'getEnumValues', null], ['extra', 'CHARACTER SET utf8mb4', 'getExtra', 'CHARACTER SET utf8mb4'], ['extra', null, 'getExtra', null], ['name', 'name', 'getName', 'name'], ['name', null, 'getName', null], - ['not_null', true, 'isNotNull', true], - ['not_null', false, 'isNotNull', false], - ['not_null', '1', 'isNotNull', true], - ['not_null', '0', 'isNotNull', false], - ['precision', 10, 'getPrecision', 10], - ['precision', null, 'getPrecision', null], - ['primary_key', true, 'isPrimaryKey', true], - ['primary_key', false, 'isPrimaryKey', false], - ['primary_key', '1', 'isPrimaryKey', true], - ['primary_key', '0', 'isPrimaryKey', false], + ['notNull', true, 'isNotNull', true], + ['notNull', false, 'isNotNull', false], + ['primaryKey', true, 'isPrimaryKey', true], + ['primaryKey', false, 'isPrimaryKey', false], ['reference', $fk = new ForeignKeyConstraint(), 'getReference', $fk], ['reference', null, 'getReference', null], ['scale', 2, 'getScale', 2], @@ -414,12 +400,8 @@ public static function load(): array ['size', null, 'getSize', null], ['unique', true, 'isUnique', true], ['unique', false, 'isUnique', false], - ['unique', '1', 'isUnique', true], - ['unique', '0', 'isUnique', false], ['unsigned', true, 'isUnsigned', true], ['unsigned', false, 'isUnsigned', false], - ['unsigned', '1', 'isUnsigned', true], - ['unsigned', '0', 'isUnsigned', false], ]; } } diff --git a/tests/Support/Stub/ColumnSchema.php b/tests/Support/Stub/ColumnSchema.php index a01a6b2e7..2d5ab86ee 100644 --- a/tests/Support/Stub/ColumnSchema.php +++ b/tests/Support/Stub/ColumnSchema.php @@ -8,11 +8,7 @@ final class ColumnSchema extends AbstractColumnSchema { - public function __construct( - private string $type = '', - ) { - parent::__construct($type); - } + protected const DEFAULT_TYPE = ''; public function dbTypecast(mixed $value): mixed { From 42181e13bf2e538ab655a91d7815d171af25b744 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 18 Oct 2024 22:19:16 +0300 Subject: [PATCH 8/8] Improve psalm annotations in `ColumnDefinitionParser` (#891) --- src/Syntax/ColumnDefinitionParser.php | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Syntax/ColumnDefinitionParser.php b/src/Syntax/ColumnDefinitionParser.php index 1800596bb..cc7329b87 100644 --- a/src/Syntax/ColumnDefinitionParser.php +++ b/src/Syntax/ColumnDefinitionParser.php @@ -4,8 +4,6 @@ namespace Yiisoft\Db\Syntax; -use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; - use function explode; use function preg_match; use function preg_match_all; @@ -18,8 +16,6 @@ /** * Parses column definition string. For example, `string(255)` or `int unsigned`. - * - * @psalm-import-type ColumnInfo from ColumnFactoryInterface */ final class ColumnDefinitionParser { @@ -30,7 +26,14 @@ final class ColumnDefinitionParser * * @return array The column information. * - * @psalm-return ColumnInfo + * @psalm-return array{ + * enumValues?: list, + * extra?: string, + * scale?: int, + * size?: int, + * type: lowercase-string, + * unsigned?: bool, + * } */ public function parse(string $definition): array { @@ -49,10 +52,12 @@ public function parse(string $definition): array $extra = substr($definition, strlen($matches[0])); - /** @var ColumnInfo */ return $info + $this->extraInfo($extra); } + /** + * @psalm-return array{enumValues: list} + */ private function enumInfo(string $values): array { preg_match_all("/'([^']*)'/", $values, $matches); @@ -60,6 +65,9 @@ private function enumInfo(string $values): array return ['enumValues' => $matches[1]]; } + /** + * @psalm-return array{unsigned?: bool, extra?: string} + */ private function extraInfo(string $extra): array { if (empty($extra)) { @@ -80,6 +88,9 @@ private function extraInfo(string $extra): array return $info; } + /** + * @psalm-return array{size: int, scale?: int} + */ private function sizeInfo(string $size): array { $values = explode(',', $size);