From 37249b9ed3bdb07baae315bd7a9364f4cd43abaa Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 7 Feb 2024 14:05:55 +0700 Subject: [PATCH] New columns for v2 --- src/Driver/Pdo/AbstractPdoConnection.php | 9 +- src/QueryBuilder/AbstractDDLQueryBuilder.php | 14 +- src/QueryBuilder/AbstractDMLQueryBuilder.php | 7 +- src/QueryBuilder/AbstractQueryBuilder.php | 39 +-- src/QueryBuilder/ColumnDefinitionBuilder.php | 255 ++++++++++++++++ .../ColumnDefinitionBuilderInterface.php | 19 ++ src/QueryBuilder/DDLQueryBuilderInterface.php | 4 +- src/QueryBuilder/QueryBuilderInterface.php | 13 +- src/Schema/AbstractColumnSchema.php | 278 ----------------- src/Schema/AbstractSchema.php | 44 +-- src/Schema/AbstractTableSchema.php | 34 ++- src/Schema/Builder/AbstractColumn.php | 3 + src/Schema/Builder/ColumnInterface.php | 4 + src/Schema/Column/BigIntColumn.php | 44 +++ src/Schema/Column/BinaryColumn.php | 37 +++ src/Schema/Column/BitColumn.php | 51 ++++ src/Schema/Column/BooleanColumn.php | 37 +++ src/Schema/Column/Column.php | 281 ++++++++++++++++++ src/Schema/Column/ColumnBuilder.php | 102 +++++++ src/Schema/Column/ColumnBuilderInterface.php | 32 ++ src/Schema/Column/ColumnFactory.php | 250 ++++++++++++++++ src/Schema/Column/ColumnFactoryInterface.php | 61 ++++ .../ColumnInterface.php} | 149 ++++++---- src/Schema/Column/DoubleColumn.php | 39 +++ src/Schema/Column/IntegerColumn.php | 39 +++ src/Schema/Column/JsonColumn.php | 45 +++ src/Schema/Column/StringColumn.php | 34 +++ src/Schema/SchemaInterface.php | 56 +++- src/Schema/TableSchemaInterface.php | 13 +- tests/AbstractSchemaTest.php | 4 +- tests/AbstractTableSchemaTest.php | 8 +- tests/Common/CommonColumnSchemaTest.php | 46 +++ tests/Common/CommonSchemaTest.php | 8 + tests/Db/Schema/ColumnSchemaTest.php | 86 +++--- tests/Db/Schema/SchemaTest.php | 71 +---- tests/Provider/ColumnSchemaProvider.php | 218 ++++++++++++++ tests/Support/Stub/ColumnSchema.php | 4 +- 37 files changed, 1889 insertions(+), 549 deletions(-) create mode 100644 src/QueryBuilder/ColumnDefinitionBuilder.php create mode 100644 src/QueryBuilder/ColumnDefinitionBuilderInterface.php delete mode 100644 src/Schema/AbstractColumnSchema.php create mode 100644 src/Schema/Column/BigIntColumn.php create mode 100644 src/Schema/Column/BinaryColumn.php create mode 100644 src/Schema/Column/BitColumn.php create mode 100644 src/Schema/Column/BooleanColumn.php create mode 100644 src/Schema/Column/Column.php create mode 100644 src/Schema/Column/ColumnBuilder.php create mode 100644 src/Schema/Column/ColumnBuilderInterface.php create mode 100644 src/Schema/Column/ColumnFactory.php create mode 100644 src/Schema/Column/ColumnFactoryInterface.php rename src/Schema/{ColumnSchemaInterface.php => Column/ColumnInterface.php} (69%) create mode 100644 src/Schema/Column/DoubleColumn.php create mode 100644 src/Schema/Column/IntegerColumn.php create mode 100644 src/Schema/Column/JsonColumn.php create mode 100644 src/Schema/Column/StringColumn.php create mode 100644 tests/Common/CommonColumnSchemaTest.php create mode 100644 tests/Provider/ColumnSchemaProvider.php diff --git a/src/Driver/Pdo/AbstractPdoConnection.php b/src/Driver/Pdo/AbstractPdoConnection.php index 477c4b5dc..fcb565693 100644 --- a/src/Driver/Pdo/AbstractPdoConnection.php +++ b/src/Driver/Pdo/AbstractPdoConnection.php @@ -19,6 +19,7 @@ use Yiisoft\Db\Profiler\ProfilerAwareInterface; use Yiisoft\Db\Profiler\ProfilerAwareTrait; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; +use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Transaction\TransactionInterface; @@ -46,10 +47,12 @@ abstract class AbstractPdoConnection extends AbstractConnection implements PdoCo protected bool|null $emulatePrepare = null; protected QueryBuilderInterface|null $queryBuilder = null; protected QuoterInterface|null $quoter = null; - protected SchemaInterface|null $schema = null; - public function __construct(protected PdoDriverInterface $driver, protected SchemaCache $schemaCache) - { + public function __construct( + protected PdoDriverInterface $driver, + protected SchemaCache $schemaCache, + protected ColumnFactoryInterface|null $columnFactory = null, + ) { } /** diff --git a/src/QueryBuilder/AbstractDDLQueryBuilder.php b/src/QueryBuilder/AbstractDDLQueryBuilder.php index 095cf04fb..cc3a6eff3 100644 --- a/src/QueryBuilder/AbstractDDLQueryBuilder.php +++ b/src/QueryBuilder/AbstractDDLQueryBuilder.php @@ -6,7 +6,7 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Query\QueryInterface; -use Yiisoft\Db\Schema\Builder\ColumnInterface; +use Yiisoft\Db\Schema\Column\ColumnInterface; use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Schema\SchemaInterface; @@ -39,14 +39,14 @@ public function addCheck(string $table, string $name, string $expression): strin . ' CHECK (' . $this->quoter->quoteSql($expression) . ')'; } - public function addColumn(string $table, string $column, string $type): string + public function addColumn(string $table, string $column, ColumnInterface|string $type): string { return 'ALTER TABLE ' . $this->quoter->quoteTableName($table) . ' ADD ' . $this->quoter->quoteColumnName($column) . ' ' - . $this->queryBuilder->getColumnType($type); + . $this->queryBuilder->buildColumnDefinition($type); } public function addCommentOnColumn(string $table, string $column, string $comment): string @@ -144,7 +144,7 @@ public function alterColumn( . $this->quoter->quoteColumnName($column) . ' ' . $this->quoter->quoteColumnName($column) . ' ' - . $this->queryBuilder->getColumnType($type); + . $this->queryBuilder->buildColumnDefinition($type); } public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string @@ -170,14 +170,14 @@ public function createTable(string $table, array $columns, string $options = nul $cols = []; /** @psalm-var string[] $columns */ - foreach ($columns as $name => $type) { + foreach ($columns as $name => $column) { if (is_string($name)) { $cols[] = "\t" . $this->quoter->quoteColumnName($name) . ' ' - . $this->queryBuilder->getColumnType($type); + . $this->queryBuilder->buildColumnDefinition($column); } else { - $cols[] = "\t" . $type; + $cols[] = "\t" . $column; } } diff --git a/src/QueryBuilder/AbstractDMLQueryBuilder.php b/src/QueryBuilder/AbstractDMLQueryBuilder.php index dc048eb2c..6080c2cee 100644 --- a/src/QueryBuilder/AbstractDMLQueryBuilder.php +++ b/src/QueryBuilder/AbstractDMLQueryBuilder.php @@ -13,7 +13,7 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Query\QueryInterface; -use Yiisoft\Db\Schema\ColumnSchemaInterface; +use Yiisoft\Db\Schema\Column\ColumnInterface; use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Schema\SchemaInterface; @@ -331,6 +331,9 @@ protected function prepareUpdateSets(string $table, array $columns, array $param return [$sets, $params]; } + return [$uniqueNames, $insertNames, null]; + } + /** * Prepare column names and constraints for "upsert" operation. * @@ -460,7 +463,7 @@ static function (Constraint $constraint) use ($quoter, $columns, &$columnNames): * * @deprecated will be removed in version 2.0.0 */ - protected function getTypecastValue(mixed $value, ColumnSchemaInterface $columnSchema = null): mixed + protected function getTypecastValue(mixed $value, ColumnInterface $columnSchema = null): mixed { if ($columnSchema) { return $columnSchema->dbTypecast($value); diff --git a/src/QueryBuilder/AbstractQueryBuilder.php b/src/QueryBuilder/AbstractQueryBuilder.php index 53971f27b..44afec916 100644 --- a/src/QueryBuilder/AbstractQueryBuilder.php +++ b/src/QueryBuilder/AbstractQueryBuilder.php @@ -8,7 +8,7 @@ use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\Condition\Interface\ConditionInterface; -use Yiisoft\Db\Schema\Builder\ColumnInterface; +use Yiisoft\Db\Schema\Column\ColumnInterface; use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Schema\SchemaInterface; @@ -44,7 +44,8 @@ public function __construct( private SchemaInterface $schema, private AbstractDDLQueryBuilder $ddlBuilder, private AbstractDMLQueryBuilder $dmlBuilder, - private AbstractDQLQueryBuilder $dqlBuilder + private AbstractDQLQueryBuilder $dqlBuilder, + private ColumnDefinitionBuilder $columnDefinitionBuilder, ) { } @@ -305,31 +306,10 @@ public function dropView(string $viewName): string return $this->ddlBuilder->dropView($viewName); } + /** @deprecated Use {@see buildColumnDefinition()}. Will be removed in version 3.0.0. */ public function getColumnType(ColumnInterface|string $type): string { - if ($type instanceof ColumnInterface) { - $type = $type->asString(); - } - - if (isset($this->typeMap[$type])) { - return $this->typeMap[$type]; - } - - if (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) { - if (isset($this->typeMap[$matches[1]])) { - return preg_replace( - '/\(.+\)/', - '(' . $matches[2] . ')', - $this->typeMap[$matches[1]] - ) . $matches[3]; - } - } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) { - if (isset($this->typeMap[$matches[1]])) { - return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type); - } - } - - return $type; + return $this->buildColumnDefinition($type); } public function getExpressionBuilder(ExpressionInterface $expression): object @@ -405,4 +385,13 @@ public function upsert( ): string { return $this->dmlBuilder->upsert($table, $insertColumns, $updateColumns, $params); } + + public function buildColumnDefinition(ColumnInterface|string $column): string + { + if (!$column instanceof ColumnInterface) { + $column = $this->schema->getColumnFactory()->fromDefinition($column); + } + + return $this->columnDefinitionBuilder->build($column); + } } diff --git a/src/QueryBuilder/ColumnDefinitionBuilder.php b/src/QueryBuilder/ColumnDefinitionBuilder.php new file mode 100644 index 000000000..d893d2356 --- /dev/null +++ b/src/QueryBuilder/ColumnDefinitionBuilder.php @@ -0,0 +1,255 @@ +clauses as $clause) { + $result .= match ($clause) { + 'type' => $this->buildType($column), + 'null' => $this->buildNull($column), + 'primary_key' => $this->buildPrimaryKey($column), + 'auto_increment' => $this->buildAutoIncrement($column), + 'unique' => $this->buildUnique($column), + 'default' => $this->buildDefault($column), + 'comment' => $this->buildComment($column), + 'check' => $this->buildCheck($column), + 'references' => $this->buildReferences($column), + 'extra' => $this->buildExtra($column), + default => '', + }; + } + + return $result; + } + + protected function buildType(ColumnInterface $column): string + { + return (string) $column->getFullDbType(); + } + + /** + * Builds the null or not null constraint for the column. + * + * @return string A string 'NOT NULL' if {@see ColumnInterface::allowNull} is false, + * 'NULL' if {@see ColumnInterface::allowNull} is true or an empty string otherwise. + */ + protected function buildNull(ColumnInterface $column): string + { + if ($column->isPrimaryKey()) { + return ''; + } + + return match ($column->isAllowNull()) { + true => ' NULL', + false => ' NOT NULL', + default => '', + }; + } + + /** + * Builds the primary key clause for column. + * + * @return string A string containing the PRIMARY KEY keyword. + */ + public function buildPrimaryKey(ColumnInterface $column): string + { + return $column->isPrimaryKey() ? ' PRIMARY KEY' : ''; + } + + /** + * Builds the auto increment clause for column. Default is empty string. + * + * @return string A string containing the AUTOINCREMENT keyword. + */ + public function buildAutoIncrement(ColumnInterface $column): string + { + return ''; + } + + /** + * Builds the unique constraint for the column. + * + * @return string A string 'UNIQUE' if {@see isUnique} is true, otherwise it returns an empty string. + */ + protected function buildUnique(ColumnInterface $column): string + { + if ($column->isPrimaryKey()) { + return ''; + } + + return $column->isUnique() ? ' UNIQUE' : ''; + } + + /** + * Builds the default value specification for the column. + * + * @return string A string containing the DEFAULT keyword and the default value. + */ + protected function buildDefault(ColumnInterface $column): string + { + if ($column->isAutoIncrement()) { + 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(ColumnInterface $column): string|null + { + $value = $column->dbTypecast($column->getDefaultValue()); + + if ($value === null) { + return $column->isAllowNull() === true ? 'NULL' : null; + } + + if ($value instanceof ExpressionInterface) { + return $this->queryBuilder->buildExpression($value); + } + + /** @var string */ + return match (get_debug_type($value)) { + 'int' => (string) $value, + 'float' => DbStringHelper::normalizeFloat((string) $value), + 'bool' => $value ? 'TRUE' : 'FALSE', + default => $this->quoter->quoteValue((string) $value), + }; + } + + /** + * Builds the check constraint for the column. + * + * @return string A string containing the CHECK constraint. + */ + protected function buildCheck(ColumnInterface $column): string + { + $check = $column->getCheck(); + + return !empty($check) ? " CHECK($check)" : ''; + } + + /** + * Builds the unsigned string for column. Default is empty string. + * + * @return string A string containing the UNSIGNED keyword. + */ + protected function buildUnsigned(ColumnInterface $column): string + { + return ''; + } + + /** + * 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(ColumnInterface $column): string + { + $extra = $column->getExtra(); + + return !empty($extra) ? " $extra" : ''; + } + + /** + * 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(ColumnInterface $column): string + { + return ''; + } + + private function buildReferences(ColumnInterface $column): string + { + $reference = $this->buildReferenceDefinition($column); + + if ($reference === null) { + return ''; + } + + return "REFERENCES $reference"; + } + + protected function buildReferenceDefinition(ColumnInterface $column): string|null + { + /** @var ForeignKeyConstraint|null $reference */ + $reference = $column->getReference(); + $table = $reference?->getForeignTableName(); + + if ($table === null) { + return null; + } + + if (null !== $schema = $reference->getForeignSchemaName()) { + $sql = $this->quoter->quoteTableName($schema) . '.' . $this->quoter->quoteTableName($table); + } else { + $sql = $this->quoter->quoteTableName($table); + } + + $columns = $reference->getForeignColumnNames(); + + if (!empty($columns)) { + $sql .= ' (' . $this->queryBuilder->buildColumns($columns) . ')'; + } + + if (null !== $onDelete = $reference->getOnDelete()) { + $sql .= ' ON DELETE ' . $onDelete; + } + + if (null !== $onUpdate = $reference->getOnUpdate()) { + $sql .= ' ON UPDATE ' . $onUpdate; + } + + return $sql; + } +} diff --git a/src/QueryBuilder/ColumnDefinitionBuilderInterface.php b/src/QueryBuilder/ColumnDefinitionBuilderInterface.php new file mode 100644 index 000000000..1e3d90219 --- /dev/null +++ b/src/QueryBuilder/ColumnDefinitionBuilderInterface.php @@ -0,0 +1,19 @@ +name('id'); - * $column->allowNull(false); - * $column->dbType('int(11)'); - * $column->phpType('integer'); - * $column->type('integer'); - * $column->defaultValue(0); - * $column->autoIncrement(true); - * $column->primaryKey(true); - * `` - */ -abstract class AbstractColumnSchema implements ColumnSchemaInterface -{ - private bool $allowNull = false; - private bool $autoIncrement = false; - 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 $phpType = null; - private int|null $precision = null; - private int|null $scale = null; - private int|null $size = null; - private string $type = ''; - private bool $unsigned = false; - - public function __construct(private string $name) - { - } - - public function allowNull(bool $value): void - { - $this->allowNull = $value; - } - - public function autoIncrement(bool $value): void - { - $this->autoIncrement = $value; - } - - public function comment(string|null $value): void - { - $this->comment = $value; - } - - public function computed(bool $value): void - { - $this->computed = $value; - } - - public function dbType(string|null $value): void - { - $this->dbType = $value; - } - - public function dbTypecast(mixed $value): mixed - { - /** - * The default implementation does the same as casting for PHP, but it should be possible to override this with - * annotation of an explicit PDO type. - */ - return $this->typecast($value); - } - - public function defaultValue(mixed $value): void - { - $this->defaultValue = $value; - } - - public function enumValues(array|null $value): void - { - $this->enumValues = $value; - } - - public function extra(string|null $value): void - { - $this->extra = $value; - } - - public function getComment(): string|null - { - return $this->comment; - } - - public function getDbType(): string|null - { - return $this->dbType; - } - - public function getDefaultValue(): mixed - { - return $this->defaultValue; - } - - public function getEnumValues(): array|null - { - return $this->enumValues; - } - - public function getExtra(): string|null - { - return $this->extra; - } - - public function getName(): string - { - return $this->name; - } - - public function getPrecision(): int|null - { - return $this->precision; - } - - public function getPhpType(): string|null - { - return $this->phpType; - } - - public function getScale(): int|null - { - return $this->scale; - } - - public function getSize(): int|null - { - return $this->size; - } - - public function getType(): string - { - return $this->type; - } - - public function isAllowNull(): bool - { - return $this->allowNull; - } - - public function isAutoIncrement(): bool - { - return $this->autoIncrement; - } - - public function isComputed(): bool - { - return $this->computed; - } - - public function isPrimaryKey(): bool - { - return $this->isPrimaryKey; - } - - public function isUnsigned(): bool - { - return $this->unsigned; - } - - public function phpType(string|null $value): void - { - $this->phpType = $value; - } - - public function phpTypecast(mixed $value): mixed - { - return $this->typecast($value); - } - - public function precision(int|null $value): void - { - $this->precision = $value; - } - - public function primaryKey(bool $value): void - { - $this->isPrimaryKey = $value; - } - - public function scale(int|null $value): void - { - $this->scale = $value; - } - - public function size(int|null $value): void - { - $this->size = $value; - } - - public function type(string $value): void - { - $this->type = $value; - } - - public function unsigned(bool $value): void - { - $this->unsigned = $value; - } - - /** - * Converts the input value according to {@see phpType} after retrieval from the database. - * - * If the value is null or an {@see Expression}, it won't be converted. - * - * @param mixed $value The value to be converted. - * - * @return mixed The converted value. - */ - protected function typecast(mixed $value): mixed - { - if ( - $value === null - || $value === '' && !in_array($this->type, [ - SchemaInterface::TYPE_TEXT, - SchemaInterface::TYPE_STRING, - SchemaInterface::TYPE_BINARY, - SchemaInterface::TYPE_CHAR, - ], true) - ) { - return null; - } - - if ($value instanceof ExpressionInterface) { - return $value; - } - - return match ($this->phpType) { - gettype($value) => $value, - SchemaInterface::PHP_TYPE_RESOURCE, - SchemaInterface::PHP_TYPE_STRING - => match (true) { - is_resource($value) => $value, - /** ensure type cast always has . as decimal separator in all locales */ - is_float($value) => DbStringHelper::normalizeFloat($value), - is_bool($value) => $value ? '1' : '0', - default => (string) $value, - }, - SchemaInterface::PHP_TYPE_INTEGER => (int) $value, - /** Treating a 0-bit value as false too (@link https://github.com/yiisoft/yii2/issues/9006) */ - SchemaInterface::PHP_TYPE_BOOLEAN => $value && $value !== "\0", - SchemaInterface::PHP_TYPE_DOUBLE => (float) $value, - default => $value, - }; - } -} diff --git a/src/Schema/AbstractSchema.php b/src/Schema/AbstractSchema.php index f9f39e400..4a3395296 100644 --- a/src/Schema/AbstractSchema.php +++ b/src/Schema/AbstractSchema.php @@ -13,6 +13,9 @@ use Yiisoft\Db\Constraint\IndexConstraint; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Schema\Column\ColumnBuilder; +use Yiisoft\Db\Schema\Column\ColumnFactory; +use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; use function array_change_key_case; use function array_map; use function gettype; @@ -46,8 +49,11 @@ abstract class AbstractSchema implements SchemaInterface private array $tableNames = []; private array $tableMetadata = []; - public function __construct(protected ConnectionInterface $db, private SchemaCache $schemaCache) - { + public function __construct( + protected ConnectionInterface $db, + private SchemaCache $schemaCache, + private ColumnFactoryInterface|null $columnFactory = null, + ) { } /** @@ -127,6 +133,11 @@ abstract protected function loadTableUniques(string $tableName): array; */ abstract protected function loadTableSchema(string $name): TableSchemaInterface|null; + public function getColumnFactory(): ColumnFactoryInterface + { + return $this->columnFactory ??= new ColumnFactory(); + } + public function getDefaultSchema(): string|null { return $this->defaultSchema; @@ -388,35 +399,6 @@ protected function findTableNames(string $schema): array throw new NotSupportedException(static::class . ' does not support fetching all table names.'); } - /** - * Extracts the PHP type from an abstract DB type. - * - * @param ColumnSchemaInterface $column The column schema information. - * - * @return string The PHP type name. - */ - protected function getColumnPhpType(ColumnSchemaInterface $column): string - { - return match ($column->getType()) { - // abstract type => php type - SchemaInterface::TYPE_TINYINT => SchemaInterface::PHP_TYPE_INTEGER, - SchemaInterface::TYPE_SMALLINT => SchemaInterface::PHP_TYPE_INTEGER, - SchemaInterface::TYPE_INTEGER => PHP_INT_SIZE === 4 && $column->isUnsigned() - ? SchemaInterface::PHP_TYPE_STRING - : SchemaInterface::PHP_TYPE_INTEGER, - SchemaInterface::TYPE_BIGINT => PHP_INT_SIZE === 8 && !$column->isUnsigned() - ? SchemaInterface::PHP_TYPE_INTEGER - : SchemaInterface::PHP_TYPE_STRING, - SchemaInterface::TYPE_BOOLEAN => SchemaInterface::PHP_TYPE_BOOLEAN, - SchemaInterface::TYPE_DECIMAL => SchemaInterface::PHP_TYPE_DOUBLE, - SchemaInterface::TYPE_FLOAT => SchemaInterface::PHP_TYPE_DOUBLE, - SchemaInterface::TYPE_DOUBLE => SchemaInterface::PHP_TYPE_DOUBLE, - SchemaInterface::TYPE_BINARY => SchemaInterface::PHP_TYPE_RESOURCE, - SchemaInterface::TYPE_JSON => SchemaInterface::PHP_TYPE_ARRAY, - default => SchemaInterface::PHP_TYPE_STRING, - }; - } - /** * Returns the metadata of the given type for all tables in the given schema. * diff --git a/src/Schema/AbstractTableSchema.php b/src/Schema/AbstractTableSchema.php index e5b6ada95..17764bddd 100644 --- a/src/Schema/AbstractTableSchema.php +++ b/src/Schema/AbstractTableSchema.php @@ -5,6 +5,7 @@ namespace Yiisoft\Db\Schema; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Schema\Column\ColumnInterface; use function array_keys; @@ -13,22 +14,43 @@ */ abstract class AbstractTableSchema implements TableSchemaInterface { - private string|null $schemaName = null; + private string $schemaName = ''; private string $name = ''; - private string|null $fullName = null; private string|null $comment = null; private string|null $sequenceName = null; /** @psalm-var string[] */ private array $primaryKey = []; - /** @psalm-var array */ - private array $columns = []; /** @psalm-var array */ protected array $foreignKeys = []; protected string|null $createSql = null; private string|null $catalogName = null; private string|null $serverName = null; - public function getColumn(string $name): ColumnSchemaInterface|null + /** + * @param ColumnInterface[] $columns + * + * @psalm-param array $columns + */ + public function __construct( + private string $fullName = '', + private array $columns = [], + ) { + $values = explode('.', $this->fullName, 2); + + if (count($values) === 2) { + [$this->schemaName, $this->name] = $values; + } else { + $this->name = $this->fullName; + } + + foreach ($columns as $columnName => $column) { + if ($column->isPrimaryKey()) { + $this->primaryKey[] = $columnName; + } + } + } + + public function getColumn(string $name): ColumnInterface|null { return $this->columns[$name] ?? null; } @@ -103,7 +125,7 @@ public function primaryKey(string $value): void $this->primaryKey[] = $value; } - public function column(string $name, ColumnSchemaInterface $value): void + public function column(string $name, ColumnInterface $value): void { $this->columns[$name] = $value; } diff --git a/src/Schema/Builder/AbstractColumn.php b/src/Schema/Builder/AbstractColumn.php index b778a8d8e..0ca087c3b 100644 --- a/src/Schema/Builder/AbstractColumn.php +++ b/src/Schema/Builder/AbstractColumn.php @@ -6,6 +6,7 @@ use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Helper\DbStringHelper; +use Yiisoft\Db\Schema\Column\Column; use Yiisoft\Db\Schema\SchemaInterface; use function gettype; @@ -26,6 +27,8 @@ * * Provides a fluent interface, which means that the methods can be chained together to create a column schema with * many properties in a single line of code. + * + * @deprecated Use {@see Column}. Will be removed in version 3.0.0. */ abstract class AbstractColumn implements ColumnInterface { diff --git a/src/Schema/Builder/ColumnInterface.php b/src/Schema/Builder/ColumnInterface.php index 220c6bdb9..270800838 100644 --- a/src/Schema/Builder/ColumnInterface.php +++ b/src/Schema/Builder/ColumnInterface.php @@ -4,11 +4,15 @@ namespace Yiisoft\Db\Schema\Builder; +use Yiisoft\Db\Schema\Column\ColumnInterface; + /** * This interface defines the methods that must be implemented by classes that build the schema of a database column. * * It provides methods for setting the column name, type, length, precision, scale, default value, and other properties * of the column, as well as methods for adding constraints, such as a primary key, unique, and not null. + * + * @deprecated Use {@see ColumnInterface}. Will be removed in version 3.0.0. */ interface ColumnInterface { diff --git a/src/Schema/Column/BigIntColumn.php b/src/Schema/Column/BigIntColumn.php new file mode 100644 index 000000000..a6df734c6 --- /dev/null +++ b/src/Schema/Column/BigIntColumn.php @@ -0,0 +1,44 @@ + $value, + $value === '' => null, + $value === false => 0, + PHP_INT_MIN <= $value && $value <= PHP_INT_MAX => (int) $value, + default => (string) $value, + }; + } + + public function phpTypecast(mixed $value): int|string|null + { + return match (true) { + $value === null => null, + PHP_INT_MIN <= $value && $value <= PHP_INT_MAX => (int) $value, + default => (string) $value, + }; + } +} diff --git a/src/Schema/Column/BinaryColumn.php b/src/Schema/Column/BinaryColumn.php new file mode 100644 index 000000000..a56e0b666 --- /dev/null +++ b/src/Schema/Column/BinaryColumn.php @@ -0,0 +1,37 @@ + new Param($value, PDO::PARAM_LOB), + $value === null, is_resource($value), $value instanceof ExpressionInterface => $value, + /** ensure type cast always has . as decimal separator in all locales */ + is_float($value) => DbStringHelper::normalizeFloat($value), + $value === false => '0', + default => (string) $value, + }; + } +} diff --git a/src/Schema/Column/BitColumn.php b/src/Schema/Column/BitColumn.php new file mode 100644 index 000000000..47902d7f1 --- /dev/null +++ b/src/Schema/Column/BitColumn.php @@ -0,0 +1,51 @@ + $value, + $value === '' => null, + default => (int) $value, + }; + } + + public function phpTypecast(mixed $value): int|null + { + if ($value === null) { + return null; + } + + return (int) $value; + } + + public function normalizeDefaultValue(string|null $value): int|ExpressionInterface|null + { + if ($value === null || $this->isComputed() || preg_match("/^\(?NULL\b/i", $value) === 1) { + return null; + } + + if (preg_match("/^[Bb]?'([01]+)'/", $value, $matches) === 1) { + /** @var int */ + return bindec($matches[1]); + } + + return new Expression($value); + } +} diff --git a/src/Schema/Column/BooleanColumn.php b/src/Schema/Column/BooleanColumn.php new file mode 100644 index 000000000..a40deda51 --- /dev/null +++ b/src/Schema/Column/BooleanColumn.php @@ -0,0 +1,37 @@ + $value, + $value === '' => null, + default => (bool) $value, + }; + } + + public function phpTypecast(mixed $value): bool|null + { + if ($value === null) { + return null; + } + + return $value && $value !== "\0"; + } +} diff --git a/src/Schema/Column/Column.php b/src/Schema/Column/Column.php new file mode 100644 index 000000000..bb6a321bc --- /dev/null +++ b/src/Schema/Column/Column.php @@ -0,0 +1,281 @@ +name('id'); + * $column->allowNull(false); + * $column->dbType('int(11)'); + * $column->phpType('integer'); + * $column->type('integer'); + * $column->defaultValue(0); + * $column->autoIncrement(); + * $column->primaryKey(); + * ``` + */ +abstract class Column implements ColumnInterface +{ + private bool|null $allowNull = null; + private bool $autoIncrement = false; + private string|ExpressionInterface|null $check = null; + private string|null $comment = null; + private bool $computed = false; + private string|null $dbType = null; + private mixed $defaultValue = null; + private string|null $extra = null; + private bool $primaryKey = false; + private ForeignKeyConstraint|null $reference = null; + private int|null $scale = null; + private int|null $size = null; + private bool $unique = false; + private bool $unsigned = false; + private array $values = []; + + public function __construct( + private string|null $type = null, + private string|null $phpType = null, + ) { + } + + public function allowNull(bool|null $value = true): static + { + $this->allowNull = $value; + return $this; + } + + public function autoIncrement(bool $value = true): static + { + $this->autoIncrement = $value; + return $this; + } + + public function comment(string $value = null): static + { + $this->comment = $value; + return $this; + } + + public function computed(bool $value = true): static + { + $this->computed = $value; + return $this; + } + + public function dbType(string $value = null): static + { + $this->dbType = $value; + return $this; + } + + public function dbTypecast(mixed $value): mixed + { + return $value; + } + + public function defaultValue(mixed $value = null): static + { + $this->defaultValue = $value; + return $this; + } + + public function extra(string $value = null): static + { + $this->extra = $value; + return $this; + } + + public function getComment(): string|null + { + return $this->comment; + } + + public function getDbType(): string|null + { + return $this->dbType; + } + + public function getDefaultValue(): mixed + { + return $this->defaultValue; + } + + public function getExtra(): string|null + { + return $this->extra; + } + + public function getFullDbType(): string|null + { + if ($this->dbType === null) { + return null; + } + + if ($this->size === null) { + return $this->dbType; + } + + return "$this->dbType($this->size)"; + } + + public function getPhpType(): string|null + { + return $this->phpType; + } + + public function getScale(): int|null + { + return $this->scale; + } + + public function getSize(): int|null + { + return $this->size; + } + + public function getType(): string + { + return $this->type; + } + + public function getValues(): array + { + return $this->values; + } + + public function isAllowNull(): bool|null + { + return $this->allowNull; + } + + public function isAutoIncrement(): bool + { + return $this->autoIncrement; + } + + public function isComputed(): bool + { + return $this->computed; + } + + public function isPrimaryKey(): bool + { + return $this->primaryKey; + } + + public function isUnsigned(): bool + { + return $this->unsigned; + } + + public function load(array $info): static + { + foreach ($info as $key => $value) { + match ($key) { + 'allow_null' => $this->allowNull($value !== null ? (bool) $value : null), + '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), + 'extra' => $this->extra($value !== null ? (string) $value : null), + 'primary_key' => $this->primaryKey((bool) $value), + 'php_type' => $this->phpType($value !== null ? (string) $value : null), + 'scale' => $this->scale($value !== null ? (int) $value : null), + 'size' => $this->size($value !== null ? (int) $value : null), + 'type' => $this->type($value !== null ? (string) $value : null), + 'unsigned' => $this->unsigned((bool) $value), + 'values' => $this->values(is_array($value) ? $value : null), + default => null, + }; + } + + if (array_key_exists('default_value_raw', $info)) { + $this->defaultValue($this->normalizeDefaultValue($info['default_value_raw'])); + } + + return $this; + } + + public function normalizeDefaultValue(string|null $value): mixed + { + if ($value === null || $this->computed || preg_match("/^\(?NULL\b/i", $value) === 1) { + return null; + } + + if (preg_match("/^'(.*)'|^\(([^()]*)\)/s", $value, $matches) === 1) { + return $this->phpTypecast($matches[2] ?? str_replace("''", "'", $matches[1])); + } + + return new Expression($value); + } + + public function phpType(string $value = null): static + { + $this->phpType = $value; + return $this; + } + + public function phpTypecast(mixed $value): mixed + { + return $value; + } + + public function primaryKey(bool $value = true): static + { + $this->primaryKey = $value; + return $this; + } + + public function scale(int $value = null): static + { + $this->scale = $value; + return $this; + } + + public function size(int $value = null): static + { + $this->size = $value; + return $this; + } + + public function type(string $value = null): static + { + $this->type = $value; + return $this; + } + + public function unsigned(bool $value = true): static + { + $this->unsigned = $value; + return $this; + } + + public function values(array $value = []): static + { + $this->values = $value; + return $this; + } +} diff --git a/src/Schema/Column/ColumnBuilder.php b/src/Schema/Column/ColumnBuilder.php new file mode 100644 index 000000000..57e4b8dab --- /dev/null +++ b/src/Schema/Column/ColumnBuilder.php @@ -0,0 +1,102 @@ +primaryKey() + ->autoIncrement($autoIncrement); + } + + public static function upk(bool $autoIncrement = true): ColumnInterface + { + return (new ColumnFactory()) + ->fromType(SchemaInterface::TYPE_INTEGER, ['unsigned' => true]) + ->primaryKey() + ->autoIncrement($autoIncrement); + } + + public static function bigpk(bool $autoIncrement = true): ColumnInterface + { + return (new ColumnFactory()) + ->fromType(SchemaInterface::TYPE_BIGINT) + ->primaryKey() + ->autoIncrement($autoIncrement); + } + + public static function ubigpk(bool $autoIncrement = true): ColumnInterface + { + return (new ColumnFactory()) + ->fromType(SchemaInterface::TYPE_BIGINT, ['unsigned' => true]) + ->primaryKey() + ->autoIncrement($autoIncrement); + } + + public static function uuidpk(bool $autoIncrement = false): ColumnInterface + { + return (new ColumnFactory()) + ->fromType(SchemaInterface::TYPE_UUID) + ->primaryKey() + ->autoIncrement($autoIncrement); + } + + public static function uuidpkseq(): ColumnInterface + { + return static::uuidpk(true); + } + + public static function string(int|null $size = 255): ColumnInterface + { + return (new ColumnFactory()) + ->fromType(SchemaInterface::TYPE_STRING) + ->size($size); + } + + public static function integer(int|null $size = null): ColumnInterface + { + return (new ColumnFactory()) + ->fromType(SchemaInterface::TYPE_INTEGER) + ->size($size); + } + + public static function float(int|null $size = null, int|null $scale = null): ColumnInterface + { + return (new ColumnFactory()) + ->fromType(SchemaInterface::TYPE_FLOAT) + ->size($size) + ->scale($scale); + } + + public static function double(int|null $size = null, int|null $scale = null): ColumnInterface + { + return (new ColumnFactory()) + ->fromType(SchemaInterface::TYPE_DOUBLE) + ->size($size) + ->scale($scale); + } + + public static function decimal(int|null $size = null, int|null $scale = null): ColumnInterface + { + return (new ColumnFactory()) + ->fromType(SchemaInterface::TYPE_DECIMAL) + ->size($size) + ->scale($scale); + } + + public static function money(int|null $size = null, int|null $scale = null): ColumnInterface + { + return (new ColumnFactory()) + ->fromType(SchemaInterface::TYPE_MONEY) + ->size($size) + ->scale($scale); + } + + // ... +} diff --git a/src/Schema/Column/ColumnBuilderInterface.php b/src/Schema/Column/ColumnBuilderInterface.php new file mode 100644 index 000000000..d3771a349 --- /dev/null +++ b/src/Schema/Column/ColumnBuilderInterface.php @@ -0,0 +1,32 @@ +> $fromDbType + * @psalm-param array> $fromType + */ +class ColumnFactory implements ColumnFactoryInterface +{ + private const BUILDERS = [ + 'pk', 'upk', 'bigpk', 'ubigpk', 'uuidpk', 'uuidpkseq', + ]; + + private const TYPES = [ + SchemaInterface::TYPE_UUID, + SchemaInterface::TYPE_CHAR, + SchemaInterface::TYPE_STRING, + SchemaInterface::TYPE_TEXT, + SchemaInterface::TYPE_BINARY, + SchemaInterface::TYPE_BIT, + 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, + SchemaInterface::TYPE_ARRAY, + SchemaInterface::TYPE_COMPOSITE, + ]; + + public function __construct( + private array $fromDbType = [], + private array $fromType = [], + ) { + } + + public function fromDbType(string $dbType, array $info = []): ColumnInterface + { + $info['db_type'] = $dbType; + $type = $info['type'] ?? $this->getType($dbType); + + if (isset($this->fromDbType[$dbType])) { + $phpType = $info['php_type'] ?? $this->getPhpType($type); + return (new $this->fromDbType[$dbType]($type, $phpType))->load($info); + } + + return $this->fromType($type, $info); + } + + public function fromDefinition(string $definition, array $info = []): ColumnInterface + { + preg_match('/^(\w*)(?:\(([^)]+)\))?\s*/', $definition, $matches); + + $dbType = strtolower($matches[1]); + + if (isset($matches[2])) { + if ($dbType === 'enum') { + preg_match_all("/'([^']*)'/", $matches[2], $values); + + $info['values'] = $values[1]; + } else { + $values = explode(',', $matches[2]); + $info['size'] = (int) $values[0]; + + if (count($values) === 2) { + $info['scale'] = (int) $values[1]; + } + } + } + + $extra = substr($definition, strlen($matches[0])); + + if (str_contains($extra, 'unsigned')) { + $info['unsigned'] = true; + $extra = trim(str_replace('unsigned', '', $extra)); + } + + if ($extra !== '') { + if (empty($info['extra'])) { + $info['extra'] = $extra; + } else { + $info['extra'] = $extra . ' ' . $info['extra']; + } + } + + if (in_array($dbType, self::BUILDERS, true)) { + return $this->getBuilderClass()::$dbType()->load($info); + } + + if (in_array($dbType, self::TYPES, true)) { + return $this->fromType($dbType, $info); + } + + return $this->fromDbType($dbType, $info); + } + + public function fromPhpType(string $phpType, array $info = []): ColumnInterface + { + $type = $info['type'] ?? $this->getTypeFromPhp($phpType); + + $column = match ($phpType) { + SchemaInterface::PHP_TYPE_INTEGER => new IntegerColumn($type, $phpType), + SchemaInterface::PHP_TYPE_DOUBLE => new DoubleColumn($type, $phpType), + SchemaInterface::PHP_TYPE_BOOLEAN => new BooleanColumn($type, $phpType), + SchemaInterface::PHP_TYPE_RESOURCE => new BinaryColumn($type, $phpType), + SchemaInterface::PHP_TYPE_ARRAY => new JsonColumn($type, $phpType), + default => new StringColumn($type, $phpType), + }; + + $column->load($info); + + return $column; + } + + public function fromType(string $type, array $info = []): ColumnInterface + { + $info['type'] = $type; + $phpType = $info['php_type'] ?? $this->getPhpType($type); + + if (isset($this->fromType[$type])) { + return (new $this->fromType[$type]($type, $phpType))->load($info); + } + + $isUnsigned = !empty($info['unsigned']); + + if ( + $isUnsigned && PHP_INT_SIZE === 4 && $type === SchemaInterface::TYPE_INTEGER + || ($isUnsigned || PHP_INT_SIZE !== 8) && $type === SchemaInterface::TYPE_BIGINT + ) { + return (new BigIntColumn($type, $phpType))->load($info); + } + + return $this->fromPhpType($phpType, $info); + } + + public function getBuilderClass(): string + { + return ColumnBuilder::class; + } + + /** + * Get the abstract database type from a database column type. + * + * @param string $dbType The database column type. + * + * @return string The abstract database type. + */ + protected function getType(string $dbType): string + { + if (in_array($dbType, self::TYPES, true)) { + return $dbType; + } + + return SchemaInterface::TYPE_STRING; + } + + /** + * Get the PHP type from an abstract database type. + * + * @param string $type The abstract database type. + * + * @return string The PHP type name. + */ + protected function getPhpType(string $type): string + { + return match ($type) { + // abstract type => php type + SchemaInterface::TYPE_BOOLEAN => SchemaInterface::PHP_TYPE_BOOLEAN, + SchemaInterface::TYPE_BIT => SchemaInterface::PHP_TYPE_INTEGER, + SchemaInterface::TYPE_TINYINT => SchemaInterface::PHP_TYPE_INTEGER, + SchemaInterface::TYPE_SMALLINT => SchemaInterface::PHP_TYPE_INTEGER, + SchemaInterface::TYPE_INTEGER => SchemaInterface::PHP_TYPE_INTEGER, + SchemaInterface::TYPE_BIGINT => SchemaInterface::PHP_TYPE_INTEGER, + SchemaInterface::TYPE_DECIMAL => SchemaInterface::PHP_TYPE_DOUBLE, + SchemaInterface::TYPE_FLOAT => SchemaInterface::PHP_TYPE_DOUBLE, + SchemaInterface::TYPE_DOUBLE => SchemaInterface::PHP_TYPE_DOUBLE, + SchemaInterface::TYPE_BINARY => SchemaInterface::PHP_TYPE_RESOURCE, + SchemaInterface::TYPE_JSON => SchemaInterface::PHP_TYPE_ARRAY, + SchemaInterface::TYPE_ARRAY => SchemaInterface::PHP_TYPE_ARRAY, + SchemaInterface::TYPE_COMPOSITE => SchemaInterface::PHP_TYPE_ARRAY, + default => SchemaInterface::PHP_TYPE_STRING, + }; + } + + protected function getTypeFromPhp(string $phpType): string + { + return match ($phpType) { + // php type => abstract type + SchemaInterface::PHP_TYPE_INTEGER => SchemaInterface::TYPE_INTEGER, + SchemaInterface::PHP_TYPE_BOOLEAN => SchemaInterface::TYPE_BOOLEAN, + SchemaInterface::PHP_TYPE_DOUBLE => SchemaInterface::TYPE_DOUBLE, + SchemaInterface::PHP_TYPE_RESOURCE => SchemaInterface::TYPE_BINARY, + SchemaInterface::PHP_TYPE_ARRAY => SchemaInterface::TYPE_JSON, + default => SchemaInterface::TYPE_STRING, + }; + } + + protected function getDbType(string $type): string + { + return match ($type) { + SchemaInterface::TYPE_CHAR => 'char', + SchemaInterface::TYPE_STRING => 'varchar', + SchemaInterface::TYPE_TEXT => 'text', + SchemaInterface::TYPE_TINYINT => 'tinyint', + SchemaInterface::TYPE_SMALLINT => 'smallint', + SchemaInterface::TYPE_INTEGER => 'integer', + SchemaInterface::TYPE_BIGINT => 'bigint', + SchemaInterface::TYPE_FLOAT => 'float', + SchemaInterface::TYPE_DOUBLE => 'double', + SchemaInterface::TYPE_DECIMAL => 'decimal', + SchemaInterface::TYPE_DATETIME => 'datetime', + SchemaInterface::TYPE_TIMESTAMP => 'timestamp', + SchemaInterface::TYPE_TIME => 'time', + SchemaInterface::TYPE_DATE => 'date', + SchemaInterface::TYPE_BINARY => 'blob', + SchemaInterface::TYPE_BOOLEAN => 'bit', + SchemaInterface::TYPE_MONEY => 'decimal', + SchemaInterface::TYPE_JSON => 'jsonb', + SchemaInterface::TYPE_UUID => 'binary', + default => 'varchar', + }; + } +} diff --git a/src/Schema/Column/ColumnFactoryInterface.php b/src/Schema/Column/ColumnFactoryInterface.php new file mode 100644 index 000000000..1fe6dbf40 --- /dev/null +++ b/src/Schema/Column/ColumnFactoryInterface.php @@ -0,0 +1,61 @@ + $this->text()->allowNull(true), + * 'description' => $this->text()->allowNull(false), * ]; * ``` */ - public function allowNull(bool $value): void; + public function allowNull(bool|null $value = true): static; /** * The database assigns auto incremented column a unique value automatically whenever you insert a new row into @@ -32,11 +53,13 @@ public function allowNull(bool $value): void; * * ```php * $columns = [ - * 'id' => $this->primaryKey()->autoIncrement(true), + * 'id' => $this->primaryKey()->autoIncrement(), * ]; * ``` */ - public function autoIncrement(bool $value): void; + public function autoIncrement(bool $value = true): static; + + public function check(string|ExpressionInterface|null $value = null): static; /** * The comment for a column in a database table. @@ -49,7 +72,7 @@ public function autoIncrement(bool $value): void; * ]; * ``` */ - public function comment(string|null $value): void; + public function comment(string|null $value = null): static; /** * A computed column is a virtual column that computes its values from an expression. @@ -62,7 +85,7 @@ public function comment(string|null $value): void; * ]; * ``` */ - public function computed(bool $value): void; + public function computed(bool $value = true): static; /** * The database data-type of column. @@ -77,7 +100,7 @@ public function computed(bool $value): void; * ]; * ``` */ - public function dbType(string|null $value): void; + public function dbType(string|null $value = null): static; /** * Convert a value from its PHP representation to a database-specific representation. @@ -100,18 +123,7 @@ public function dbTypecast(mixed $value): mixed; * ]; * ``` */ - public function defaultValue(mixed $value): void; - - /** - * The list of possible values for the `ENUM` column. - * - * ```php - * $columns = [ - * 'status' => $this->string(16)->enumValues(['active', 'inactive']), - * ]; - * ``` - */ - public function enumValues(array|null $value): void; + public function defaultValue(mixed $value = null): static; /** * Extra SQL to append to the generated SQL for a column. @@ -125,7 +137,9 @@ public function enumValues(array|null $value): void; * ]; * ``` */ - public function extra(string|null $value): void; + public function extra(string|null $value = null): static; + + public function getCheck(): string|ExpressionInterface|null; /** * @return string|null The comment of the column. @@ -138,7 +152,7 @@ public function getComment(): string|null; * @return string|null The database type of the column. * Null means the column has no type in the database. * - * Note that the type includes size for columns supporting it, e.g. `varchar(128)`. The size can be obtained + * Note that the type is not including size for columns supporting it, e.g. `varchar(128)`. The size can be obtained * separately via {@see getSize()}. * * @see dbType() @@ -152,13 +166,6 @@ public function getDbType(): string|null; */ public function getDefaultValue(): mixed; - /** - * @return array|null The enum values of the column. - * - * @see enumValues() - */ - public function getEnumValues(): array|null; - /** * @return string|null The extra SQL for the column. * @@ -167,16 +174,11 @@ public function getEnumValues(): array|null; public function getExtra(): string|null; /** - * @return string The name of the column. - */ - public function getName(): string; - - /** - * @return int|null The precision of the column. + * @return string|null The database type of the column including size, precision and scale. * - * @see precision() + * @see getDbType(), getSize(), getScale() for more details. */ - public function getPrecision(): int|null; + public function getFullDbType(): string|null; /** * @return string|null The PHP type of the column. @@ -185,6 +187,8 @@ public function getPrecision(): int|null; */ public function getPhpType(): string|null; + public function getReference(): ForeignKeyConstraint|null; + /** * @return int|null The scale of the column. * @@ -206,12 +210,19 @@ public function getSize(): int|null; */ public function getType(): string; + /** + * @return array|null The `ENUM`, `SET` or other values of the column. + * + * @see values() + */ + public function getValues(): array|null; + /** * Whether this column is nullable. * * @see allowNull() */ - public function isAllowNull(): bool; + public function isAllowNull(): bool|null; /** * Whether this column is auto incremental. @@ -236,14 +247,30 @@ public function isComputed(): bool; */ public function isPrimaryKey(): bool; + public function isUnique(): bool; + /** - * Whether this column is unsigned. This is only meaningful when {@see type} is `smallint`, `integer` + * Whether this column is unsigned. This is only meaningful when {@see type} is `tinyint`, `smallint`, `integer` * or `bigint`. * * @see unsigned() */ public function isUnsigned(): bool; + /** + * @psalm-param ColumnInfo $info + */ + public function load(array $info): static; + + /** + * Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database. + * + * @param string|null $value The default value retrieved from the database. + * + * @return mixed The normalized default value. + */ + public function normalizeDefaultValue(string|null $value): mixed; + /** * The PHP data type for representing the data stored in the column. * It's determined based on the data type of the column as defined in the database schema. @@ -259,7 +286,7 @@ public function isUnsigned(): bool; * ]; * ``` */ - public function phpType(string|null $value): void; + public function phpType(string|null $value = null): static; /** * Converts the input value according to {@see phpType} after retrieval from the database. @@ -268,17 +295,6 @@ public function phpType(string|null $value): void; */ public function phpTypecast(mixed $value): mixed; - /** - * The precision is the total number of digits that represent the value. - * This is only meaningful when {@see type} is `decimal`. - * - * ```php - * $columns = [ - * 'price' => $this->decimal(10, 2)->precision(10), - * ]; - */ - public function precision(int|null $value): void; - /** * The primary key is a column or set of columns that uniquely identifies each row in a table. * @@ -288,7 +304,9 @@ public function precision(int|null $value): void; * ]; * ``` */ - public function primaryKey(bool $value): void; + public function primaryKey(bool $value = true): static; + + public function reference(ForeignKeyConstraint|null $value = null): static; /** * The scale is the number of digits to the right of the decimal point and is only meaningful when {@see type} is @@ -300,12 +318,12 @@ public function primaryKey(bool $value): void; * ]; * ``` */ - public function scale(int|null $value): void; + public function scale(int|null $value = null): static; /** * The size refers to the number of characters or digits allowed in a column of a database table. The size is - * typically used for character or numeric data types, such as `VARCHAR` or `INT`, to specify the maximum length or - * precision of the data in the column. + * typically used for character or numeric data types, such as `VARCHAR`, `INT` or DECIMAL, to specify the maximum + * length or precision of the data in the column. * * ```php * $columns = [ @@ -313,7 +331,7 @@ public function scale(int|null $value): void; * ]; * ``` */ - public function size(int|null $value): void; + public function size(int|null $value = null): static; /** * The database type of the column. @@ -323,7 +341,9 @@ public function size(int|null $value): void; * 'description' => $this->text()->type('text'), * ]; */ - public function type(string $value): void; + public function type(string|null $value = null): static; + + public function unique(bool $value = true): static; /** * Whether the column type is an unsigned integer. @@ -335,5 +355,16 @@ public function type(string $value): void; * ]; * ``` */ - public function unsigned(bool $value): void; + public function unsigned(bool $value = true): static; + + /** + * The list of possible values for the `ENUM`, `SET` or other column. + * + * ```php + * $columns = [ + * 'status' => $this->string(16)->values(['active', 'inactive']), + * ]; + * ``` + */ + public function values(array $value = []): static; } diff --git a/src/Schema/Column/DoubleColumn.php b/src/Schema/Column/DoubleColumn.php new file mode 100644 index 000000000..136fbeeee --- /dev/null +++ b/src/Schema/Column/DoubleColumn.php @@ -0,0 +1,39 @@ + $value, + $value === '' => null, + default => (float) $value, + }; + } + + public function phpTypecast(mixed $value): float|null + { + if ($value === null) { + return null; + } + + return (float) $value; + } +} diff --git a/src/Schema/Column/IntegerColumn.php b/src/Schema/Column/IntegerColumn.php new file mode 100644 index 000000000..e9d23d10e --- /dev/null +++ b/src/Schema/Column/IntegerColumn.php @@ -0,0 +1,39 @@ + $value, + $value === '' => null, + default => (int) $value, + }; + } + + public function phpTypecast(mixed $value): int|null + { + if ($value === null) { + return null; + } + + return (int) $value; + } +} diff --git a/src/Schema/Column/JsonColumn.php b/src/Schema/Column/JsonColumn.php new file mode 100644 index 000000000..81aa1f934 --- /dev/null +++ b/src/Schema/Column/JsonColumn.php @@ -0,0 +1,45 @@ +dbType('json'); + } + + public function dbTypecast(mixed $value): ExpressionInterface|null + { + if ($value === null || $value instanceof ExpressionInterface) { + return $value; + } + + return new JsonExpression($value, $this->getDbType()); + } + + /** + * @throws \JsonException + */ + public function phpTypecast(mixed $value): mixed + { + if (is_string($value)) { + return json_decode($value, true, 512, JSON_THROW_ON_ERROR); + } + + return $value; + } +} diff --git a/src/Schema/Column/StringColumn.php b/src/Schema/Column/StringColumn.php new file mode 100644 index 000000000..d9d2a22e9 --- /dev/null +++ b/src/Schema/Column/StringColumn.php @@ -0,0 +1,34 @@ + $value, + /** ensure type cast always has . as decimal separator in all locales */ + is_float($value) => DbStringHelper::normalizeFloat($value), + $value === false => '0', + default => (string) $value, + }; + } +} diff --git a/src/Schema/SchemaInterface.php b/src/Schema/SchemaInterface.php index 5bfa909a0..60ab32d93 100644 --- a/src/Schema/SchemaInterface.php +++ b/src/Schema/SchemaInterface.php @@ -11,6 +11,8 @@ use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Schema\Builder\ColumnInterface; +use Yiisoft\Db\Schema\Column\ColumnBuilder; +use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; /** * Represents the schema for a database table. @@ -120,26 +122,38 @@ interface SchemaInterface extends ConstraintSchemaInterface public const INDEX_BITMAP = 'BITMAP'; /** * Define the abstract column type as a primary key. + * + * @deprecated Use {@see ColumnBuilder::pk()}. Will be removed in version 3.0.0. */ public const TYPE_PK = 'pk'; /** * Define the abstract column type as an `unsigned` primary key. + * + * @deprecated Use {@see ColumnBuilder::upk()}. Will be removed in version 3.0.0. */ public const TYPE_UPK = 'upk'; /** * Define the abstract column type as big primary key. + * + * @deprecated Use {@see ColumnBuilder::bigpk()}. Will be removed in version 3.0.0. */ public const TYPE_BIGPK = 'bigpk'; /** * Define the abstract column type as `unsigned` big primary key. + * + * @deprecated Use {@see ColumnBuilder::ubigpk()}. Will be removed in version 3.0.0. */ public const TYPE_UBIGPK = 'ubigpk'; /** * Define the abstract column type as an `uuid` primary key. + * + * @deprecated Use {@see ColumnBuilder::uuidpk()}. Will be removed in version 3.0.0. */ public const TYPE_UUID_PK = 'uuid_pk'; /** * Define the abstract column type as an`uuid` primary key with a sequence. + * + * @deprecated Use {@see ColumnBuilder::uuidpk(true)}. Will be removed in version 3.0.0. */ public const TYPE_UUID_PK_SEQ = 'uuid_pk_seq'; /** @@ -158,6 +172,18 @@ interface SchemaInterface extends ConstraintSchemaInterface * Define the abstract column type as `text`. */ public const TYPE_TEXT = 'text'; + /** + * Define the abstract column type as `binary`. + */ + public const TYPE_BINARY = 'binary'; + /** + * Define the abstract column type as `boolean`. + */ + public const TYPE_BOOLEAN = 'boolean'; + /** + * Define the abstract column type as `bit`. + */ + public const TYPE_BIT = 'bit'; /** * Define the abstract column type as `tinyint`. */ @@ -186,6 +212,10 @@ interface SchemaInterface extends ConstraintSchemaInterface * Define the abstract column type as `decimal`. */ public const TYPE_DECIMAL = 'decimal'; + /** + * Define the abstract column type as `money`. + */ + public const TYPE_MONEY = 'money'; /** * Define the abstract column type as `datetime`. */ @@ -202,28 +232,18 @@ interface SchemaInterface extends ConstraintSchemaInterface * Define the abstract column type as `date`. */ public const TYPE_DATE = 'date'; - /** - * Define the abstract column type as `binary`. - */ - public const TYPE_BINARY = 'binary'; - /** - * Define the abstract column type as `boolean`. - */ - public const TYPE_BOOLEAN = 'boolean'; - /** - * Define the abstract column type as `money`. - */ - public const TYPE_MONEY = 'money'; /** * Define the abstract column type as `json`. */ public const TYPE_JSON = 'json'; /** - * Define the abstract column type as `jsonb`. - * - * @deprecated will be removed in version 2.0.0. Use `SchemaInterface::TYPE_JSON` instead. + * Define the abstract column type as `array`. */ - public const TYPE_JSONB = 'jsonb'; + public const TYPE_ARRAY = 'array'; + /** + * Define the abstract column type as `composite`. + */ + public const TYPE_COMPOSITE = 'composite'; /** * Define the php type as `integer` for cast to php value. @@ -256,9 +276,13 @@ interface SchemaInterface extends ConstraintSchemaInterface /** * @psalm-param string[]|int[]|int|string|null $length + * + * @deprecated Use {@see getColumnFactory()} or {@see ColumnBuilder}. Will be removed in version 3.0.0. */ public function createColumn(string $type, array|int|string $length = null): ColumnInterface; + public function getColumnFactory(): ColumnFactoryInterface; + /** * @return string|null The default schema name. */ diff --git a/src/Schema/TableSchemaInterface.php b/src/Schema/TableSchemaInterface.php index 47fe0c107..315c87848 100644 --- a/src/Schema/TableSchemaInterface.php +++ b/src/Schema/TableSchemaInterface.php @@ -5,6 +5,7 @@ namespace Yiisoft\Db\Schema; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Schema\Column\ColumnInterface; /** * Represents the metadata of a database table. @@ -22,9 +23,9 @@ interface TableSchemaInterface * * @param string $name The column name. * - * @return ColumnSchemaInterface|null The named column metadata. Null if the named column doesn't exist. + * @return ColumnInterface|null The named column metadata. Null if the named column doesn't exist. */ - public function getColumn(string $name): ColumnSchemaInterface|null; + public function getColumn(string $name): ColumnInterface|null; /** * @return array The names of all columns in this table. @@ -66,10 +67,10 @@ public function getSequenceName(): string|null; public function getPrimaryKey(): array; /** - * @return ColumnSchemaInterface[] The column metadata of this table. - * Array of {@see ColumnSchemaInterface} objects indexed by column names. + * @return ColumnInterface[] The column metadata of this table. + * Array of {@see ColumnInterface} objects indexed by column names. * - * @psalm-return array + * @psalm-return array */ public function getColumns(): array; @@ -125,7 +126,7 @@ public function primaryKey(string $value): void; * * @param string $name The column name. */ - public function column(string $name, ColumnSchemaInterface $value): void; + public function column(string $name, ColumnInterface $value): void; /** * @return string|null The name of the catalog (database) that this table belongs to. Defaults to null, meaning no diff --git a/tests/AbstractSchemaTest.php b/tests/AbstractSchemaTest.php index e868a1a60..b0c64218c 100644 --- a/tests/AbstractSchemaTest.php +++ b/tests/AbstractSchemaTest.php @@ -9,7 +9,7 @@ use Yiisoft\Db\Schema\Builder\ColumnInterface; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Tests\Support\Assert; -use Yiisoft\Db\Tests\Support\Stub\ColumnSchema; +use Yiisoft\Db\Tests\Support\Stub\Column; use Yiisoft\Db\Tests\Support\TestTrait; use function fclose; @@ -30,7 +30,7 @@ public function testCreateColumnSchemaBuilder(): void public function testColumnSchemaDbTypecastWithEmptyCharType(): void { - $columnSchema = new ColumnSchema('new'); + $columnSchema = new Column('new'); $columnSchema->type(SchemaInterface::TYPE_CHAR); $this->assertSame('', $columnSchema->dbTypecast('')); diff --git a/tests/AbstractTableSchemaTest.php b/tests/AbstractTableSchemaTest.php index a22446710..773680025 100644 --- a/tests/AbstractTableSchemaTest.php +++ b/tests/AbstractTableSchemaTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Db\Exception\NotSupportedException; -use Yiisoft\Db\Tests\Support\Stub\ColumnSchema; +use Yiisoft\Db\Tests\Support\Stub\Column; use Yiisoft\Db\Tests\Support\Stub\TableSchema; use Yiisoft\Db\Tests\Support\TestTrait; @@ -49,7 +49,7 @@ public function testGetComment(): void public function testGetColumn(): void { // Defined column schema. - $columnSchema = new ColumnSchema('id'); + $columnSchema = new Column('id'); // Create table schema. $tableSchema = new TableSchema(); @@ -64,7 +64,7 @@ public function testGetColumn(): void public function testGetColumns(): void { // Defined column schema. - $columnSchema = new ColumnSchema('id'); + $columnSchema = new Column('id'); // Create table schema. $tableSchema = new TableSchema(); @@ -79,7 +79,7 @@ public function testGetColumns(): void public function testGetColumnName(): void { // Defined column schema. - $columnSchema = new ColumnSchema('id'); + $columnSchema = new Column('id'); // Create table schema. $tableSchema = new TableSchema(); diff --git a/tests/Common/CommonColumnSchemaTest.php b/tests/Common/CommonColumnSchemaTest.php new file mode 100644 index 000000000..bfabc24a1 --- /dev/null +++ b/tests/Common/CommonColumnSchemaTest.php @@ -0,0 +1,46 @@ +assertSame('column_name', $column->getName()); + $this->assertSame($type, $column->getType()); + $this->assertSame($phpType, $column->getPhpType()); + } + + /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnSchemaProvider::dbTypecastColumns */ + public function testDbTypecastColumns(string $className, array $values) + { + $column = new $className('column_name'); + + foreach ($values as [$expected, $value]) { + if (is_object($expected) && !(is_object($value) && $expected::class === $value::class)) { + $this->assertEquals($expected, $column->dbTypecast($value)); + } else { + $this->assertSame($expected, $column->dbTypecast($value)); + } + } + } + + /** @dataProvider \Yiisoft\Db\Tests\Provider\ColumnSchemaProvider::phpTypecastColumns */ + public function testPhpTypecastColumns(string $className, array $values) + { + $column = new $className('column_name'); + + foreach ($values as [$expected, $value]) { + $this->assertSame($expected, $column->phpTypecast($value)); + } + } +} diff --git a/tests/Common/CommonSchemaTest.php b/tests/Common/CommonSchemaTest.php index c91b4e9ff..e0af4bb9d 100644 --- a/tests/Common/CommonSchemaTest.php +++ b/tests/Common/CommonSchemaTest.php @@ -898,6 +898,14 @@ protected function columnSchema(array $columns, string $table): void ); } + if (isset($expected['unsigned'])) { + $this->assertSame( + $expected['unsigned'], + $column->isUnsigned(), + "unsigned of column $name does not match" + ); + } + /* Pgsql only */ if (isset($expected['dimension'])) { /** @psalm-suppress UndefinedMethod */ diff --git a/tests/Db/Schema/ColumnSchemaTest.php b/tests/Db/Schema/ColumnSchemaTest.php index b2b1a2838..d308b45ca 100644 --- a/tests/Db/Schema/ColumnSchemaTest.php +++ b/tests/Db/Schema/ColumnSchemaTest.php @@ -5,8 +5,12 @@ namespace Yiisoft\Db\Tests\Db\Schema; use PHPUnit\Framework\TestCase; +use Yiisoft\Db\Schema\Column\BooleanColumn; +use Yiisoft\Db\Schema\Column\DoubleColumn; +use Yiisoft\Db\Schema\Column\IntegerColumn; +use Yiisoft\Db\Schema\Column\StringColumn; use Yiisoft\Db\Schema\SchemaInterface; -use Yiisoft\Db\Tests\Support\Stub\ColumnSchema; +use Yiisoft\Db\Tests\Support\Stub\Column; /** * @group db @@ -17,7 +21,7 @@ final class ColumnSchemaTest extends TestCase { public function testAllowNull(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertFalse($column->isAllowNull()); @@ -32,7 +36,7 @@ public function testAllowNull(): void public function testAutoIncrement(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertFalse($column->isAutoIncrement()); @@ -47,7 +51,7 @@ public function testAutoIncrement(): void public function testComment(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertNull($column->getComment()); @@ -62,7 +66,7 @@ public function testComment(): void public function testComputed(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertFalse($column->isComputed()); @@ -77,7 +81,7 @@ public function testComputed(): void public function testDbType(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertNull($column->getDbType()); @@ -92,14 +96,14 @@ public function testDbType(): void public function testDbTypecast(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); - $this->assertNull($column->dbTypecast('')); + $this->assertSame('', $column->dbTypecast('')); } public function testDefaultValue(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertNull($column->getDefaultValue()); @@ -114,7 +118,7 @@ public function testDefaultValue(): void public function testEnumValues(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertNull($column->getEnumValues()); @@ -129,7 +133,7 @@ public function testEnumValues(): void public function testExtra(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertNull($column->getExtra()); @@ -147,7 +151,7 @@ public function testExtra(): void */ public function testTypecastIssue718(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $param = [1, 2]; $result = $column->dbTypecast($param); @@ -156,14 +160,14 @@ public function testTypecastIssue718(): void public function testName(): void { - $column = new ColumnSchema('test'); + $column = new Column('test'); $this->assertSame('test', $column->getName()); } public function testPhpType(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertNull($column->getPhpType()); @@ -178,88 +182,76 @@ public function testPhpType(): void public function testPhpTypecast(): void { - $column = new ColumnSchema('new'); - - $column->phpType(SchemaInterface::PHP_TYPE_STRING); + $column = new StringColumn('new'); $this->assertSame('test', $column->phpTypecast('test')); } public function testPhpTypecastWithBoolean(): void { - $column = new ColumnSchema('new'); - - $column->phpType(SchemaInterface::PHP_TYPE_BOOLEAN); + $column = new BooleanColumn('new'); $this->assertTrue($column->phpTypecast(1)); } public function testPhpTypecastWithDouble(): void { - $column = new ColumnSchema('new'); - - $column->phpType(SchemaInterface::PHP_TYPE_DOUBLE); + $column = new DoubleColumn('new'); $this->assertSame(1.2, $column->phpTypecast('1.2')); } public function testPhpTypecastWithInteger(): void { - $column = new ColumnSchema('new'); - - $column->phpType(SchemaInterface::PHP_TYPE_INTEGER); + $column = new IntegerColumn('new'); $this->assertSame(1, $column->phpTypecast('1')); } public function testPhpTypecastWithStringBooleanValue(): void { - $column = new ColumnSchema('new'); + self::markTestSkipped('Wrong test: database does not return bool value for string type'); - $column->phpType(SchemaInterface::PHP_TYPE_STRING); + $column = new StringColumn('new'); $this->assertSame('1', $column->phpTypecast(true)); } public function testPhpTypecastWithStringFloatValue(): void { - $column = new ColumnSchema('new'); + self::markTestSkipped('Wrong test: database does not return double value for string type'); - $column->phpType(SchemaInterface::PHP_TYPE_STRING); + $column = new StringColumn('new'); $this->assertSame('1.1', $column->phpTypecast(1.1)); } public function testPhpTypecastWithStringIntegerValue(): void { - $column = new ColumnSchema('new'); + self::markTestSkipped('Wrong test: database does not return int value for string type'); - $column->phpType(SchemaInterface::PHP_TYPE_STRING); + $column = new StringColumn('new'); $this->assertSame('1', $column->phpTypecast(1)); } public function testPhpTypecastWithStringNullValue(): void { - $column = new ColumnSchema('new'); - - $column->phpType(SchemaInterface::PHP_TYPE_STRING); + $column = new StringColumn('new'); $this->assertNull($column->phpTypecast(null)); } public function testPhpTypecastWithStringResourceValue(): void { - $column = new ColumnSchema('new'); - - $column->phpType(SchemaInterface::PHP_TYPE_STRING); + $column = new StringColumn('new'); $this->assertIsResource($column->phpTypecast(fopen('php://memory', 'rb'))); } public function testPrecision(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertNull($column->getPrecision()); @@ -274,22 +266,22 @@ public function testPrecision(): void public function testPrimaryKey(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); - $this->assertFalse($column->isPrimaryKey()); + $this->assertFalse($column->primaryKey()); $column->primaryKey(true); - $this->assertTrue($column->isPrimaryKey()); + $this->assertTrue($column->primaryKey()); $column->primaryKey(false); - $this->assertFalse($column->isPrimaryKey()); + $this->assertFalse($column->primaryKey()); } public function testScale(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertNull($column->getScale()); @@ -304,7 +296,7 @@ public function testScale(): void public function testSize(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertNull($column->getSize()); @@ -319,7 +311,7 @@ public function testSize(): void public function testType(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertSame('', $column->getType()); @@ -334,7 +326,7 @@ public function testType(): void public function testUnsigned(): void { - $column = new ColumnSchema('new'); + $column = new Column('new'); $this->assertFalse($column->isUnsigned()); diff --git a/tests/Db/Schema/SchemaTest.php b/tests/Db/Schema/SchemaTest.php index 7db253b30..f9bc783af 100644 --- a/tests/Db/Schema/SchemaTest.php +++ b/tests/Db/Schema/SchemaTest.php @@ -11,11 +11,12 @@ use Yiisoft\Db\Constraint\ForeignKeyConstraint; use Yiisoft\Db\Constraint\IndexConstraint; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Schema\Column\ColumnBuilder; use Yiisoft\Db\Schema\TableSchemaInterface; use Yiisoft\Db\Tests\AbstractSchemaTest; use Yiisoft\Db\Tests\Support\Assert; use Yiisoft\Db\Tests\Support\DbHelper; -use Yiisoft\Db\Tests\Support\Stub\ColumnSchema; +use Yiisoft\Db\Tests\Support\Stub\Column; use Yiisoft\Db\Tests\Support\Stub\Schema; use Yiisoft\Db\Tests\Support\Stub\TableSchema; use Yiisoft\Db\Tests\Support\TestTrait; @@ -65,33 +66,21 @@ public function testGetColumnPhpType(): void $schema = $db->getSchema(); - $columnBigInt = new ColumnSchema('bigint'); - $columnBigInt->type('bigint'); - - $columnBoolean = new ColumnSchema('boolean'); - $columnBoolean->type('boolean'); - - $columnInteger = new ColumnSchema('integer'); - $columnInteger->type('integer'); - - $columnString = new ColumnSchema('string'); - $columnString->type('string'); - $this->assertSame( 'integer', - Assert::invokeMethod($schema, 'getColumnPhpType', [$columnBigInt]), + Assert::invokeMethod($schema, 'getColumnPhpType', ['bigint']), ); $this->assertSame( 'boolean', - Assert::invokeMethod($schema, 'getColumnPhpType', [$columnBoolean]), + Assert::invokeMethod($schema, 'getColumnPhpType', ['boolean']), ); $this->assertSame( 'integer', - Assert::invokeMethod($schema, 'getColumnPhpType', [$columnInteger]), + Assert::invokeMethod($schema, 'getColumnPhpType', ['integer']), ); $this->assertSame( 'string', - Assert::invokeMethod($schema, 'getColumnPhpType', [$columnString]), + Assert::invokeMethod($schema, 'getColumnPhpType', ['string']), ); } @@ -436,49 +425,15 @@ public function testSetTableMetadata(): void private function createTableSchemaStub(): TableSchemaInterface { - // defined column C_id - $columnCid = new ColumnSchema('C_id'); - $columnCid->autoIncrement(true); - $columnCid->dbType('int'); - $columnCid->primaryKey(true); - $columnCid->phpType('integer'); - $columnCid->type('integer'); - - // defined column C_not_null - $columnCNotNull = new ColumnSchema('C_not_null'); - $columnCNotNull->dbType('int'); - $columnCNotNull->phpType('int'); - $columnCNotNull->type('int'); - - // defined column C_check - $columnCCheck = new ColumnSchema('C_check'); - $columnCCheck->dbType('varchar(255)'); - $columnCCheck->phpType('string'); - $columnCCheck->type('string'); - - // defined column C_default - $columnCDefault = new ColumnSchema('C_default'); - $columnCDefault->dbType('int'); - $columnCDefault->phpType('integer'); - $columnCDefault->type('integer'); - - // defined column C_unique - $columnCUnique = new ColumnSchema('C_unique'); - $columnCUnique->dbType('int'); - $columnCUnique->phpType('integer'); - $columnCUnique->type('integer'); - // defined table T_constraints_1 - $tableSchema = new TableSchema(); - $tableSchema->column('C_id', $columnCid); - $tableSchema->column('C_not_null', $columnCNotNull); - $tableSchema->column('C_check', $columnCCheck); - $tableSchema->column('C_default', $columnCDefault); - $tableSchema->column('C_unique', $columnCUnique); - $tableSchema->fullName('T_constraints_1'); - $tableSchema->name('T_constraints_1'); + $tableSchema = new TableSchema('dbo.T_constraints_1', [ + 'C_id' => ColumnBuilder::pk(), + 'C_not_null' => ColumnBuilder::integer(), + 'C_check' => ColumnBuilder::string(), + 'C_default' => ColumnBuilder::integer(), + 'C_unique' => ColumnBuilder::integer(), + ]); $tableSchema->primaryKey('C_id'); - $tableSchema->schemaName('dbo'); return $tableSchema; } diff --git a/tests/Provider/ColumnSchemaProvider.php b/tests/Provider/ColumnSchemaProvider.php new file mode 100644 index 000000000..27a86e06e --- /dev/null +++ b/tests/Provider/ColumnSchemaProvider.php @@ -0,0 +1,218 @@ + [IntegerColumn::class, SchemaInterface::TYPE_INTEGER, SchemaInterface::PHP_TYPE_INTEGER], + 'bigint' => [BigIntColumn::class, SchemaInterface::TYPE_BIGINT, SchemaInterface::PHP_TYPE_INTEGER], + 'double' => [DoubleColumn::class, SchemaInterface::TYPE_DOUBLE, SchemaInterface::PHP_TYPE_DOUBLE], + 'string' => [StringColumn::class, SchemaInterface::TYPE_STRING, SchemaInterface::PHP_TYPE_STRING], + 'binary' => [BinaryColumn::class, SchemaInterface::TYPE_BINARY, SchemaInterface::PHP_TYPE_RESOURCE], + 'boolean' => [BooleanColumn::class, SchemaInterface::TYPE_BOOLEAN, SchemaInterface::PHP_TYPE_BOOLEAN], + 'json' => [JsonColumn::class, SchemaInterface::TYPE_JSON, SchemaInterface::PHP_TYPE_ARRAY], + ]; + } + + public static function dbTypecastColumns(): array + { + return [ + 'integer' => [ + IntegerColumn::class, + [ + // [expected, typecast value] + [null, null], + [null, ''], + [1, 1], + [1, 1.0], + [1, '1'], + [1, true], + [0, false], + [$expression = new Expression('1'), $expression], + ], + ], + 'bigint' => [ + BigIntColumn::class, + [ + [null, null], + [null, ''], + [1, 1], + [1, 1.0], + [1, '1'], + [1, true], + [0, false], + ['12345678901234567890', '12345678901234567890'], + [$expression = new Expression('1'), $expression], + ], + ], + 'double' => [ + DoubleColumn::class, + [ + [null, null], + [null, ''], + [1.0, 1.0], + [1.0, 1], + [1.0, '1'], + [1.0, true], + [0.0, false], + [$expression = new Expression('1'), $expression], + ], + ], + 'string' => [ + StringColumn::class, + [ + [null, null], + ['', ''], + ['1', 1], + ['1', true], + ['0', false], + ['string', 'string'], + [$resource = fopen('php://memory', 'rb'), $resource], + [$expression = new Expression('expression'), $expression], + ], + ], + 'binary' => [ + BinaryColumn::class, + [ + [null, null], + ['', ''], + ['1', 1], + ['1', true], + ['0', false], + [new Param("\x10\x11\x12", PDO::PARAM_LOB), "\x10\x11\x12"], + [$resource = fopen('php://memory', 'rb'), $resource], + [$expression = new Expression('expression'), $expression], + ], + ], + 'boolean' => [ + BooleanColumn::class, + [ + [null, null], + [null, ''], + [true, true], + [true, 1], + [true, 1.0], + [true, '1'], + [false, false], + [false, 0], + [false, 0.0], + [false, '0'], + [false, "\0"], + [$expression = new Expression('expression'), $expression], + ], + ], + 'json' => [ + JsonColumn::class, + [ + [null, null], + [new JsonExpression('', 'json'), ''], + [new JsonExpression(1, 'json'), 1], + [new JsonExpression(true, 'json'), true], + [new JsonExpression(false, 'json'), false], + [new JsonExpression('string', 'json'), 'string'], + [new JsonExpression([1, 2, 3], 'json'), [1, 2, 3]], + [new JsonExpression(['key' => 'value'], 'json'), ['key' => 'value']], + [new JsonExpression(new stdClass(), 'json'), new stdClass()], + [$expression = new JsonExpression([1, 2, 3]), $expression], + ], + ], + ]; + } + + public static function phpTypecastColumns(): array + { + return [ + 'integer' => [ + IntegerColumn::class, + [ + // [expected, typecast value] + [null, null], + [1, 1], + [1, '1'], + ], + ], + 'bigint' => [ + BigIntColumn::class, + [ + [null, null], + [1, 1], + [1, '1'], + ['12345678901234567890', '12345678901234567890'], + ], + ], + 'double' => [ + DoubleColumn::class, + [ + [null, null], + [1.0, 1.0], + [1.0, '1.0'], + ], + ], + 'string' => [ + StringColumn::class, + [ + [null, null], + ['', ''], + ['string', 'string'], + [$resource = fopen('php://memory', 'rb'), $resource], + ], + ], + 'binary' => [ + BinaryColumn::class, + [ + [null, null], + ['', ''], + ["\x10\x11\x12", "\x10\x11\x12"], + [$resource = fopen('php://memory', 'rb'), $resource], + ], + ], + 'boolean' => [ + BooleanColumn::class, + [ + [null, null], + [true, true], + [true, '1'], + [false, false], + [false, '0'], + [false, "\0"], + ], + ], + 'json' => [ + JsonColumn::class, + [ + [null, null], + ['', '""'], + [1.0, '1.0'], + [1, '1'], + [true, 'true'], + [false, 'false'], + ['string', '"string"'], + [[1, 2, 3], '[1,2,3]'], + [['key' => 'value'], '{"key":"value"}'], + ], + ], + ]; + } +} diff --git a/tests/Support/Stub/ColumnSchema.php b/tests/Support/Stub/ColumnSchema.php index 61809cfba..c270bd66d 100644 --- a/tests/Support/Stub/ColumnSchema.php +++ b/tests/Support/Stub/ColumnSchema.php @@ -4,8 +4,8 @@ namespace Yiisoft\Db\Tests\Support\Stub; -use Yiisoft\Db\Schema\AbstractColumnSchema; +use Yiisoft\Db\Schema\Column\Column; -final class ColumnSchema extends AbstractColumnSchema +final class ColumnSchema extends Column { }