diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b6e1a0a7..803b338a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ - 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) +- Enh #875: Ignore "Packets out of order..." warnings in `AbstractPdoCommand::internalExecute()` method (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/UPGRADE.md b/UPGRADE.md index bbce1e55a..5592abbf3 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -67,6 +67,7 @@ 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; +- `load(array $info)` method is added; - constructor of `AbstractColumnSchema` class is changed to `__construct(string $type, string|null $phpType = null)`; - added method chaining. @@ -88,9 +89,10 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace - `BinaryColumnSchema` for columns with binary type; - `JsonColumnSchema` for columns with json type. -### New methods in `QuoterInterface` +### New methods -- `QuoterInterface::getRawTableName()` - returns the raw table name without quotes. +- `QuoterInterface::getRawTableName()` - returns the raw table name without quotes; +- `SchemaInterface::getColumnFactory()` - returns the column factory. ### Remove methods diff --git a/src/Driver/Pdo/AbstractPdoCommand.php b/src/Driver/Pdo/AbstractPdoCommand.php index 7a31bba2c..bfcb018b5 100644 --- a/src/Driver/Pdo/AbstractPdoCommand.php +++ b/src/Driver/Pdo/AbstractPdoCommand.php @@ -23,6 +23,10 @@ use Yiisoft\Db\Query\Data\DataReader; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; +use function restore_error_handler; +use function set_error_handler; +use function str_starts_with; + /** * Represents a database command that can be executed using a PDO (PHP Data Object) database connection. * @@ -204,7 +208,17 @@ protected function internalExecute(): void $this->isolationLevel ); } else { - $this->pdoStatement?->execute(); + set_error_handler( + static fn(int $errorNumber, string $errorString): bool => + str_starts_with($errorString, 'Packets out of order. Expected '), + E_WARNING, + ); + + try { + $this->pdoStatement?->execute(); + } finally { + restore_error_handler(); + } } break; } catch (PDOException $e) { diff --git a/src/Schema/AbstractSchema.php b/src/Schema/AbstractSchema.php index 0d3532b2e..78b42cc95 100644 --- a/src/Schema/AbstractSchema.php +++ b/src/Schema/AbstractSchema.php @@ -13,15 +13,6 @@ use Yiisoft\Db\Constraint\IndexConstraint; use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Constant\GettypeResult; -use Yiisoft\Db\Schema\Column\BinaryColumnSchema; -use Yiisoft\Db\Schema\Column\BitColumnSchema; -use Yiisoft\Db\Schema\Column\BooleanColumnSchema; -use Yiisoft\Db\Schema\Column\ColumnSchemaInterface; -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\BigIntColumnSchema; use function gettype; use function is_array; @@ -374,49 +365,6 @@ protected function findTableNames(string $schema): array throw new NotSupportedException(static::class . ' does not support fetching all table names.'); } - /** - * Creates a column schema for the database. - * - * This method may be overridden by child classes to create a DBMS-specific column schema. - * - * @param string $type The abstract data type. - * @param mixed ...$info The column information. - * @psalm-param array{unsigned?: bool} $info The set of parameters may be different for a specific DBMS. - * - * @return ColumnSchemaInterface - */ - protected function createColumnSchema(string $type, mixed ...$info): ColumnSchemaInterface - { - $isUnsigned = !empty($info['unsigned']); - - $column = $this->createColumnSchemaFromType($type, $isUnsigned); - $column->unsigned($isUnsigned); - - return $column; - } - - protected function createColumnSchemaFromType(string $type, bool $isUnsigned = false): ColumnSchemaInterface - { - return match ($type) { - SchemaInterface::TYPE_BOOLEAN => new BooleanColumnSchema($type), - SchemaInterface::TYPE_BIT => new BitColumnSchema($type), - SchemaInterface::TYPE_TINYINT => new IntegerColumnSchema($type), - SchemaInterface::TYPE_SMALLINT => new IntegerColumnSchema($type), - SchemaInterface::TYPE_INTEGER => PHP_INT_SIZE !== 8 && $isUnsigned - ? new BigIntColumnSchema($type) - : new IntegerColumnSchema($type), - SchemaInterface::TYPE_BIGINT => PHP_INT_SIZE !== 8 || $isUnsigned - ? new BigIntColumnSchema($type) - : new IntegerColumnSchema($type), - SchemaInterface::TYPE_DECIMAL => new DoubleColumnSchema($type), - SchemaInterface::TYPE_FLOAT => new DoubleColumnSchema($type), - SchemaInterface::TYPE_DOUBLE => new DoubleColumnSchema($type), - SchemaInterface::TYPE_BINARY => new BinaryColumnSchema($type), - SchemaInterface::TYPE_JSON => new JsonColumnSchema($type), - default => new StringColumnSchema($type), - }; - } - /** * Returns the metadata of the given type for all tables in the given schema. * diff --git a/src/Schema/Column/AbstractColumnFactory.php b/src/Schema/Column/AbstractColumnFactory.php new file mode 100644 index 000000000..53bb68745 --- /dev/null +++ b/src/Schema/Column/AbstractColumnFactory.php @@ -0,0 +1,108 @@ +getType($dbType, $info); + + return $this->fromType($type, $info); + } + + public function fromDefinition(string $definition, array $info = []): ColumnSchemaInterface + { + preg_match('/^(\w*)(?:\(([^)]+)\))?\s*/', $definition, $matches); + + $dbType = strtolower($matches[1]); + + if (isset($matches[2])) { + $values = explode(',', $matches[2]); + $info['size'] = (int) $values[0]; + $info['precision'] = (int) $values[0]; + + if (isset($values[1])) { + $info['scale'] = (int) $values[1]; + } + } + + $extra = substr($definition, strlen($matches[0])); + + if (!empty($extra)) { + if (stripos($extra, 'unsigned') !== false) { + $info['unsigned'] = true; + $extra = trim(str_ireplace('unsigned', '', $extra)); + } + + if (!empty($extra)) { + if (empty($info['extra'])) { + $info['extra'] = $extra; + } else { + /** @psalm-suppress MixedOperand */ + $info['extra'] = $extra . ' ' . $info['extra']; + } + } + } + + return $this->fromDbType($dbType, $info); + } + + public function fromType(string $type, array $info = []): ColumnSchemaInterface + { + $column = match ($type) { + SchemaInterface::TYPE_BOOLEAN => new BooleanColumnSchema($type), + SchemaInterface::TYPE_BIT => new BitColumnSchema($type), + SchemaInterface::TYPE_TINYINT => new IntegerColumnSchema($type), + SchemaInterface::TYPE_SMALLINT => new IntegerColumnSchema($type), + SchemaInterface::TYPE_INTEGER => PHP_INT_SIZE !== 8 && !empty($info['unsigned']) + ? new BigIntColumnSchema($type) + : new IntegerColumnSchema($type), + SchemaInterface::TYPE_BIGINT => PHP_INT_SIZE !== 8 || !empty($info['unsigned']) + ? new BigIntColumnSchema($type) + : new IntegerColumnSchema($type), + SchemaInterface::TYPE_DECIMAL => new DoubleColumnSchema($type), + SchemaInterface::TYPE_FLOAT => new DoubleColumnSchema($type), + SchemaInterface::TYPE_DOUBLE => new DoubleColumnSchema($type), + SchemaInterface::TYPE_BINARY => new BinaryColumnSchema($type), + SchemaInterface::TYPE_JSON => new JsonColumnSchema($type), + default => new StringColumnSchema($type), + }; + + return $column->load($info); + } +} diff --git a/src/Schema/Column/AbstractColumnSchema.php b/src/Schema/Column/AbstractColumnSchema.php index f24f15427..2ae050bb9 100644 --- a/src/Schema/Column/AbstractColumnSchema.php +++ b/src/Schema/Column/AbstractColumnSchema.php @@ -6,6 +6,8 @@ use Yiisoft\Db\Constant\PhpType; +use function is_array; + /** * Represents the metadata of a column in a database table. * @@ -21,11 +23,10 @@ * ```php * use Yiisoft\Db\Schema\ColumnSchema; * - * $column = (new ColumnSchema()) + * $column = (new IntegerColumnSchema()) * ->name('id') * ->allowNull(false) * ->dbType('int(11)') - * ->phpType('integer') * ->type('integer') * ->defaultValue(0) * ->autoIncrement() @@ -182,6 +183,35 @@ public function isUnsigned(): bool return $this->unsigned; } + public function load(array $info): static + { + foreach ($info as $key => $value) { + /** + * @psalm-suppress PossiblyInvalidCast + * @psalm-suppress RiskyCast + */ + match ($key) { + 'allow_null' => $this->allowNull((bool) $value), + 'auto_increment' => $this->autoIncrement((bool) $value), + '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), + 'primary_key' => $this->primaryKey((bool) $value), + 'precision' => $this->precision($value !== null ? (int) $value : null), + 'scale' => $this->scale($value !== null ? (int) $value : null), + 'size' => $this->size($value !== null ? (int) $value : null), + 'unsigned' => $this->unsigned((bool) $value), + default => null, + }; + } + + return $this; + } + public function name(string|null $name): static { $this->name = $name; diff --git a/src/Schema/Column/ColumnFactoryInterface.php b/src/Schema/Column/ColumnFactoryInterface.php new file mode 100644 index 000000000..6f4656d5b --- /dev/null +++ b/src/Schema/Column/ColumnFactoryInterface.php @@ -0,0 +1,44 @@ + + * } */ interface ColumnSchemaInterface { @@ -250,7 +271,14 @@ public function isPrimaryKey(): bool; public function isUnsigned(): bool; /** - * Sets the name of the column. + * 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. * * ```php * $columns = [ diff --git a/src/Schema/SchemaInterface.php b/src/Schema/SchemaInterface.php index 1e784b7ac..225e145b2 100644 --- a/src/Schema/SchemaInterface.php +++ b/src/Schema/SchemaInterface.php @@ -11,6 +11,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. @@ -228,6 +229,11 @@ interface SchemaInterface extends ConstraintSchemaInterface */ 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 new file mode 100644 index 000000000..8ade7dbe6 --- /dev/null +++ b/tests/AbstractColumnFactoryTest.php @@ -0,0 +1,73 @@ +getConnection(); + $factory = $db->getSchema()->getColumnFactory(); + + $column = $factory->fromDbType($dbType); + + $this->assertInstanceOf($expectedInstanceOf, $column); + $this->assertSame($expectedType, $column->getType()); + } + + /** + * @dataProvider \Yiisoft\Db\Tests\Provider\ColumnFactoryProvider::definitions + */ + public function testFromDefinition( + string $definition, + string $expectedType, + string $expectedInstanceOf, + array $expectedInfo = [] + ): void { + $db = $this->getConnection(); + $factory = $db->getSchema()->getColumnFactory(); + + $column = $factory->fromDefinition($definition); + + $this->assertInstanceOf($expectedInstanceOf, $column); + $this->assertSame($expectedType, $column->getType()); + + foreach ($expectedInfo as $method => $value) { + $this->assertSame($value, $column->$method()); + } + } + + /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnFactoryProvider::types */ + public function testFromType(string $type, string $expectedType, string $expectedInstanceOf): void + { + $db = $this->getConnection(); + $factory = $db->getSchema()->getColumnFactory(); + + $column = $factory->fromType($type); + + $this->assertInstanceOf($expectedInstanceOf, $column); + $this->assertSame($expectedType, $column->getType()); + } + + public function testFromDefinitionWithExtra(): void + { + $db = $this->getConnection(); + $factory = $db->getSchema()->getColumnFactory(); + + $column = $factory->fromDefinition('char(1) NOT NULL', ['extra' => 'UNIQUE']); + + $this->assertInstanceOf(StringColumnSchema::class, $column); + $this->assertSame('char', $column->getType()); + $this->assertSame(1, $column->getSize()); + $this->assertSame('NOT NULL UNIQUE', $column->getExtra()); + } +} diff --git a/tests/Db/Schema/Column/ColumnFactoryTest.php b/tests/Db/Schema/Column/ColumnFactoryTest.php new file mode 100644 index 000000000..b03aae921 --- /dev/null +++ b/tests/Db/Schema/Column/ColumnFactoryTest.php @@ -0,0 +1,14 @@ +assertSame('', $column->getExtra()); } + /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnSchemaProvider::load */ + public function testLoad(string $parameter, mixed $value, string $method, mixed $expected): void + { + $column = new ColumnSchema(); + + $column->load([$parameter => $value]); + + $this->assertSame($expected, $column->$method()); + } + public function testName(): void { $column = new ColumnSchema(); diff --git a/tests/Provider/ColumnFactoryProvider.php b/tests/Provider/ColumnFactoryProvider.php new file mode 100644 index 000000000..60062d881 --- /dev/null +++ b/tests/Provider/ColumnFactoryProvider.php @@ -0,0 +1,53 @@ + ['', 'string', StringColumnSchema::class], + 'text' => ['text', 'text', StringColumnSchema::class], + 'text NOT NULL' => ['text NOT NULL', 'text', StringColumnSchema::class, ['getExtra' => 'NOT NULL']], + 'char(1)' => ['char(1)', 'char', StringColumnSchema::class, ['getSize' => 1]], + 'decimal(10,2)' => ['decimal(10,2)', 'decimal', DoubleColumnSchema::class, ['getPrecision' => 10, 'getScale' => 2]], + 'bigint UNSIGNED' => ['bigint UNSIGNED', 'bigint', BigIntColumnSchema::class, ['isUnsigned' => true]], + ]; + } + + public static function types(): array + { + return [ + 'uuid' => ['uuid', 'uuid', StringColumnSchema::class], + 'char' => ['char', 'char', StringColumnSchema::class], + 'string' => ['string', 'string', StringColumnSchema::class], + 'text' => ['text', 'text', StringColumnSchema::class], + 'binary' => ['binary', 'binary', BinaryColumnSchema::class], + 'boolean' => ['boolean', 'boolean', BooleanColumnSchema::class], + 'tinyint' => ['tinyint', 'tinyint', IntegerColumnSchema::class], + 'smallint' => ['smallint', 'smallint', IntegerColumnSchema::class], + 'integer' => ['integer', 'integer', IntegerColumnSchema::class], + 'bigint' => ['bigint', 'bigint', IntegerColumnSchema::class], + 'float' => ['float', 'float', DoubleColumnSchema::class], + 'double' => ['double', 'double', DoubleColumnSchema::class], + 'decimal' => ['decimal', 'decimal', DoubleColumnSchema::class], + 'money' => ['money', 'money', StringColumnSchema::class], + 'datetime' => ['datetime', 'datetime', StringColumnSchema::class], + 'timestamp' => ['timestamp', 'timestamp', StringColumnSchema::class], + 'time' => ['time', 'time', StringColumnSchema::class], + 'date' => ['date', 'date', StringColumnSchema::class], + 'json' => ['json', 'json', JsonColumnSchema::class], + ]; + } +} diff --git a/tests/Provider/ColumnSchemaProvider.php b/tests/Provider/ColumnSchemaProvider.php index d5c070476..4ae2a12a2 100644 --- a/tests/Provider/ColumnSchemaProvider.php +++ b/tests/Provider/ColumnSchemaProvider.php @@ -243,4 +243,49 @@ public static function phpTypecastColumns(): array ], ]; } + + public static function load(): 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], + ['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], + ['extra', 'CHARACTER SET utf8mb4', 'getExtra', 'CHARACTER SET utf8mb4'], + ['extra', null, 'getExtra', null], + ['name', 'name', 'getName', 'name'], + ['name', null, 'getName', null], + ['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], + ['scale', 2, 'getScale', 2], + ['scale', null, 'getScale', null], + ['size', 255, 'getSize', 255], + ['size', null, 'getSize', null], + ['unsigned', true, 'isUnsigned', true], + ['unsigned', false, 'isUnsigned', false], + ['unsigned', '1', 'isUnsigned', true], + ['unsigned', '0', 'isUnsigned', false], + ]; + } } diff --git a/tests/Support/Stub/ColumnFactory.php b/tests/Support/Stub/ColumnFactory.php new file mode 100644 index 000000000..e1d4f58a5 --- /dev/null +++ b/tests/Support/Stub/ColumnFactory.php @@ -0,0 +1,42 @@ +isType($dbType) ? $dbType : SchemaInterface::TYPE_STRING; + } + + protected function isType(string $dbType): bool + { + return match ($dbType) { + SchemaInterface::TYPE_UUID, + SchemaInterface::TYPE_CHAR, + SchemaInterface::TYPE_STRING, + SchemaInterface::TYPE_TEXT, + SchemaInterface::TYPE_BINARY, + SchemaInterface::TYPE_BOOLEAN, + SchemaInterface::TYPE_TINYINT, + SchemaInterface::TYPE_SMALLINT, + SchemaInterface::TYPE_INTEGER, + SchemaInterface::TYPE_BIGINT, + SchemaInterface::TYPE_FLOAT, + SchemaInterface::TYPE_DOUBLE, + SchemaInterface::TYPE_DECIMAL, + SchemaInterface::TYPE_MONEY, + SchemaInterface::TYPE_DATETIME, + SchemaInterface::TYPE_TIMESTAMP, + SchemaInterface::TYPE_TIME, + SchemaInterface::TYPE_DATE, + SchemaInterface::TYPE_JSON => true, + default => false, + }; + } +} 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.');