This commit is contained in:
Sebastian Molenda
2026-05-12 21:10:38 +02:00
commit ab96d82fcf
2544 changed files with 721700 additions and 0 deletions
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use Psr\SimpleCache\CacheInterface;
/**
* Decorates a schema collection and adds caching
*/
class CachedCollection implements CollectionInterface
{
/**
* Cacher instance.
*
* @var \Psr\SimpleCache\CacheInterface
*/
protected CacheInterface $cacher;
/**
* The decorated schema collection
*
* @var \Cake\Database\Schema\CollectionInterface
*/
protected CollectionInterface $collection;
/**
* The cache key prefix
*
* @var string
*/
protected string $prefix;
/**
* Constructor.
*
* @param \Cake\Database\Schema\CollectionInterface $collection The collection to wrap.
* @param string $prefix The cache key prefix to use. Typically the connection name.
* @param \Psr\SimpleCache\CacheInterface $cacher Cacher instance.
*/
public function __construct(CollectionInterface $collection, string $prefix, CacheInterface $cacher)
{
$this->collection = $collection;
$this->prefix = $prefix;
$this->cacher = $cacher;
}
/**
* @inheritDoc
*/
public function listTablesWithoutViews(): array
{
return $this->collection->listTablesWithoutViews();
}
/**
* @inheritDoc
*/
public function listTables(): array
{
return $this->collection->listTables();
}
/**
* Get the column metadata for a table.
*
* The name can include a database schema name in the form 'schema.table'.
*
* Caching will be applied if `cacheMetadata` key is present in the Connection
* configuration options. Defaults to _cake_model_ when true.
*
* ### Options
*
* - `forceRefresh` - Set to true to force rebuilding the cached metadata.
* Defaults to false.
*
* @param string $name The name of the table to describe.
* @param array<string, mixed> $options The options to use, see above.
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
*/
public function describe(string $name, array $options = []): TableSchemaInterface
{
$options += ['forceRefresh' => false];
$cacheKey = $this->cacheKey($name);
if (!$options['forceRefresh']) {
$cached = $this->cacher->get($cacheKey);
if ($cached !== null) {
return $cached;
}
}
$table = $this->collection->describe($name, $options);
$this->cacher->set($cacheKey, $table);
return $table;
}
/**
* Get the cache key for a given name.
*
* @param string $name The name to get a cache key for.
* @return string The cache key.
*/
public function cacheKey(string $name): string
{
return $this->prefix . '_' . $name;
}
/**
* Set a cacher.
*
* @param \Psr\SimpleCache\CacheInterface $cacher Cacher object
* @return $this
*/
public function setCacher(CacheInterface $cacher)
{
$this->cacher = $cacher;
return $this;
}
/**
* Get a cacher.
*
* @return \Psr\SimpleCache\CacheInterface $cacher Cacher object
*/
public function getCacher(): CacheInterface
{
return $this->cacher;
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use InvalidArgumentException;
/**
* Check constraint value object
*
* Models a check constraint.
*/
class CheckConstraint extends Constraint
{
protected string $type = self::CHECK;
/**
* Constructor
*
* @param string $name Constraint name.
* @param string $expression The check constraint expression (e.g., "age >= 18")
*/
public function __construct(
protected string $name,
protected string $expression,
) {
}
/**
* Set the check constraint expression.
*
* @param string $expression The SQL expression for the check constraint
* @return $this
* @throws \InvalidArgumentException
*/
public function setExpression(string $expression)
{
if (trim($expression) === '') {
throw new InvalidArgumentException('Check constraint expression cannot be empty');
}
$this->expression = trim($expression);
return $this;
}
/**
* Get the check constraint expression.
*
* @return string
*/
public function getExpression(): string
{
return $this->expression;
}
/**
* Converts a constraint to an array that is compatible
* with the constructor.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'type' => $this->type,
'expression' => $this->expression,
];
}
}
+99
View File
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use Cake\Database\Connection;
/**
* Represents a database schema collection
*
* Gives a simple high-level schema reflection API that can be
* decorated or extended with additional behavior like caching.
*
* @see \Cake\Database\Schema\SchemaDialect For lower level schema reflection API
*/
class Collection implements CollectionInterface
{
/**
* Connection object
*
* @var \Cake\Database\Connection
*/
protected Connection $_connection;
/**
* Schema dialect instance.
*
* @var \Cake\Database\Schema\SchemaDialect|null
*/
protected ?SchemaDialect $_dialect = null;
/**
* Constructor.
*
* @param \Cake\Database\Connection $connection The connection instance.
*/
public function __construct(Connection $connection)
{
$this->_connection = $connection;
}
/**
* Get the list of tables, excluding any views, available in the current connection.
*
* @return array<string> The list of tables in the connected database/schema.
*/
public function listTablesWithoutViews(): array
{
return $this->getDialect()->listTablesWithoutViews();
}
/**
* Get the list of tables and views available in the current connection.
*
* @return array<string> The list of tables and views in the connected database/schema.
*/
public function listTables(): array
{
return $this->getDialect()->listTables();
}
/**
* Get the column metadata for a table.
*
* The name can include a database schema name in the form 'schema.table'.
*
* @param string $name The name of the table to describe.
* @param array<string, mixed> $options Unused
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
*/
public function describe(string $name, array $options = []): TableSchemaInterface
{
return $this->getDialect()->describe($name);
}
/**
* Setups the schema dialect to be used for this collection.
*
* @return \Cake\Database\Schema\SchemaDialect
*/
protected function getDialect(): SchemaDialect
{
return $this->_dialect ??= $this->_connection->getWriteDriver()->schemaDialect();
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
/**
* Represents a database schema collection
*
* Used to access information about the tables,
* and other data in a database.
*
* @method array<string> listTablesWithoutViews() Get the list of tables available in the current connection.
* This will exclude any views in the schema.
*/
interface CollectionInterface
{
/**
* Get the list of tables available in the current connection.
*
* @return array<string> The list of tables in the connected database/schema.
*/
public function listTables(): array;
/**
* Get the column metadata for a table.
*
* Caching will be applied if `cacheMetadata` key is present in the Connection
* configuration options. Defaults to _cake_model_ when true.
*
* ### Options
*
* - `forceRefresh` - Set to true to force rebuilding the cached metadata.
* Defaults to false.
*
* @param string $name The name of the table to describe.
* @param array<string, mixed> $options The options to use, see above.
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
*/
public function describe(string $name, array $options = []): TableSchemaInterface;
}
+595
View File
@@ -0,0 +1,595 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @copyright Copyright (c) Cake Software Foundation, Inc.
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use Cake\Database\TypeFactory;
use RuntimeException;
/**
* Schema metadata for a single column
*
* Used by `TableSchema` when reflecting schema or creating tables.
*/
class Column
{
/**
* Constructor.
*
* @param string $name Name of the column
* @param string $type Type of the column
* @param bool $null Whether the column allows null values
* @param mixed $default Default value for the column
* @param int|null $length Length of the column
* @param bool $identity Whether the column is an identity column
* @param string|null $generated Postgres identity option (always|default)
* @param int|null $precision Precision for decimal or float columns
* @param int|null $increment Increment for identity columns
* @param string|null $after Name of the column to add this column after
* @param string|null $onUpdate MySQL 'ON UPDATE' function
* @param string|null $comment Comment for the column
* @param bool|null $unsigned Whether the column is unsigned
* @param string|null $collate Collation for the column
* @param int|null $srid SRID for geometry fields
* @param string|null $baseType The basic schema type if the column type is a complex/custom type.
*/
public function __construct(
protected string $name,
protected string $type,
protected ?bool $null = null,
protected mixed $default = null,
protected ?int $length = null,
protected bool $identity = false,
protected ?string $generated = null,
protected ?int $precision = null,
protected ?int $increment = null,
protected ?string $after = null,
protected ?string $onUpdate = null,
protected ?string $comment = null,
protected ?bool $unsigned = null,
protected ?string $collate = null,
protected ?int $srid = null,
protected ?string $baseType = null,
) {
}
/**
* Sets the column name.
*
* @param string $name Name
* @return $this
*/
public function setName(string $name)
{
$this->name = $name;
return $this;
}
/**
* Gets the column name.
*
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Get the base type if defined. Will fallback to `type` if not set.
*
* Used to get the base type of a column when the column type is a complex/custom type.
*
* @return string|null
*/
public function getBaseType(): ?string
{
if (isset($this->baseType)) {
return $this->baseType;
}
$type = $this->type;
if (TypeFactory::getMapped($type)) {
$type = TypeFactory::build($type)->getBaseType();
}
return $this->baseType = $type;
}
/**
* Sets the base type of the column.
*
* Used to set the base type of a column when the column type is a complex/custom type.
*
* @param string|null $baseType Base type
* @return $this
*/
public function setBaseType(?string $baseType)
{
$this->baseType = $baseType;
return $this;
}
/**
* Sets the column type.
*
* Type names are not validated, as drivers and dialects may implement
* platform specific types that are not known by cakephp.
*
* Drivers are expected to handle unknown types gracefully.
*
* @param string $type Column type
* @return $this
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
/**
* Gets the column type.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Sets the column length.
*
* @param int|null $length Length
* @return $this
*/
public function setLength(?int $length)
{
$this->length = $length;
return $this;
}
/**
* Gets the column length.
*
* @return int|null
*/
public function getLength(): ?int
{
return $this->length;
}
/**
* Sets whether the column allows nulls.
*
* @param bool $null Null
* @return $this
*/
public function setNull(bool $null)
{
$this->null = $null;
return $this;
}
/**
* Gets whether the column allows nulls.
*
* @return bool|null
*/
public function getNull(): ?bool
{
return $this->null;
}
/**
* Does the column allow nulls?
*
* @return bool
*/
public function isNull(): bool
{
return $this->getNull() === true;
}
/**
* Sets the default column value.
*
* @param mixed $default Default
* @return $this
*/
public function setDefault(mixed $default)
{
$this->default = $default;
return $this;
}
/**
* Gets the default column value.
*
* @return mixed
*/
public function getDefault(): mixed
{
return $this->default;
}
/**
* Sets generated option for identity columns. Ignored otherwise.
*
* @param string|null $generated Generated option
* @return $this
*/
public function setGenerated(?string $generated)
{
$this->generated = $generated;
return $this;
}
/**
* Gets generated option for identity columns. Null otherwise
*
* @return string|null
*/
public function getGenerated(): ?string
{
return $this->generated;
}
/**
* Sets whether the column is an identity column.
*
* @param bool $identity Identity
* @return $this
*/
public function setIdentity(bool $identity)
{
$this->identity = $identity;
return $this;
}
/**
* Gets whether the column is an identity column.
*
* @return bool
*/
public function getIdentity(): bool
{
return $this->identity;
}
/**
* Is the column an identity column?
*
* @return bool
*/
public function isIdentity(): bool
{
return $this->getIdentity();
}
/**
* Sets the name of the column to add this column after.
*
* @param string $after After
* @return $this
*/
public function setAfter(string $after)
{
$this->after = $after;
return $this;
}
/**
* Returns the name of the column to add this column after.
*
* Used by MySQL and MariaDB in ALTER TABLE statements.
*
* @return string|null
*/
public function getAfter(): ?string
{
return $this->after;
}
/**
* Sets the 'ON UPDATE' mysql column function.
*
* Used by MySQL and MariaDB in ALTER TABLE statements.
*
* @param string $update On Update function
* @return $this
*/
public function setOnUpdate(string $update)
{
$this->onUpdate = $update;
return $this;
}
/**
* Returns the value of the ON UPDATE column function.
*
* @return string|null
*/
public function getOnUpdate(): ?string
{
return $this->onUpdate;
}
/**
* Sets the number precision for decimal or float column.
*
* For example `DECIMAL(5,2)`, 5 is the length and 2 is the precision,
* and the column could store value from -999.99 to 999.99.
*
* @param int|null $precision Number precision
* @return $this
*/
public function setPrecision(?int $precision)
{
$this->precision = $precision;
return $this;
}
/**
* Gets the number precision for decimal or float column.
*
* For example `DECIMAL(5,2)`, 5 is the length and 2 is the precision,
* and the column could store value from -999.99 to 999.99.
*
* @return int|null
*/
public function getPrecision(): ?int
{
return $this->precision;
}
/**
* Sets the column identity increment.
*
* @param int $increment Number increment
* @return $this
*/
public function setIncrement(int $increment)
{
$this->increment = $increment;
return $this;
}
/**
* Gets the column identity increment.
*
* @return int|null
*/
public function getIncrement(): ?int
{
return $this->increment;
}
/**
* Sets the column comment.
*
* @param string|null $comment Comment
* @return $this
*/
public function setComment(?string $comment)
{
$this->comment = $comment;
return $this;
}
/**
* Gets the column comment.
*
* @return string
*/
public function getComment(): ?string
{
return $this->comment;
}
/**
* Sets whether field should be unsigned.
*
* @param bool $unsigned Signed
* @return $this
*/
public function setUnsigned(bool $unsigned)
{
$this->unsigned = $unsigned;
return $this;
}
/**
* Gets whether field should be unsigned.
*
* @return bool|null
*/
public function getUnsigned(): ?bool
{
return $this->unsigned;
}
/**
* Should the column be signed?
*
* @return bool
*/
public function isSigned(): bool
{
return !$this->getUnsigned();
}
/**
* Should the column be unsigned?
*
* @return bool
*/
public function isUnsigned(): bool
{
return $this->getUnsigned() === true;
}
/**
* Sets the column collation.
*
* @param string $collation Collation
* @return $this
*/
public function setCollate(string $collation)
{
$this->collate = $collation;
return $this;
}
/**
* Gets the column collation.
*
* @return string|null
*/
public function getCollate(): ?string
{
return $this->collate;
}
/**
* Sets the column SRID for geometry fields.
*
* @param int $srid SRID
* @return $this
*/
public function setSrid(int $srid)
{
$this->srid = $srid;
return $this;
}
/**
* Gets the column SRID from geometry fields.
*
* @return int|null
*/
public function getSrid(): ?int
{
return $this->srid;
}
/**
* Gets all allowed options. Each option must have a corresponding `setFoo` method.
*
* @return array
*/
protected function getValidOptions(): array
{
return [
'name',
'length',
'precision',
'default',
'null',
'identity',
'after',
'onUpdate',
'comment',
'unsigned',
'type',
'properties',
'collate',
'srid',
'increment',
'generated',
];
}
/**
* Utility method that maps an array of column attributes to this object's methods.
*
* @param array<string, mixed> $attributes Attributes
* @throws \RuntimeException
* @return $this
*/
public function setAttributes(array $attributes)
{
$validOptions = $this->getValidOptions();
if (isset($attributes['identity']) && $attributes['identity'] && !isset($attributes['null'])) {
$attributes['null'] = false;
}
foreach ($attributes as $attribute => $value) {
if (!in_array($attribute, $validOptions, true)) {
throw new RuntimeException(sprintf('"%s" is not a valid column option.', $attribute));
}
$method = 'set' . ucfirst($attribute);
$this->$method($value);
}
return $this;
}
/**
* Convert an index into an array that is compatible with the Column constructor.
*
* @return array
*/
public function toArray(): array
{
$type = $this->getType();
$length = $this->getLength();
$precision = $this->getPrecision();
if ($precision !== null && $precision > 0) {
if ($type === TableSchemaInterface::TYPE_TIMESTAMP) {
$type = 'timestampfractional';
} elseif ($type === TableSchemaInterface::TYPE_DATETIME) {
$type = 'datetimefractional';
}
}
return [
'name' => $this->getName(),
'baseType' => $this->getBaseType(),
'type' => $type,
'length' => $length,
'null' => $this->getNull(),
'default' => $this->getDefault(),
'generated' => $this->getGenerated(),
'unsigned' => $this->getUnsigned(),
'onUpdate' => $this->getOnUpdate(),
'collate' => $this->getCollate(),
'precision' => $precision,
'srid' => $this->getSrid(),
'comment' => $this->getComment(),
'autoIncrement' => $this->getIdentity(),
'identity' => $this->getIdentity(),
];
}
}
+145
View File
@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @copyright Copyright (c) Cake Software Foundation, Inc.
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
/**
* Constraint base class object
*
* Models a database constraint like a unique or primary key.
*/
class Constraint
{
/**
* @var string
*/
public const PRIMARY = TableSchema::CONSTRAINT_PRIMARY;
/**
* @var string
*/
public const UNIQUE = TableSchema::CONSTRAINT_UNIQUE;
/**
* @var string
*/
public const FOREIGN = TableSchema::CONSTRAINT_FOREIGN;
/**
* @var string
*/
public const CHECK = TableSchema::CONSTRAINT_CHECK;
/**
* Constructor
*
* @param string $name The name of the constraint.
* @param array<string> $columns The columns to constraint.
* @param string $type The type of constraint, e.g. 'unique', 'primary'.
*/
public function __construct(
protected string $name,
protected array $columns,
protected string $type,
) {
}
/**
* Sets the constraint columns.
*
* @param array<string>|string $columns Columns
* @return $this
*/
public function setColumns(string|array $columns)
{
$this->columns = (array)$columns;
return $this;
}
/**
* Gets the constraint columns.
*
* @return ?array<string>
*/
public function getColumns(): ?array
{
return $this->columns;
}
/**
* Sets the constraint type.
*
* @param string $type Type
* @return $this
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
/**
* Gets the constraint type.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Sets the constraint name.
*
* @param string $name Name
* @return $this
*/
public function setName(string $name)
{
$this->name = $name;
return $this;
}
/**
* Gets the constraint name.
*
* @return ?string
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Converts a constraint to an array that is compatible
* with the constructor.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'type' => $this->type,
'columns' => $this->columns,
];
}
}
+267
View File
@@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @copyright Copyright (c) Cake Software Foundation, Inc.
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use InvalidArgumentException;
/**
* ForeignKey metadata object
*
* Models a database foreign key constraint
*/
class ForeignKey extends Constraint
{
public const CASCADE = 'cascade';
public const RESTRICT = 'restrict';
public const SET_NULL = 'setNull';
public const NO_ACTION = 'noAction';
public const SET_DEFAULT = 'setDefault';
public const DEFERRED = 'DEFERRABLE INITIALLY DEFERRED';
public const IMMEDIATE = 'DEFERRABLE INITIALLY IMMEDIATE';
public const NOT_DEFERRED = 'NOT DEFERRABLE';
/**
* An allow list of valid actions
*
* @var array<string>
*/
protected array $validActions = [
self::CASCADE,
self::RESTRICT,
self::SET_NULL,
self::NO_ACTION,
self::SET_DEFAULT,
];
/**
* The action to take when the referenced row is deleted.
*/
protected ?string $delete = null;
/**
* The action to take when the referenced row is updated.
*/
protected ?string $update = null;
/**
* @var string|null
*/
protected ?string $deferrable = null;
/**
* Constructor
*
* @param string $name The name of the index.
* @param array<string> $columns The columns to index.
* @param ?string $referencedTable The columns to index.
* @param array<string> $referencedColumns The columns in $referencedTable that this key references.
* @param ?string $delete The action to take when the referenced row is deleted.
* @param ?string $update The action to take when the referenced row is updated.
*/
public function __construct(
protected string $name,
protected array $columns,
protected ?string $referencedTable = null,
protected array $referencedColumns = [],
?string $delete = null,
?string $update = null,
?string $deferrable = null,
) {
$this->type = self::FOREIGN;
$this->delete = $this->normalizeAction($delete ?? self::NO_ACTION);
$this->update = $this->normalizeAction($update ?? self::NO_ACTION);
if ($deferrable) {
$this->deferrable = $this->normalizeDeferrable($deferrable);
}
}
/**
* Sets the foreign key referenced table.
*
* @param string $table The table this KEY is pointing to
* @return $this
*/
public function setReferencedTable(string $table)
{
$this->referencedTable = $table;
return $this;
}
/**
* Gets the foreign key referenced table.
*
* @return ?string
*/
public function getReferencedTable(): ?string
{
return $this->referencedTable;
}
/**
* Sets the foreign key referenced columns.
*
* @param array<string>|string $referencedColumns Referenced columns
* @return $this
*/
public function setReferencedColumns(array|string $referencedColumns)
{
$referencedColumns = is_string($referencedColumns) ? [$referencedColumns] : $referencedColumns;
$this->referencedColumns = $referencedColumns;
return $this;
}
/**
* Gets the foreign key referenced columns.
*
* @return array<string>
*/
public function getReferencedColumns(): array
{
return $this->referencedColumns;
}
/**
* Converts the foreign key to an array that is compatible
* with the constructor.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'type' => $this->type,
'columns' => $this->columns,
'referencedTable' => $this->referencedTable,
'referencedColumns' => $this->referencedColumns,
'delete' => $this->delete,
'update' => $this->update,
'deferrable' => $this->deferrable,
];
}
/**
* Sets ON DELETE action for the foreign key.
*
* @param string $delete On Delete
* @return $this
*/
public function setDelete(string $delete)
{
$this->delete = $this->normalizeAction($delete);
return $this;
}
/**
* Gets ON DELETE action for the foreign key.
*
* @return string|null
*/
public function getDelete(): ?string
{
return $this->delete;
}
/**
* Gets ON UPDATE action for the foreign key.
*
* @return string|null
*/
public function getUpdate(): ?string
{
return $this->update;
}
/**
* Sets ON UPDATE action for the foreign key.
*
* @param string $update On Update
* @return $this
*/
public function setUpdate(string $update)
{
$this->update = $this->normalizeAction($update);
return $this;
}
/**
* From passed value checks if it's correct and fixes if needed
*
* @param string $action Action
* @throws \InvalidArgumentException
* @return string
*/
protected function normalizeAction(string $action): string
{
if (in_array($action, $this->validActions, true)) {
return $action;
}
throw new InvalidArgumentException('Unknown action passed: ' . $action);
}
/**
* Sets deferrable mode for the foreign key.
*
* @param string $deferrable Constraint
* @return $this
*/
public function setDeferrable(string $deferrable)
{
$this->deferrable = $this->normalizeDeferrable($deferrable);
return $this;
}
/**
* Gets deferrable mode for the foreign key.
*/
public function getDeferrable(): ?string
{
return $this->deferrable;
}
/**
* From passed value checks if it's correct and fixes if needed
*
* @param string $deferrable Deferrable
* @throws \InvalidArgumentException
* @return string
*/
protected function normalizeDeferrable(string $deferrable): string
{
$mapping = [
'DEFERRED' => ForeignKey::DEFERRED,
'IMMEDIATE' => ForeignKey::IMMEDIATE,
'NOT DEFERRED' => ForeignKey::NOT_DEFERRED,
ForeignKey::DEFERRED => ForeignKey::DEFERRED,
ForeignKey::IMMEDIATE => ForeignKey::IMMEDIATE,
ForeignKey::NOT_DEFERRED => ForeignKey::NOT_DEFERRED,
];
$normalized = strtoupper(str_replace('_', ' ', $deferrable));
if (array_key_exists($normalized, $mapping)) {
return $mapping[$normalized];
}
throw new InvalidArgumentException('Unknown deferrable passed: ' . $deferrable);
}
}
+273
View File
@@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @copyright Copyright (c) Cake Software Foundation, Inc.
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use RuntimeException;
/**
* Index value object
*
* Models a database index and its attributes.
*/
class Index
{
// TODO change the direction of these.
/**
* @var string
*/
public const INDEX = 'index';
/**
* @var string
*/
public const FULLTEXT = 'fulltext';
/**
* Constructor
*
* @param string $name The name of the index.
* @param array<string> $columns The columns to index.
* @param string $type The type of index, e.g. 'index', 'fulltext'.
* @param array<string, int>|int|null $length The length of the index.
* @param array<string>|null $order The sort order of the index columns.
* @param array<string>|null $include The included columns for covering indexes.
* @param ?string $where The where clause for partial indexes.
*/
public function __construct(
protected string $name,
protected array $columns,
protected string $type = self::INDEX,
protected array|int|null $length = null,
protected ?array $order = null,
protected ?array $include = null,
protected ?string $where = null,
) {
}
/**
* Sets the index columns.
*
* @param array<string>|string $columns Columns
* @return $this
*/
public function setColumns(string|array $columns)
{
$this->columns = (array)$columns;
return $this;
}
/**
* Gets the index columns.
*
* @return ?array<string>
*/
public function getColumns(): ?array
{
return $this->columns;
}
/**
* Sets the index type.
*
* @param string $type Type
* @return $this
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
/**
* Gets the index type.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Sets the index name.
*
* @param string $name Name
* @return $this
*/
public function setName(string $name)
{
$this->name = $name;
return $this;
}
/**
* Gets the index name.
*
* @return ?string
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Sets the index length.
*
* In MySQL indexes can have limit clauses to control the number of
* characters indexed in text and char columns.
*
* @param array<string, int>|int $length length value or array of length value
* @return $this
*/
public function setLength(int|array $length)
{
$this->length = $length;
return $this;
}
/**
* Gets the index length.
*
* Can be an array of column names and lengths under MySQL.
*
* @return array<string, int>|int|null
*/
public function getLength(): array|int|null
{
return $this->length;
}
/**
* Sets the index columns sort order.
*
* @param array<string> $order column name sort order key value pair
* @return $this
*/
public function setOrder(array $order)
{
$this->order = $order;
return $this;
}
/**
* Gets the index columns sort order.
*
* @return ?array<string>
*/
public function getOrder(): ?array
{
return $this->order;
}
/**
* Sets the index included columns for a 'covering index'.
*
* In postgres and sqlserver, indexes can define additional non-key
* columns to build 'covering indexes'. This feature allows you to
* further optimize well-crafted queries that leverage specific
* indexes by reading all data from the index.
*
* @param array<string> $includedColumns Columns
* @return $this
*/
public function setInclude(array $includedColumns)
{
$this->include = $includedColumns;
return $this;
}
/**
* Gets the index included columns.
*
* @return ?array<string>
*/
public function getInclude(): ?array
{
return $this->include;
}
/**
* Set the where clause for partial indexes.
*
* @param ?string $where The where clause for partial indexes.
* @return $this
*/
public function setWhere(?string $where)
{
$this->where = $where;
return $this;
}
/**
* Get the where clause for partial indexes.
*
* @return ?string
*/
public function getWhere(): ?string
{
return $this->where;
}
/**
* Utility method that maps an array of index options to this object's methods.
*
* @param array<string, mixed> $attributes Attributes to set.
* @throws \RuntimeException
* @return $this
*/
public function setAttributes(array $attributes)
{
// Valid Options
$validOptions = ['columns', 'type', 'name', 'length', 'order', 'include', 'where'];
foreach ($attributes as $attr => $value) {
if (!in_array($attr, $validOptions, true)) {
throw new RuntimeException(sprintf('"%s" is not a valid index option.', $attr));
}
$method = 'set' . ucfirst($attr);
$this->$method($value);
}
return $this;
}
/**
* Convert an index into an array that is compatible with the Index constructor.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->getName(),
'columns' => $this->getColumns(),
'type' => $this->getType(),
'length' => $this->getLength(),
'order' => $this->getOrder(),
'include' => $this->getInclude(),
'where' => $this->getWhere(),
];
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+760
View File
@@ -0,0 +1,760 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use Cake\Database\Driver;
use Cake\Database\Exception\DatabaseException;
use Cake\Database\Exception\QueryException;
use Cake\Database\Type\ColumnSchemaAwareInterface;
use Cake\Database\TypeFactory;
use InvalidArgumentException;
use PDOException;
use function Cake\Core\deprecationWarning;
/**
* Base class for schema implementations.
*
* This class contains methods that are common across
* the various SQL dialects.
*
* Provides methods for performing schema reflection. Results
* will be in the form of structured arrays. The structure
* of each result will be documented in this class. Subclasses
* are free to include *additional* data that is not documented.
*
* @method array<mixed> listTablesWithoutViewsSql(array $config) Generate the SQL to list the tables, excluding all views.
*/
abstract class SchemaDialect
{
/**
* The driver instance being used.
*
* @var \Cake\Database\Driver
*/
protected Driver $_driver;
/**
* Constructor
*
* This constructor will connect the driver so that methods like columnSql() and others
* will fail when the driver has not been connected.
*
* @param \Cake\Database\Driver $driver The driver to use.
*/
public function __construct(Driver $driver)
{
$driver->connect();
$this->_driver = $driver;
}
/**
* Generate an ON clause for a foreign key.
*
* @param string $on The on clause
* @return string
*/
protected function _foreignOnClause(string $on): string
{
if ($on === TableSchema::ACTION_SET_NULL) {
return 'SET NULL';
}
if ($on === TableSchema::ACTION_SET_DEFAULT) {
return 'SET DEFAULT';
}
if ($on === TableSchema::ACTION_CASCADE) {
return 'CASCADE';
}
if ($on === TableSchema::ACTION_RESTRICT) {
return 'RESTRICT';
}
if ($on === TableSchema::ACTION_NO_ACTION) {
return 'NO ACTION';
}
throw new InvalidArgumentException('Invalid value for "on": ' . $on);
}
/**
* Convert string on clauses to the abstract ones.
*
* @param string $clause The on clause to convert.
* @return string
*/
protected function _convertOnClause(string $clause): string
{
if ($clause === 'CASCADE' || $clause === 'RESTRICT') {
return strtolower($clause);
}
if ($clause === 'NO ACTION') {
return TableSchema::ACTION_NO_ACTION;
}
return TableSchema::ACTION_SET_NULL;
}
/**
* Convert foreign key constraints references to a valid
* stringified list
*
* @param array<string>|string $references The referenced columns of a foreign key constraint statement
* @return string
*/
protected function _convertConstraintColumns(array|string $references): string
{
if (is_string($references)) {
return $this->_driver->quoteIdentifier($references);
}
return implode(', ', array_map(
$this->_driver->quoteIdentifier(...),
$references,
));
}
/**
* Tries to use a matching database type to generate the SQL
* fragment for a single column in a table.
*
* @param string $columnType The column type.
* @param \Cake\Database\Schema\TableSchemaInterface $schema The table schema instance the column is in.
* @param string $column The name of the column.
* @return string|null An SQL fragment, or `null` in case no corresponding type was found or the type didn't provide
* custom column SQL.
*/
protected function _getTypeSpecificColumnSql(
string $columnType,
TableSchemaInterface $schema,
string $column,
): ?string {
if (!TypeFactory::getMapped($columnType)) {
return null;
}
$type = TypeFactory::build($columnType);
if (!($type instanceof ColumnSchemaAwareInterface)) {
return null;
}
return $type->getColumnSql($schema, $column, $this->_driver);
}
/**
* Tries to use a matching database type to convert a SQL column
* definition to an abstract type definition.
*
* @param string $columnType The column type.
* @param array $definition The column definition.
* @return array<string, mixed>|null Array of column information, or `null`
* in case no corresponding type was found or the type didn't provide custom column information.
*/
protected function _applyTypeSpecificColumnConversion(string $columnType, array $definition): ?array
{
if (!TypeFactory::getMapped($columnType)) {
return null;
}
$type = TypeFactory::build($columnType);
if (!($type instanceof ColumnSchemaAwareInterface)) {
return null;
}
return $type->convertColumnDefinition($definition, $this->_driver);
}
/**
* Generate the SQL to drop a table.
*
* @param \Cake\Database\Schema\TableSchema $schema Schema instance
* @return array SQL statements to drop a table.
*/
public function dropTableSql(TableSchema $schema): array
{
$sql = sprintf(
'DROP TABLE %s',
$this->_driver->quoteIdentifier($schema->name()),
);
return [$sql];
}
/**
* Generate the SQL to list the tables.
*
* @param array<string, mixed> $config The connection configuration to use for
* getting tables from.
* @return array An array of (sql, params) to execute.
* @deprecated 5.2.0 Use `listTables()` instead.
*/
abstract public function listTablesSql(array $config): array;
/**
* Generate the SQL to describe a table.
*
* @param string $tableName The table name to get information on.
* @param array<string, mixed> $config The connection configuration.
* @return array An array of (sql, params) to execute.
* @deprecated 5.2.0 Use `describeColumns()` instead.
*/
abstract public function describeColumnSql(string $tableName, array $config): array;
/**
* Generate the SQL to describe the indexes in a table.
*
* @param string $tableName The table name to get information on.
* @param array<string, mixed> $config The connection configuration.
* @return array An array of (sql, params) to execute.
* @deprecated 5.2.0 Use `describeIndexes()` instead.
*/
abstract public function describeIndexSql(string $tableName, array $config): array;
/**
* Generate the SQL to describe the foreign keys in a table.
*
* @param string $tableName The table name to get information on.
* @param array<string, mixed> $config The connection configuration.
* @return array An array of (sql, params) to execute.
* @deprecated 5.2.0 Use `describeForeignKeys()` instead.
*/
abstract public function describeForeignKeySql(string $tableName, array $config): array;
/**
* Generate the SQL to describe table options
*
* @param string $tableName Table name.
* @param array<string, mixed> $config The connection configuration.
* @return array SQL statements to get options for a table.
* @deprecated 5.2.0 Use `describeOptions()` instead.
*/
public function describeOptionsSql(string $tableName, array $config): array
{
return ['', ''];
}
/**
* Convert field description results into abstract schema fields.
*
* @param \Cake\Database\Schema\TableSchema $schema The table object to append fields to.
* @param array $row The row data from `describeColumnSql`.
* @return void
* @deprecated 5.2.0 Use `describeColumns()` instead.
*/
abstract public function convertColumnDescription(TableSchema $schema, array $row): void;
/**
* Convert an index description results into abstract schema indexes or constraints.
*
* @param \Cake\Database\Schema\TableSchema $schema The table object to append
* an index or constraint to.
* @param array $row The row data from `describeIndexSql`.
* @return void
* @deprecated 5.2.0 Use `describeIndexes()` instead.
*/
abstract public function convertIndexDescription(TableSchema $schema, array $row): void;
/**
* Convert a foreign key description into constraints on the Table object.
*
* @param \Cake\Database\Schema\TableSchema $schema The table object to append
* a constraint to.
* @param array $row The row data from `describeForeignKeySql`.
* @return void
* @deprecated 5.2.0 Use `describeForeignKeys()` instead.
*/
abstract public function convertForeignKeyDescription(TableSchema $schema, array $row): void;
/**
* Convert options data into table options.
*
* @param \Cake\Database\Schema\TableSchema $schema Table instance.
* @param array $row The row of data.
* @return void
* @deprecated 5.2.0 Use `describeOptions()` instead.
*/
public function convertOptionsDescription(TableSchema $schema, array $row): void
{
}
/**
* Generate the SQL to create a table.
*
* @param \Cake\Database\Schema\TableSchema $schema Table instance.
* @param array<string> $columns The columns to go inside the table.
* @param array<string> $constraints The constraints for the table.
* @param array<string> $indexes The indexes for the table.
* @return array<string> SQL statements to create a table.
*/
abstract public function createTableSql(
TableSchema $schema,
array $columns,
array $constraints,
array $indexes,
): array;
/**
* Generate the SQL fragment for a single column in a table.
*
* @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in.
* @param string $name The name of the column.
* @return string SQL fragment.
*/
abstract public function columnSql(TableSchema $schema, string $name): string;
/**
* Generate the SQL queries needed to add foreign key constraints to the table
*
* @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
* @return array SQL fragment.
*/
abstract public function addConstraintSql(TableSchema $schema): array;
/**
* Generate the SQL queries needed to drop foreign key constraints from the table
*
* @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
* @return array SQL fragment.
*/
abstract public function dropConstraintSql(TableSchema $schema): array;
/**
* Generate the SQL fragments for defining table constraints.
*
* @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in.
* @param string $name The name of the column.
* @return string SQL fragment.
*/
abstract public function constraintSql(TableSchema $schema, string $name): string;
/**
* Generate the SQL fragment for a single index in a table.
*
* @param \Cake\Database\Schema\TableSchema $schema The table object the column is in.
* @param string $name The name of the column.
* @return string SQL fragment.
*/
abstract public function indexSql(TableSchema $schema, string $name): string;
/**
* Generate the SQL to truncate a table.
*
* @param \Cake\Database\Schema\TableSchema $schema Table instance.
* @return array SQL statements to truncate a table.
*/
abstract public function truncateTableSql(TableSchema $schema): array;
/**
* Create a SQL snippet for a column based on the array shape
* that `describeColumns()` creates.
*
* @param array $column The column metadata
* @return string Generated SQL fragment for a column
*/
public function columnDefinitionSql(array $column): string
{
deprecationWarning(
'5.2.0',
'SchemaDialect subclasses need to implement `columnDefinitionSql` before 6.0.0',
);
$table = new TableSchema('placeholder');
$table->addColumn($column['name'], $column);
return $this->columnSql($table, $column['name']);
}
/**
* Get the list of tables, excluding any views, available in the current connection.
*
* @return array<string> The list of tables in the connected database/schema.
*/
public function listTablesWithoutViews(): array
{
[$sql, $params] = $this->listTablesWithoutViewsSql($this->_driver->config());
$result = [];
$statement = $this->_driver->execute($sql, $params);
while ($row = $statement->fetch()) {
$result[] = $row[0];
}
return $result;
}
/**
* Get the list of tables and views available in the current connection.
*
* @param string|null $schema The schema to get the tables for. If null the default schema is used.
* @return array<string> The list of tables and views in the connected database/schema.
*/
public function listTables(?string $schema = null): array
{
$config = $this->_driver->config();
if ($schema !== null) {
$config['schema'] = $schema;
// Set database for MySQL
$config['database'] = $schema;
}
[$sql, $params] = $this->listTablesSql($config);
$result = [];
$statement = $this->_driver->execute($sql, $params);
while ($row = $statement->fetch()) {
$result[] = $row[0];
}
return $result;
}
/**
* Get the column metadata for a table.
*
* The name can include a database schema name in the form 'schema.table'.
*
* @param string $name The name of the table to describe.
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
*/
public function describe(string $name): TableSchemaInterface
{
$tableName = $name;
if (str_contains($name, '.')) {
$tableName = explode('.', $name)[1];
}
$table = $this->_driver->newTableSchema($tableName);
foreach ($this->describeColumns($name) as $column) {
$table->addColumn($column['name'], $column);
}
foreach ($this->describeIndexes($name) as $index) {
if (in_array($index['type'], [TableSchema::CONSTRAINT_UNIQUE, TableSchema::CONSTRAINT_PRIMARY])) {
$table->addConstraint($index['name'], $index);
} else {
$table->addIndex($index['name'], $index);
}
}
foreach ($this->describeForeignKeys($name) as $key) {
$table->addConstraint($key['name'], $key);
}
foreach ($this->describeCheckConstraints($name) as $key) {
$table->addConstraint($key['name'], $key);
}
$options = $this->describeOptions($name);
if ($options) {
$table->setOptions($options);
}
if ($table->columns() === []) {
throw new DatabaseException(sprintf('Cannot describe %s. It has 0 columns.', $name));
}
return $table;
}
/**
* Get a list of column metadata as a array
*
* Each item in the array will contain the following:
*
* - name : the name of the column.
* - type : the abstract type of the column.
* - length : the length of the column.
* - default : the default value of the column or null.
* - null : boolean indicating whether the column can be null.
* - comment : the column comment or null.
*
* Additionaly the `autoIncrement` key will be set for columns that are a primary key.
*
* @param string $tableName The name of the table to describe columns on.
* @return array
*/
public function describeColumns(string $tableName): array
{
deprecationWarning(
'5.2.0',
'SchemaDialect subclasses need to implement `describeColumns` before 6.0.0',
);
$config = $this->_driver->config();
if (str_contains($tableName, '.')) {
[$config['schema'], $tableName] = explode('.', $tableName);
}
/** @var \Cake\Database\Schema\TableSchema $table */
$table = $this->_driver->newTableSchema($tableName);
[$sql, $params] = $this->describeColumnSql($tableName, $config);
$statement = $this->_driver->execute($sql, $params);
foreach ($statement->fetchAll('assoc') as $row) {
$this->convertColumnDescription($table, $row);
}
$columns = [];
foreach ($table->columns() as $columnName) {
$column = $table->getColumn($columnName);
$column['name'] = $columnName;
$columns[] = $column;
}
return $columns;
}
/**
* Get a list of constraint metadata as a array
*
* Each item in the array will contain the following:
*
* - name : The name of the constraint
* - type : the type of the constraint. Generally `foreign`.
* - columns : the columns in the constraint on the.
* - references : A list of the table + all columns in the referenced table
* - update : The update action or null
* - delete : The delete action or null
*
* @param string $tableName The name of the table to describe foreign keys on.
* @return array
*/
public function describeForeignKeys(string $tableName): array
{
deprecationWarning(
'5.2.0',
'SchemaDialect subclasses need to implement `describeForeignKeys` before 6.0.0',
);
$config = $this->_driver->config();
if (str_contains($tableName, '.')) {
[$config['schema'], $tableName] = explode('.', $tableName);
}
/** @var \Cake\Database\Schema\TableSchema $table */
$table = $this->_driver->newTableSchema($tableName);
// Add the columns because TableSchema needs them.
foreach ($this->describeColumns($tableName) as $column) {
$table->addColumn($column['name'], $column);
}
[$sql, $params] = $this->describeForeignKeySql($tableName, $config);
$statement = $this->_driver->execute($sql, $params);
foreach ($statement->fetchAll('assoc') as $row) {
$this->convertForeignKeyDescription($table, $row);
}
$keys = [];
foreach ($table->constraints() as $name) {
$key = $table->getConstraint($name);
$key['name'] = $name;
$keys[] = $key;
}
return $keys;
}
/**
* Get a list of index metadata as a array
*
* Each item in the array will contain the following:
*
* - name : the name of the index.
* - type : the type of the index. One of `unique`, `index`, `primary`.
* - columns : the columns in the index.
* - length : the length of the index if applicable.
*
* @param string $tableName The name of the table to describe indexes on.
* @return array
*/
public function describeIndexes(string $tableName): array
{
deprecationWarning(
'5.2.0',
'SchemaDialect subclasses need to implement `describeIndexes` before 6.0.0',
);
$config = $this->_driver->config();
if (str_contains($tableName, '.')) {
[$config['schema'], $tableName] = explode('.', $tableName);
}
/** @var \Cake\Database\Schema\TableSchema $table */
$table = $this->_driver->newTableSchema($tableName);
// Add the columns because TableSchema needs them.
foreach ($this->describeColumns($tableName) as $column) {
$table->addColumn($column['name'], $column);
}
[$sql, $params] = $this->describeIndexSql($tableName, $config);
$statement = $this->_driver->execute($sql, $params);
foreach ($statement->fetchAll('assoc') as $row) {
$this->convertIndexDescription($table, $row);
}
$indexes = [];
foreach ($table->indexes() as $name) {
$index = $table->getIndex($name);
$index['name'] = $name;
$indexes[] = $index;
}
return $indexes;
}
/**
* Get platform specific options
*
* No keys are guaranteed to be present as they are database driver dependent.
*
* @param string $tableName The name of the table to describe options on.
* @return array
*/
public function describeOptions(string $tableName): array
{
deprecationWarning(
'5.2.0',
'SchemaDialect subclasses need to implement `describeOptions` before 6.0.0',
);
$config = $this->_driver->config();
if (str_contains($tableName, '.')) {
[$config['schema'], $tableName] = explode('.', $tableName);
}
/** @var \Cake\Database\Schema\TableSchema $table */
$table = $this->_driver->newTableSchema($tableName);
[$sql, $params] = $this->describeOptionsSql($tableName, $config);
if ($sql) {
$statement = $this->_driver->execute($sql, $params);
foreach ($statement->fetchAll('assoc') as $row) {
$this->convertOptionsDescription($table, $row);
}
}
return $table->getOptions();
}
/**
* Get a list of check constraint metadata as an array.
*
* Each item in the array will contain the following keys:
*
* - name - The name of the constraint.
* - expression - The check constraint expression as a SQL fragment.
*
* @param string $tableName The name of the table to describe options on.
* @return array
*/
public function describeCheckConstraints(string $tableName): array
{
return [];
}
/**
* Check if a table has a column with a given name.
*
* @param string $tableName The name of the table
* @param string $columnName The name of the column
* @return bool
*/
public function hasColumn(string $tableName, string $columnName): bool
{
try {
$columns = $this->describeColumns($tableName);
} catch (PDOException | DatabaseException) {
return false;
}
foreach ($columns as $column) {
if ($column['name'] === $columnName) {
return true;
}
}
return false;
}
/**
* Check if a table exists
*
* @param string $tableName The name of the table
* @param string|null $schema The schema look for table in. If null the default schema is used.
* @return bool
*/
public function hasTable(string $tableName, ?string $schema = null): bool
{
$tables = $this->listTables($schema);
return in_array($tableName, $tables, true);
}
/**
* Check if a table has an index with a given name.
*
* @param string $tableName The name of the table
* @param array<string> $columns The columns in the index. Specific
* ordering matters.
* @param string $name The name of the index to match on. Can be used alone,
* or with $columns to match indexes more precisely.
* @return bool
*/
public function hasIndex(string $tableName, array $columns = [], ?string $name = null): bool
{
try {
$indexes = $this->describeIndexes($tableName);
} catch (QueryException) {
return false;
}
$found = null;
foreach ($indexes as $index) {
if ($columns && $index['columns'] === $columns) {
$found = $index;
break;
}
if ($columns === [] && $name !== null) {
if ($index['name'] === $name) {
$found = $index;
break;
}
if (isset($index['constraint']) && $index['constraint'] === $name) {
$found = $index;
break;
}
}
}
// Both columns and name provided, both must match;
if ($columns && $found && $name !== null && $found['name'] !== $name) {
return false;
}
return $found !== null;
}
/**
* Check if a table has a foreign key with a given name.
*
* @param string $tableName The name of the table
* @param array<string> $columns The columns in the foriegn key. Specific
* ordering matters.
* @param string $name The name of the foreign key to match on. Can be used alone,
* or with $columns to match keys more precisely.
* @return bool
*/
public function hasForeignKey(string $tableName, array $columns = [], ?string $name = null): bool
{
try {
$keys = $this->describeForeignKeys($tableName);
} catch (QueryException) {
return false;
}
$found = null;
foreach ($keys as $key) {
if ($columns && $key['columns'] === $columns) {
$found = $key;
break;
}
if (!$columns && $name !== null && $key['name'] === $name) {
$found = $key;
break;
}
}
// Both columns and name provided, both must match;
if ($found !== null && $name !== null && $found['name'] !== $name) {
return false;
}
return $found !== null;
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use Cake\Database\Connection;
/**
* An interface used by TableSchema objects.
*/
interface SqlGeneratorInterface
{
/**
* Generate the SQL to create the Table.
*
* Uses the connection to access the schema dialect
* to generate platform specific SQL.
*
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
* @return array List of SQL statements to create the table and the
* required indexes.
*/
public function createSql(Connection $connection): array;
/**
* Generate the SQL to drop a table.
*
* Uses the connection to access the schema dialect to generate platform
* specific SQL.
*
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
* @return array SQL to drop a table.
*/
public function dropSql(Connection $connection): array;
/**
* Generate the SQL statements to truncate a table
*
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
* @return array SQL to truncate a table.
*/
public function truncateSql(Connection $connection): array;
/**
* Generate the SQL statements to add the constraints to the table
*
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
* @return array SQL to add the constraints.
*/
public function addConstraintSql(Connection $connection): array;
/**
* Generate the SQL statements to drop the constraints to the table
*
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
* @return array SQL to drop a table.
*/
public function dropConstraintSql(Connection $connection): array;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,971 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
/**
* Schema management/reflection features for SQLServer.
*
* @internal
*/
class SqlserverSchemaDialect extends SchemaDialect
{
/**
* @var string
*/
public const DEFAULT_SCHEMA_NAME = 'dbo';
/**
* Generate the SQL to list the tables and views.
*
* @param array<string, mixed> $config The connection configuration to use for
* getting tables from.
* @return array An array of (sql, params) to execute.
*/
public function listTablesSql(array $config): array
{
$sql = "SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = ?
AND (TABLE_TYPE = 'BASE TABLE' OR TABLE_TYPE = 'VIEW')
ORDER BY TABLE_NAME";
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
return [$sql, [$schema]];
}
/**
* Generate the SQL to list the tables, excluding all views.
*
* @param array<string, mixed> $config The connection configuration to use for
* getting tables from.
* @return array<mixed> An array of (sql, params) to execute.
*/
public function listTablesWithoutViewsSql(array $config): array
{
$sql = "SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = ?
AND (TABLE_TYPE = 'BASE TABLE')
ORDER BY TABLE_NAME";
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
return [$sql, [$schema]];
}
/**
* @inheritDoc
*/
public function describeColumnSql(string $tableName, array $config): array
{
$sql = $this->describeColumnQuery();
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
return [$sql, [$tableName, $schema]];
}
/**
* Helper method for creating SQL to describe columns in a table.
*
* @return string SQL to reflect columns
*/
private function describeColumnQuery(): string
{
return 'SELECT DISTINCT
AC.column_id AS [column_id],
AC.name AS [name],
TY.name AS [type],
AC.max_length AS [char_length],
AC.precision AS [precision],
AC.scale AS [scale],
AC.is_identity AS [autoincrement],
AC.is_nullable AS [null],
OBJECT_DEFINITION(AC.default_object_id) AS [default],
AC.collation_name AS [collation_name],
EP.[value] AS [comment]
FROM sys.[objects] T
INNER JOIN sys.[schemas] S ON S.[schema_id] = T.[schema_id]
INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id]
INNER JOIN sys.[types] TY ON TY.[user_type_id] = AC.[user_type_id]
LEFT JOIN sys.[extended_properties] as EP
ON T.[object_id] = EP.[major_id]
AND AC.[column_id] = EP.[minor_id]
AND EP.[name] = \'MS_Description\'
WHERE T.[name] = ? AND S.[name] = ?
ORDER BY column_id';
}
/**
* Convert a column definition to the abstract types.
*
* The returned type will be a type that
* Cake\Database\TypeFactory can handle.
*
* @param string $col The column type
* @param int|null $length the column length
* @param int|null $precision The column precision
* @param int|null $scale The column scale
* @return array<string, mixed> Array of column information.
* @link https://technet.microsoft.com/en-us/library/ms187752.aspx
*/
protected function _convertColumn(
string $col,
?int $length = null,
?int $precision = null,
?int $scale = null,
): array {
$col = strtolower($col);
$type = $this->_applyTypeSpecificColumnConversion(
$col,
compact('length', 'precision', 'scale'),
);
if ($type !== null) {
return $type;
}
if (in_array($col, ['date', 'time'])) {
return ['type' => $col, 'length' => null];
}
if ($col === 'datetime') {
// datetime cannot parse more than 3 digits of precision and isn't accurate
return ['type' => TableSchemaInterface::TYPE_DATETIME, 'length' => null];
}
if (str_contains($col, 'datetime')) {
$typeName = TableSchemaInterface::TYPE_DATETIME;
if ($scale > 0) {
$typeName = TableSchemaInterface::TYPE_DATETIME_FRACTIONAL;
}
return ['type' => $typeName, 'length' => null, 'precision' => $scale];
}
if ($col === 'char') {
return ['type' => TableSchemaInterface::TYPE_CHAR, 'length' => $length];
}
if ($col === 'tinyint') {
return ['type' => TableSchemaInterface::TYPE_TINYINTEGER, 'length' => $precision ?: 3];
}
if ($col === 'smallint') {
return ['type' => TableSchemaInterface::TYPE_SMALLINTEGER, 'length' => $precision ?: 5];
}
if ($col === 'int' || $col === 'integer') {
return ['type' => TableSchemaInterface::TYPE_INTEGER, 'length' => $precision ?: 10];
}
if ($col === 'bigint') {
return ['type' => TableSchemaInterface::TYPE_BIGINTEGER, 'length' => $precision ?: 20];
}
if ($col === 'bit') {
return ['type' => TableSchemaInterface::TYPE_BOOLEAN, 'length' => null];
}
if (
str_contains($col, 'numeric') ||
str_contains($col, 'money') ||
str_contains($col, 'decimal')
) {
return ['type' => TableSchemaInterface::TYPE_DECIMAL, 'length' => $precision, 'precision' => $scale];
}
if ($col === 'real' || $col === 'float') {
return ['type' => TableSchemaInterface::TYPE_FLOAT, 'length' => null];
}
// SqlServer schema reflection returns double length for unicode
// columns because internally it uses UTF16/UCS2
if (in_array($col, ['nvarchar', 'nchar', 'ntext'], true)) {
$length /= 2;
}
if (str_contains($col, 'varchar') && $length < 0) {
return ['type' => TableSchemaInterface::TYPE_TEXT, 'length' => null];
}
if (str_contains($col, 'varchar')) {
return ['type' => TableSchemaInterface::TYPE_STRING, 'length' => $length ?: 255];
}
if (str_contains($col, 'char')) {
return ['type' => TableSchemaInterface::TYPE_CHAR, 'length' => $length];
}
if (str_contains($col, 'text')) {
return ['type' => TableSchemaInterface::TYPE_TEXT, 'length' => null];
}
if ($col === 'image' || str_contains($col, 'binary')) {
// -1 is the value for MAX which we treat as a 'long' binary
if ($length == -1) {
$length = TableSchema::LENGTH_LONG;
}
return ['type' => TableSchemaInterface::TYPE_BINARY, 'length' => $length];
}
if ($col === 'uniqueidentifier') {
return ['type' => TableSchemaInterface::TYPE_UUID];
}
if ($col === 'geometry') {
return ['type' => TableSchemaInterface::TYPE_GEOMETRY];
}
if ($col === 'geography') {
// SQLserver only has one generic geometry type that
// we map to point.
return ['type' => TableSchemaInterface::TYPE_POINT];
}
return ['type' => TableSchemaInterface::TYPE_STRING, 'length' => null];
}
/**
* @inheritDoc
*/
public function convertColumnDescription(TableSchema $schema, array $row): void
{
$field = $this->_convertColumn(
$row['type'],
$row['char_length'] !== null ? (int)$row['char_length'] : null,
$row['precision'] !== null ? (int)$row['precision'] : null,
$row['scale'] !== null ? (int)$row['scale'] : null,
);
if (!empty($row['autoincrement'])) {
$field['autoIncrement'] = true;
}
$field += [
'null' => $row['null'] === '1',
'default' => $this->_defaultValue($field['type'], $row['default']),
'collate' => $row['collation_name'],
];
$schema->addColumn($row['name'], $field);
}
/**
* Split a tablename into a tuple of schema, table
* If the table does not have a schema name included, the connection
* schema will be used.
*
* @param string $tableName The table name to split
* @return array A tuple of [schema, tablename]
*/
private function splitTablename(string $tableName): array
{
$config = $this->_driver->config();
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
if (str_contains($tableName, '.')) {
return explode('.', $tableName);
}
return [$schema, $tableName];
}
/**
* @inheritDoc
*/
public function describeColumns(string $tableName): array
{
[$schema, $name] = $this->splitTablename($tableName);
$sql = $this->describeColumnQuery();
$statement = $this->_driver->execute($sql, [$name, $schema]);
$columns = [];
foreach ($statement->fetchAll('assoc') as $row) {
$field = $this->_convertColumn(
$row['type'],
$row['char_length'] !== null ? (int)$row['char_length'] : null,
$row['precision'] !== null ? (int)$row['precision'] : null,
$row['scale'] !== null ? (int)$row['scale'] : null,
);
if (!empty($row['autoincrement'])) {
$field['autoIncrement'] = true;
}
$field += [
'name' => $row['name'],
'null' => $row['null'] === '1',
'default' => $this->_defaultValue($field['type'], $row['default']),
'comment' => $row['comment'] ?? null,
'collate' => $row['collation_name'],
];
$columns[] = $field;
}
return $columns;
}
/**
* Manipulate the default value.
*
* Removes () wrapping default values, extracts strings from
* N'' wrappers and collation text and converts NULL strings.
*
* @param string $type The schema type
* @param string|null $default The default value.
* @return string|int|null
*/
protected function _defaultValue(string $type, ?string $default): string|int|null
{
if ($default === null) {
return null;
}
// remove () surrounding value (NULL) but leave () at the end of functions
// integers might have two ((0)) wrapping value
if (preg_match('/^\(+(.*?(\(\))?)\)+$/', $default, $matches)) {
$default = $matches[1];
}
if ($default === 'NULL') {
return null;
}
if ($type === TableSchemaInterface::TYPE_BOOLEAN) {
return (int)$default;
}
// Remove quotes
if (preg_match("/^\(?N?'(.*)'\)?/", $default, $matches)) {
return str_replace("''", "'", $matches[1]);
}
return $default;
}
/**
* @inheritDoc
*/
public function describeIndexSql(string $tableName, array $config): array
{
$sql = $this->describeIndexQuery();
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
return [$sql, [$tableName, $schema]];
}
/**
* Get the query to describe indexes
*
* @return string
*/
private function describeIndexQuery(): string
{
return "SELECT
I.[name] AS [index_name],
IC.[index_column_id] AS [index_order],
AC.[name] AS [column_name],
I.[is_unique], I.[is_primary_key],
I.[is_unique_constraint],
IC.[is_included_column]
FROM sys.[tables] AS T
INNER JOIN sys.[schemas] S ON S.[schema_id] = T.[schema_id]
INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
INNER JOIN sys.[index_columns] IC ON I.[object_id] = IC.[object_id] AND I.[index_id] = IC.[index_id]
INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id] AND IC.[column_id] = AC.[column_id]
WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP' AND T.[name] = ? AND S.[name] = ?
ORDER BY I.[index_id], IC.[index_column_id]";
}
/**
* @inheritDoc
*/
public function convertIndexDescription(TableSchema $schema, array $row): void
{
$type = TableSchema::INDEX_INDEX;
$name = $row['index_name'];
if ($row['is_primary_key']) {
$name = TableSchema::CONSTRAINT_PRIMARY;
$type = TableSchema::CONSTRAINT_PRIMARY;
}
if (($row['is_unique'] || $row['is_unique_constraint']) && $type === TableSchema::INDEX_INDEX) {
$type = TableSchema::CONSTRAINT_UNIQUE;
}
if ($type === TableSchema::INDEX_INDEX) {
$existing = $schema->getIndex($name);
} else {
$existing = $schema->getConstraint($name);
}
$columns = [$row['column_name']];
if ($existing) {
$columns = array_merge($existing['columns'], $columns);
}
if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) {
$schema->addConstraint($name, [
'type' => $type,
'columns' => $columns,
]);
return;
}
$schema->addIndex($name, [
'type' => $type,
'columns' => $columns,
]);
}
/**
* @inheritDoc
*/
public function describeIndexes(string $tableName): array
{
[$schema, $name] = $this->splitTablename($tableName);
$sql = $this->describeIndexQuery();
$indexes = [];
$statement = $this->_driver->execute($sql, [$name, $schema]);
foreach ($statement->fetchAll('assoc') as $row) {
$type = TableSchema::INDEX_INDEX;
$name = $row['index_name'];
$constraint = null;
if ($row['is_primary_key']) {
$constraint = $name;
$name = TableSchema::CONSTRAINT_PRIMARY;
$type = TableSchema::CONSTRAINT_PRIMARY;
}
if (($row['is_unique'] || $row['is_unique_constraint']) && $type === TableSchema::INDEX_INDEX) {
$type = TableSchema::CONSTRAINT_UNIQUE;
}
if (!isset($indexes[$name])) {
$indexes[$name] = [
'name' => $name,
'type' => $type,
'columns' => [],
'length' => [],
];
}
if ($row['is_included_column']) {
$indexes[$name]['include'][] = $row['column_name'];
} else {
$indexes[$name]['columns'][] = $row['column_name'];
}
if ($constraint) {
$indexes[$name]['constraint'] = $constraint;
}
}
return array_values($indexes);
}
/**
* Get the query to describe foreign keys
*
* @return string
*/
private function describeForeignKeyQuery(): string
{
// phpcs:disable Generic.Files.LineLength
return 'SELECT FK.[name] AS [foreign_key_name],
FK.[delete_referential_action_desc] AS [delete_type],
FK.[update_referential_action_desc] AS [update_type],
C.name AS [column],
RT.name AS [reference_table],
RC.name AS [reference_column]
FROM sys.foreign_keys FK
INNER JOIN sys.foreign_key_columns FKC ON FKC.constraint_object_id = FK.object_id
INNER JOIN sys.tables T ON T.object_id = FKC.parent_object_id
INNER JOIN sys.tables RT ON RT.object_id = FKC.referenced_object_id
INNER JOIN sys.schemas S ON S.schema_id = T.schema_id AND S.schema_id = RT.schema_id
INNER JOIN sys.columns C ON C.column_id = FKC.parent_column_id AND C.object_id = FKC.parent_object_id
INNER JOIN sys.columns RC ON RC.column_id = FKC.referenced_column_id AND RC.object_id = FKC.referenced_object_id
WHERE FK.is_ms_shipped = 0 AND T.name = ? AND S.name = ?
ORDER BY FKC.constraint_column_id';
// phpcs:enable Generic.Files.LineLength
}
/**
* @inheritDoc
*/
public function describeForeignKeys(string $tableName): array
{
[$schema, $name] = $this->splitTablename($tableName);
$sql = $this->describeForeignKeyQuery();
$keys = [];
$statement = $this->_driver->execute($sql, [$name, $schema]);
foreach ($statement->fetchAll('assoc') as $row) {
$name = $row['foreign_key_name'];
if (!isset($keys[$name])) {
$keys[$name] = [
'name' => $name,
'type' => TableSchema::CONSTRAINT_FOREIGN,
'columns' => [],
'references' => [$row['reference_table'], []],
'update' => $this->_convertOnClause($row['update_type']),
'delete' => $this->_convertOnClause($row['delete_type']),
];
}
$keys[$name]['columns'][] = $row['column'];
$keys[$name]['references'][1][] = $row['reference_column'];
}
foreach ($keys as $id => $key) {
// references.1 is the referenced columns. Backwards compat
// requires a single column to be a string, but multiple to be an array.
if (count($key['references'][1]) === 1) {
$keys[$id]['references'][1] = $key['references'][1][0];
}
}
return array_values($keys);
}
/**
* @inheritDoc
*/
public function describeForeignKeySql(string $tableName, array $config): array
{
$sql = $this->describeForeignKeyQuery();
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
return [$sql, [$tableName, $schema]];
}
/**
* @inheritDoc
*/
public function convertForeignKeyDescription(TableSchema $schema, array $row): void
{
$data = [
'type' => TableSchema::CONSTRAINT_FOREIGN,
'columns' => [$row['column']],
'references' => [$row['reference_table'], $row['reference_column']],
'update' => $this->_convertOnClause($row['update_type']),
'delete' => $this->_convertOnClause($row['delete_type']),
];
$name = $row['foreign_key_name'];
$schema->addConstraint($name, $data);
}
/**
* @inheritDoc
*/
public function describeOptions(string $tableName): array
{
return [];
}
/**
* @inheritDoc
*/
protected function _foreignOnClause(string $on): string
{
$parent = parent::_foreignOnClause($on);
return $parent === 'RESTRICT' ? parent::_foreignOnClause(TableSchema::ACTION_NO_ACTION) : $parent;
}
/**
* @inheritDoc
*/
protected function _convertOnClause(string $clause): string
{
return match ($clause) {
'NO_ACTION' => TableSchema::ACTION_NO_ACTION,
'CASCADE' => TableSchema::ACTION_CASCADE,
'SET_NULL' => TableSchema::ACTION_SET_NULL,
'SET_DEFAULT' => TableSchema::ACTION_SET_DEFAULT,
default => TableSchema::ACTION_SET_NULL,
};
}
/**
* @inheritDoc
*/
public function columnSql(TableSchema $schema, string $name): string
{
$data = $schema->getColumn($name);
assert($data !== null);
$data['name'] = $name;
$sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
if ($sql !== null) {
return $sql;
}
$autoIncrementTypes = [
TableSchemaInterface::TYPE_TINYINTEGER,
TableSchemaInterface::TYPE_SMALLINTEGER,
TableSchemaInterface::TYPE_INTEGER,
TableSchemaInterface::TYPE_BIGINTEGER,
];
$primaryKey = $schema->getPrimaryKey();
if (
in_array($data['type'], $autoIncrementTypes, true) &&
$primaryKey === [$name] &&
$name === 'id'
) {
$data['autoIncrement'] = true;
}
return $this->columnDefinitionSql($data);
}
/**
* @inheritDoc
*/
public function columnDefinitionSql(array $column): string
{
$name = $column['name'];
$column += [
'length' => null,
'precision' => null,
];
$out = $this->_driver->quoteIdentifier($name);
$typeMap = [
TableSchemaInterface::TYPE_TINYINTEGER => ' TINYINT',
TableSchemaInterface::TYPE_SMALLINTEGER => ' SMALLINT',
TableSchemaInterface::TYPE_INTEGER => ' INTEGER',
TableSchemaInterface::TYPE_BIGINTEGER => ' BIGINT',
TableSchemaInterface::TYPE_BINARY_UUID => ' UNIQUEIDENTIFIER',
TableSchemaInterface::TYPE_BOOLEAN => ' BIT',
TableSchemaInterface::TYPE_CHAR => ' NCHAR',
TableSchemaInterface::TYPE_STRING => ' NVARCHAR',
TableSchemaInterface::TYPE_FLOAT => ' FLOAT',
TableSchemaInterface::TYPE_DECIMAL => ' DECIMAL',
TableSchemaInterface::TYPE_DATE => ' DATE',
TableSchemaInterface::TYPE_TIME => ' TIME',
TableSchemaInterface::TYPE_DATETIME => ' DATETIME2',
TableSchemaInterface::TYPE_DATETIME_FRACTIONAL => ' DATETIME2',
TableSchemaInterface::TYPE_TIMESTAMP => ' DATETIME2',
TableSchemaInterface::TYPE_TIMESTAMP_FRACTIONAL => ' DATETIME2',
TableSchemaInterface::TYPE_TIMESTAMP_TIMEZONE => ' DATETIME2',
TableSchemaInterface::TYPE_UUID => ' UNIQUEIDENTIFIER',
TableSchemaInterface::TYPE_NATIVE_UUID => ' UNIQUEIDENTIFIER',
TableSchemaInterface::TYPE_JSON => ' NVARCHAR(MAX)',
TableSchemaInterface::TYPE_GEOMETRY => ' GEOMETRY',
TableSchemaInterface::TYPE_POINT => ' GEOGRAPHY',
TableSchemaInterface::TYPE_LINESTRING => ' GEOGRAPHY',
TableSchemaInterface::TYPE_POLYGON => ' GEOGRAPHY',
];
$foundType = false;
if (isset($typeMap[$column['type']])) {
$out .= $typeMap[$column['type']];
$foundType = true;
}
$hasLength = [
TableSchemaInterface::TYPE_CHAR,
TableSchemaInterface::TYPE_STRING,
TableSchemaInterface::TYPE_BINARY,
];
$autoIncrementTypes = [
TableSchemaInterface::TYPE_TINYINTEGER,
TableSchemaInterface::TYPE_SMALLINTEGER,
TableSchemaInterface::TYPE_INTEGER,
TableSchemaInterface::TYPE_BIGINTEGER,
];
$autoIncrement = (bool)($column['autoIncrement'] ?? false);
if (in_array($column['type'], $autoIncrementTypes, true) && $autoIncrement) {
$out .= ' IDENTITY(1, 1)';
$foundType = true;
unset($column['default']);
}
if ($column['type'] === TableSchemaInterface::TYPE_STRING && !isset($column['length'])) {
$column['length'] = TableSchema::LENGTH_TINY;
} elseif (
$column['type'] === TableSchemaInterface::TYPE_TEXT &&
$column['length'] !== TableSchema::LENGTH_TINY
) {
$out .= ' NVARCHAR(MAX)';
$foundType = true;
}
if ($column['type'] === TableSchemaInterface::TYPE_BINARY) {
if (
!isset($column['length'])
|| in_array($column['length'], [TableSchema::LENGTH_MEDIUM, TableSchema::LENGTH_LONG], true)
) {
$column['length'] = 'MAX';
}
if ($column['length'] === 1) {
$out .= ' BINARY';
} else {
$out .= ' VARBINARY';
}
$foundType = true;
}
if ($column['type'] === TableSchemaInterface::TYPE_TEXT && $column['length'] === TableSchema::LENGTH_TINY) {
$out .= ' NVARCHAR';
$hasLength[] = $column['type'];
$foundType = true;
}
if (!$foundType) {
$out .= ' ' . strtoupper($column['type']);
$hasLength[] = $column['type'];
}
if (in_array($column['type'], $hasLength, true) && isset($column['length'])) {
$out .= '(' . $column['length'] . ')';
}
$hasCollate = [
TableSchemaInterface::TYPE_TEXT,
TableSchemaInterface::TYPE_STRING,
TableSchemaInterface::TYPE_CHAR,
];
if (in_array($column['type'], $hasCollate, true) && isset($column['collate']) && $column['collate'] !== '') {
$out .= ' COLLATE ' . $column['collate'];
}
$precisionTypes = [
TableSchemaInterface::TYPE_FLOAT,
TableSchemaInterface::TYPE_DATETIME,
TableSchemaInterface::TYPE_DATETIME_FRACTIONAL,
TableSchemaInterface::TYPE_TIMESTAMP,
TableSchemaInterface::TYPE_TIMESTAMP_FRACTIONAL,
];
if (in_array($column['type'], $precisionTypes, true) && isset($column['precision'])) {
$out .= '(' . (int)$column['precision'] . ')';
}
if (
$column['type'] === TableSchemaInterface::TYPE_DECIMAL &&
(isset($column['length']) || isset($column['precision']))
) {
$out .= '(' . (int)$column['length'] . ',' . (int)$column['precision'] . ')';
}
if (isset($column['null']) && $column['null'] === false) {
$out .= ' NOT NULL';
}
$dateTimeTypes = [
TableSchemaInterface::TYPE_DATETIME,
TableSchemaInterface::TYPE_DATETIME_FRACTIONAL,
TableSchemaInterface::TYPE_TIMESTAMP,
TableSchemaInterface::TYPE_TIMESTAMP_FRACTIONAL,
];
$dateTimeDefaults = [
'current_timestamp',
'getdate()',
'getutcdate()',
'sysdatetime()',
'sysutcdatetime()',
'sysdatetimeoffset()',
];
if (
isset($column['default']) &&
in_array($column['type'], $dateTimeTypes, true) &&
is_string($column['default']) &&
in_array(strtolower($column['default']), $dateTimeDefaults, true)
) {
$out .= ' DEFAULT ' . strtoupper($column['default']);
} elseif (isset($column['default'])) {
$default = is_bool($column['default'])
? (int)$column['default']
: $this->_driver->schemaValue($column['default']);
$out .= ' DEFAULT ' . $default;
} elseif (isset($column['null']) && $column['null'] !== false) {
$out .= ' DEFAULT NULL';
}
return $out;
}
/**
* @inheritDoc
*/
public function addConstraintSql(TableSchema $schema): array
{
$sqlPattern = 'ALTER TABLE %s ADD %s;';
$sql = [];
foreach ($schema->constraints() as $name) {
$constraint = $schema->getConstraint($name);
assert($constraint !== null);
if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
$tableName = $this->_driver->quoteIdentifier($schema->name());
$sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name));
}
}
return $sql;
}
/**
* @inheritDoc
*/
public function dropConstraintSql(TableSchema $schema): array
{
$sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;';
$sql = [];
foreach ($schema->constraints() as $name) {
$constraint = $schema->getConstraint($name);
assert($constraint !== null);
if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
$tableName = $this->_driver->quoteIdentifier($schema->name());
$constraintName = $this->_driver->quoteIdentifier($name);
$sql[] = sprintf($sqlPattern, $tableName, $constraintName);
}
}
return $sql;
}
/**
* @inheritDoc
*/
public function indexSql(TableSchema $schema, string $name): string
{
$index = $schema->index($name);
$columns = array_map(
$this->_driver->quoteIdentifier(...),
(array)$index->getColumns(),
);
$include = '';
$included = $index->getInclude();
if ($included !== null) {
$included = array_map(
$this->_driver->quoteIdentifier(...),
$included,
);
$include = sprintf(' INCLUDE (%s)', implode(', ', $included));
}
return sprintf(
'CREATE INDEX %s ON %s (%s)%s',
$this->_driver->quoteIdentifier($name),
$this->_driver->quoteIdentifier($schema->name()),
implode(', ', $columns),
$include,
);
}
/**
* @inheritDoc
*/
public function constraintSql(TableSchema $schema, string $name): string
{
$data = $schema->getConstraint($name);
assert($data !== null);
$out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name);
if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
$out = 'PRIMARY KEY';
}
if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
$out .= ' UNIQUE';
}
return $this->_keySql($out, $data);
}
/**
* Helper method for generating key SQL snippets.
*
* @param string $prefix The key prefix
* @param array $data Key data.
* @return string
*/
protected function _keySql(string $prefix, array $data): string
{
$columns = array_map(
$this->_driver->quoteIdentifier(...),
$data['columns'],
);
if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
return $prefix . sprintf(
' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s',
implode(', ', $columns),
$this->_driver->quoteIdentifier($data['references'][0]),
$this->_convertConstraintColumns($data['references'][1]),
$this->_foreignOnClause($data['update']),
$this->_foreignOnClause($data['delete']),
);
}
return $prefix . ' (' . implode(', ', $columns) . ')';
}
/**
* @inheritDoc
*/
public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array
{
$content = array_merge($columns, $constraints);
$content = implode(",\n", array_filter($content));
$tableName = $this->_driver->quoteIdentifier($schema->name());
$out = [];
$out[] = sprintf("CREATE TABLE %s (\n%s\n)", $tableName, $content);
foreach ($indexes as $index) {
$out[] = $index;
}
foreach ($schema->columns() as $name) {
$column = $schema->getColumn($name);
$comment = $column['comment'] ?? null;
if ($comment !== null) {
$out[] = $this->columnCommentSql($schema, $name, $comment);
}
}
return $out;
}
/**
* Generate the SQL to create a column comment.
*
* @param \Cake\Database\Schema\TableSchema $schema The table schema.
* @param string $name The column name.
* @param string $comment The column comment.
* @return string
*/
protected function columnCommentSql(TableSchema $schema, string $name, string $comment): string
{
$tableName = $this->_driver->quoteIdentifier($schema->name());
$columnName = $this->_driver->quoteIdentifier($name);
$comment = $this->_driver->schemaValue($comment);
return sprintf(
"EXEC sp_addextendedproperty N'MS_Description', %s, N'SCHEMA', N'dbo', N'TABLE', %s, N'COLUMN', %s;",
$comment,
$tableName,
$columnName,
);
}
/**
* @inheritDoc
*/
public function truncateTableSql(TableSchema $schema): array
{
$name = $this->_driver->quoteIdentifier($schema->name());
$queries = [
sprintf('DELETE FROM %s', $name),
];
// Restart identity sequences
$pk = $schema->getPrimaryKey();
if (count($pk) === 1) {
$column = $schema->getColumn($pk[0]);
assert($column !== null);
if (in_array($column['type'], ['integer', 'biginteger'])) {
$queries[] = sprintf(
"IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = '%s' AND " .
"last_value IS NOT NULL) DBCC CHECKIDENT('%s', RESEED, 0)",
$schema->name(),
$schema->name(),
);
}
}
return $queries;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,381 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use Cake\Datasource\SchemaInterface;
/**
* An interface used by database TableSchema objects.
*
* @method \Cake\Database\Schema\Column column(string $name)
* @method \Cake\Database\Schema\Index index(string $name)
* @method \Cake\Database\Schema\Constraint constraint(string $name)
*/
interface TableSchemaInterface extends SchemaInterface
{
/**
* Binary column type
*
* @var string
*/
public const TYPE_BINARY = 'binary';
/**
* Binary UUID column type
*
* @var string
*/
public const TYPE_BINARY_UUID = 'binaryuuid';
/**
* Date column type
*
* @var string
*/
public const TYPE_DATE = 'date';
/**
* Datetime column type
*
* @var string
*/
public const TYPE_DATETIME = 'datetime';
/**
* Datetime with fractional seconds column type
*
* @var string
*/
public const TYPE_DATETIME_FRACTIONAL = 'datetimefractional';
/**
* Time column type
*
* @var string
*/
public const TYPE_TIME = 'time';
/**
* Year column type
*
* Currently only implemented in MySQL
*
* @var string
*/
public const TYPE_YEAR = 'year';
/**
* Timestamp column type
*
* @var string
*/
public const TYPE_TIMESTAMP = 'timestamp';
/**
* Timestamp with fractional seconds column type
*
* @var string
*/
public const TYPE_TIMESTAMP_FRACTIONAL = 'timestampfractional';
/**
* Timestamp with time zone column type
*
* @var string
*/
public const TYPE_TIMESTAMP_TIMEZONE = 'timestamptimezone';
/**
* Datetime interval. Only implemented in postgres.
*
* @var string
*/
public const TYPE_INTERVAL = 'interval';
/**
* JSON column type
*
* @var string
*/
public const TYPE_JSON = 'json';
/**
* String column type
*
* @var string
*/
public const TYPE_STRING = 'string';
/**
* Char column type
*
* @var string
*/
public const TYPE_CHAR = 'char';
/**
* Case-insensitive text column type.
*
* Only implemented in postgres
*
* @var string
*/
public const TYPE_CITEXT = 'citext';
/**
* Text column type
*
* @var string
*/
public const TYPE_TEXT = 'text';
/**
* Tiny Integer column type
*
* @var string
*/
public const TYPE_TINYINTEGER = 'tinyinteger';
/**
* Small Integer column type
*
* @var string
*/
public const TYPE_SMALLINTEGER = 'smallinteger';
/**
* Integer column type
*
* @var string
*/
public const TYPE_INTEGER = 'integer';
/**
* Big Integer column type
*
* @var string
*/
public const TYPE_BIGINTEGER = 'biginteger';
/**
* Float column type
*
* @var string
*/
public const TYPE_FLOAT = 'float';
/**
* Decimal column type
*
* @var string
*/
public const TYPE_DECIMAL = 'decimal';
/**
* Boolean column type
*
* @var string
*/
public const TYPE_BOOLEAN = 'boolean';
/**
* UUID column type
*
* @var string
*/
public const TYPE_UUID = 'uuid';
/**
* Native UUID column type
*
* @var string
*/
public const TYPE_NATIVE_UUID = 'nativeuuid';
/**
* Geometry column type
*
* @var string
*/
public const TYPE_GEOMETRY = 'geometry';
/**
* Point column type
*
* @var string
*/
public const TYPE_POINT = 'point';
/**
* Linestring column type
*
* @var string
*/
public const TYPE_LINESTRING = 'linestring';
/**
* Polgon column type
*
* @var string
*/
public const TYPE_POLYGON = 'polygon';
/**
* INET type. Only implemented in postgres.
*
* @var string
*/
public const TYPE_INET = 'inet';
/**
* CIDR type. Only implemented in postgres.
*
* @var string
*/
public const TYPE_CIDR = 'cidr';
/**
* Macaddr type. Only implemented in postgres.
*
* @var string
*/
public const TYPE_MACADDR = 'macaddr';
/**
* Geospatial column types
*
* @var array
*/
public const GEOSPATIAL_TYPES = [
self::TYPE_GEOMETRY,
self::TYPE_POINT,
self::TYPE_LINESTRING,
self::TYPE_POLYGON,
];
/**
* Check whether a table has an autoIncrement column defined.
*
* @return bool
*/
public function hasAutoincrement(): bool;
/**
* Sets whether the table is temporary in the database.
*
* @param bool $temporary Whether the table is to be temporary.
* @return $this
*/
public function setTemporary(bool $temporary);
/**
* Gets whether the table is temporary in the database.
*
* @return bool The current temporary setting.
*/
public function isTemporary(): bool;
/**
* Get the column(s) used for the primary key.
*
* @return array<string> Column name(s) for the primary key. An
* empty list will be returned when the table has no primary key.
*/
public function getPrimaryKey(): array;
/**
* Add an index.
*
* Used to add indexes, and full text indexes in platforms that support
* them.
*
* ### Attributes
*
* - `type` The type of index being added.
* - `columns` The columns in the index.
*
* @param string $name The name of the index.
* @param array<string, mixed>|string $attrs The attributes for the index.
* If string it will be used as `type`.
* @return $this
* @throws \Cake\Database\Exception\DatabaseException
*/
public function addIndex(string $name, array|string $attrs);
/**
* Read information about an index based on name.
*
* @param string $name The name of the index.
* @return array<string, mixed>|null Array of index data, or null
*/
public function getIndex(string $name): ?array;
/**
* Get the names of all the indexes in the table.
*
* @return array<string>
*/
public function indexes(): array;
/**
* Add a constraint.
*
* Used to add constraints to a table. For example primary keys, unique
* keys, check constraints and foreign keys.
*
* ### Attributes
*
* - `type` The type of constraint being added.
* - `columns` The columns in the index.
* - `references` The table, column a foreign key references.
* - `update` The behavior on update. Options are 'restrict', 'setNull', 'cascade', 'noAction'.
* - `delete` The behavior on delete. Options are 'restrict', 'setNull', 'cascade', 'noAction'.
* - `expression` The SQL expression for check constraints.
*
* The default for 'update' & 'delete' is 'cascade'.
*
* @param string $name The name of the constraint.
* @param array<string, mixed>|string $attrs The attributes for the constraint.
* If string it will be used as `type`.
* @return $this
* @throws \Cake\Database\Exception\DatabaseException
*/
public function addConstraint(string $name, array|string $attrs);
/**
* Read information about a constraint based on name.
*
* @param string $name The name of the constraint.
* @return array<string, mixed>|null Array of constraint data, or null
*/
public function getConstraint(string $name): ?array;
/**
* Remove a constraint.
*
* @param string $name Name of the constraint to remove
* @return $this
*/
public function dropConstraint(string $name);
/**
* Get the names of all the constraints in the table.
*
* @return array<string>
*/
public function constraints(): array;
}
+155
View File
@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @copyright Copyright (c) Cake Software Foundation, Inc.
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
* @link https://cakephp.org CakePHP(tm) Project
* @since 5.3.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
/**
* UniqueKey class
*
* Models a unique key constraint, and provides methods to set driver specific attributes.
*/
class UniqueKey extends Constraint
{
/**
* Constructor
*
* @param string $name The name of the constraint.
* @param array<string> $columns The columns to constraint.
* @param array<string, int>|null $length The length of the columns, if applicable.
*/
public function __construct(
protected string $name,
protected array $columns,
protected ?array $length = null,
) {
$this->type = self::UNIQUE;
}
/**
* Sets the constraint columns.
*
* @param array<string>|string $columns Columns
* @return $this
*/
public function setColumns(string|array $columns)
{
$this->columns = (array)$columns;
return $this;
}
/**
* Gets the constraint columns.
*
* @return ?array<string>
*/
public function getColumns(): ?array
{
return $this->columns;
}
/**
* Sets the constraint type.
*
* @param string $type Type
* @return $this
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
/**
* Gets the constraint type.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Sets the constraint name.
*
* @param string $name Name
* @return $this
*/
public function setName(string $name)
{
$this->name = $name;
return $this;
}
/**
* Gets the constraint name.
*
* @return ?string
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Sets the constraint length.
*
* In MySQL unique constraints can have limit clauses to control the number of
* characters indexed in text and char columns.
*
* @param array<string, int> $length array of length values
* @return $this
*/
public function setLength(array $length)
{
$this->length = $length;
return $this;
}
/**
* Gets the constraint length.
*
* Can be an array of column names and lengths under MySQL.
*
* @return array<string, int>|null
*/
public function getLength(): ?array
{
return $this->length;
}
/**
* Converts a constraint to an array that is compatible
* with the constructor.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'type' => $this->type,
'columns' => $this->columns,
'length' => $this->length,
];
}
}