From 73ac13e6d7fe5d201f9273c97c04cf9a198d6f74 Mon Sep 17 00:00:00 2001 From: Sergey Makinen Date: Wed, 3 May 2017 17:23:30 +0300 Subject: [PATCH] Implement retrieving dbms constraints --- framework/db/CheckConstraint.php | 22 + framework/db/Command.php | 103 +++ framework/db/Constraint.php | 28 + framework/db/ConstraintFinderTrait.php | 216 +++++ framework/db/DefaultConstraint.php | 22 + framework/db/ForeignKeyConstraint.php | 38 + framework/db/IndexConstraint.php | 26 + framework/db/QueryBuilder.php | 109 ++- framework/db/Schema.php | 310 ++++--- framework/db/SqlToken.php | 296 ++++++ framework/db/SqlTokenizer.php | 378 ++++++++ framework/db/cubrid/QueryBuilder.php | 20 + framework/db/cubrid/Schema.php | 230 +++-- framework/db/mssql/QueryBuilder.php | 20 + framework/db/mssql/Schema.php | 322 ++++++- framework/db/mysql/QueryBuilder.php | 19 + framework/db/mysql/Schema.php | 258 +++++- framework/db/oci/QueryBuilder.php | 1 + framework/db/oci/Schema.php | 344 +++++-- framework/db/pgsql/Schema.php | 309 +++++-- framework/db/sqlite/QueryBuilder.php | 55 ++ framework/db/sqlite/Schema.php | 195 +++- framework/db/sqlite/SqlTokenizer.php | 288 ++++++ tests/data/cubrid.sql | 43 + tests/data/mssql.sql | 43 + tests/data/mysql.sql | 47 + tests/data/oci.sql | 43 + tests/data/postgres.sql | 44 + tests/data/sqlite.sql | 43 + tests/framework/db/AnyCaseValue.php | 23 + tests/framework/db/AnyValue.php | 19 + tests/framework/db/CommandTest.php | 30 + tests/framework/db/CompareValue.php | 9 + tests/framework/db/QueryBuilderTest.php | 226 ++++- tests/framework/db/SchemaTest.php | 229 +++++ tests/framework/db/cubrid/QueryBuilderTest.php | 10 + tests/framework/db/cubrid/SchemaTest.php | 62 +- tests/framework/db/mssql/SchemaTest.php | 18 + tests/framework/db/mysql/QueryBuilderTest.php | 36 + tests/framework/db/mysql/SchemaTest.php | 14 + tests/framework/db/oci/QueryBuilderTest.php | 39 + tests/framework/db/oci/SchemaTest.php | 72 +- tests/framework/db/pgsql/QueryBuilderTest.php | 12 + tests/framework/db/pgsql/SchemaTest.php | 14 +- tests/framework/db/sqlite/QueryBuilderTest.php | 31 +- tests/framework/db/sqlite/SchemaTest.php | 24 +- tests/framework/db/sqlite/SqlTokenizerTest.php | 1138 ++++++++++++++++++++++++ 47 files changed, 5444 insertions(+), 434 deletions(-) create mode 100644 framework/db/CheckConstraint.php create mode 100644 framework/db/Constraint.php create mode 100644 framework/db/ConstraintFinderTrait.php create mode 100644 framework/db/DefaultConstraint.php create mode 100644 framework/db/ForeignKeyConstraint.php create mode 100644 framework/db/IndexConstraint.php create mode 100644 framework/db/SqlToken.php create mode 100644 framework/db/SqlTokenizer.php create mode 100644 framework/db/sqlite/SqlTokenizer.php create mode 100644 tests/framework/db/AnyCaseValue.php create mode 100644 tests/framework/db/AnyValue.php create mode 100644 tests/framework/db/CompareValue.php create mode 100644 tests/framework/db/sqlite/SqlTokenizerTest.php diff --git a/framework/db/CheckConstraint.php b/framework/db/CheckConstraint.php new file mode 100644 index 0000000..1d03942 --- /dev/null +++ b/framework/db/CheckConstraint.php @@ -0,0 +1,22 @@ + + * @since 2.0.13 + */ +class CheckConstraint extends Constraint +{ + /** + * @var string + */ + public $expression; +} diff --git a/framework/db/Command.php b/framework/db/Command.php index dc360e3..c7cd664 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -726,6 +726,109 @@ class Command extends Component } /** + * Creates a SQL command for adding an unique constraint to an existing table. + * @param string $name the name of the unique constraint. + * The name will be properly quoted by the method. + * @param string $table the table that the unique constraint will be added to. + * The name will be properly quoted by the method. + * @param string|array $columns the name of the column to that the constraint will be added on. + * If there are multiple columns, separate them with commas. + * The name will be properly quoted by the method. + * @return $this the command object itself. + * @since 2.0.13 + */ + public function addUnique($name, $table, $columns) + { + $sql = $this->db->getQueryBuilder()->addUnique($name, $table, $columns); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for dropping an unique constraint. + * @param string $name the name of the unique constraint to be dropped. + * The name will be properly quoted by the method. + * @param string $table the table whose unique constraint is to be dropped. + * The name will be properly quoted by the method. + * @return $this the command object itself. + * @since 2.0.13 + */ + public function dropUnique($name, $table) + { + $sql = $this->db->getQueryBuilder()->dropUnique($name, $table); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for adding a check constraint to an existing table. + * @param string $name the name of the check constraint. + * The name will be properly quoted by the method. + * @param string $table the table that the check constraint will be added to. + * The name will be properly quoted by the method. + * @param string $check the SQL of the `CHECK` constraint. + * @return $this the command object itself. + * @since 2.0.13 + */ + public function addCheck($name, $table, $check) + { + $sql = $this->db->getQueryBuilder()->addCheck($name, $table, $check); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for dropping a check constraint. + * @param string $name the name of the check constraint to be dropped. + * The name will be properly quoted by the method. + * @param string $table the table whose check constraint is to be dropped. + * The name will be properly quoted by the method. + * @return $this the command object itself. + * @since 2.0.13 + */ + public function dropCheck($name, $table) + { + $sql = $this->db->getQueryBuilder()->dropCheck($name, $table); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for adding a default value constraint to an existing table. + * @param string $name the name of the default value constraint. + * The name will be properly quoted by the method. + * @param string $table the table that the default value constraint will be added to. + * The name will be properly quoted by the method. + * @param string $column the name of the column to that the constraint will be added on. + * The name will be properly quoted by the method. + * @param mixed $default default value. + * @return $this the command object itself. + * @since 2.0.13 + */ + public function addDefaultValue($name, $table, $column, $default) + { + $sql = $this->db->getQueryBuilder()->addDefaultValue($name, $table, $column, $default); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for dropping a default value constraint. + * @param string $name the name of the default value constraint to be dropped. + * The name will be properly quoted by the method. + * @param string $table the table whose default value constraint is to be dropped. + * The name will be properly quoted by the method. + * @return $this the command object itself. + * @since 2.0.13 + */ + public function dropDefaultValue($name, $table) + { + $sql = $this->db->getQueryBuilder()->dropDefaultValue($name, $table); + + return $this->setSql($sql); + } + + /** * Creates a SQL command for resetting the sequence value of a table's primary key. * The sequence will be reset such that the primary key of the next new row inserted * will have the specified value or 1. diff --git a/framework/db/Constraint.php b/framework/db/Constraint.php new file mode 100644 index 0000000..542b730 --- /dev/null +++ b/framework/db/Constraint.php @@ -0,0 +1,28 @@ + + * @since 2.0.13 + */ +class Constraint extends Object +{ + /** + * @var string[]|null list of column names the constraint belongs to. + */ + public $columnNames; + /** + * @var string|null the constraint name. + */ + public $name; +} diff --git a/framework/db/ConstraintFinderTrait.php b/framework/db/ConstraintFinderTrait.php new file mode 100644 index 0000000..66a02c4 --- /dev/null +++ b/framework/db/ConstraintFinderTrait.php @@ -0,0 +1,216 @@ + + * @since 2.0.13 + */ +trait ConstraintFinderTrait +{ + /** + * Loads a primary key for the given table. + * @param string $tableName table name. + * @return Constraint|null primary key for the given table, `null` if the table has no primary key. + */ + protected abstract function loadTablePrimaryKey($tableName); + + /** + * Loads all foreign keys for the given table. + * @param string $tableName table name. + * @return ForeignKeyConstraint[] foreign keys for the given table. + */ + protected abstract function loadTableForeignKeys($tableName); + + /** + * Loads all indexes for the given table. + * @param string $tableName table name. + * @return IndexConstraint[] indexes for the given table. + */ + protected abstract function loadTableIndexes($tableName); + + /** + * Loads all unique constraints for the given table. + * @param string $tableName table name. + * @return Constraint[] unique constraints for the given table. + */ + protected abstract function loadTableUniques($tableName); + + /** + * Loads all check constraints for the given table. + * @param string $tableName table name. + * @return CheckConstraint[] check constraints for the given table. + */ + protected abstract function loadTableChecks($tableName); + + /** + * Loads all default value constraints for the given table. + * @param string $tableName table name. + * @return DefaultConstraint[] default value constraints for the given table. + */ + protected abstract function loadTableDefaultValues($tableName); + + /** + * Obtains the primary key for the named table. + * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. + * @param bool $refresh whether to reload the information even if it is found in the cache. + * @return Constraint|null table primary key, `null` if the table has no primary key. + */ + public function getTablePrimaryKey($name, $refresh = false) + { + return $this->getTableMetadata($name, 'primaryKey', $refresh); + } + + /** + * Returns primary keys for all tables in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. + * @param bool $refresh whether to fetch the latest available table schemas. If this is `false`, + * cached data may be returned if available. + * @return Constraint[] primary keys for all tables in the database. + * Each array element is an instance of [[Constraint]] or its child class. + */ + public function getSchemaPrimaryKeys($schema = '', $refresh = false) + { + return $this->getSchemaMetadata($schema, 'primaryKey', $refresh); + } + + /** + * Obtains the foreign keys information for the named table. + * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. + * @param bool $refresh whether to reload the information even if it is found in the cache. + * @return ForeignKeyConstraint[] table foreign keys. + */ + public function getTableForeignKeys($name, $refresh = false) + { + return $this->getTableMetadata($name, 'foreignKeys', $refresh); + } + + /** + * Returns foreign keys for all tables in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. + * @param bool $refresh whether to fetch the latest available table schemas. If this is false, + * cached data may be returned if available. + * @return ForeignKeyConstraint[][] foreign keys for all tables in the database. + * Each array element is an array of [[ForeignKeyConstraint]] or its child classes. + */ + public function getSchemaForeignKeys($schema = '', $refresh = false) + { + return $this->getSchemaMetadata($schema, 'foreignKeys', $refresh); + } + + /** + * Obtains the indexes information for the named table. + * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. + * @param bool $refresh whether to reload the information even if it is found in the cache. + * @return IndexConstraint[] table indexes. + */ + public function getTableIndexes($name, $refresh = false) + { + return $this->getTableMetadata($name, 'indexes', $refresh); + } + + /** + * Returns indexes for all tables in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. + * @param bool $refresh whether to fetch the latest available table schemas. If this is false, + * cached data may be returned if available. + * @return IndexConstraint[][] indexes for all tables in the database. + * Each array element is an array of [[IndexConstraint]] or its child classes. + */ + public function getSchemaIndexes($schema = '', $refresh = false) + { + return $this->getSchemaMetadata($schema, 'indexes', $refresh); + } + + /** + * Obtains the unique constraints information for the named table. + * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. + * @param bool $refresh whether to reload the information even if it is found in the cache. + * @return Constraint[] table unique constraints. + */ + public function getTableUniques($name, $refresh = false) + { + return $this->getTableMetadata($name, 'uniques', $refresh); + } + + /** + * Returns unique constraints for all tables in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. + * @param bool $refresh whether to fetch the latest available table schemas. If this is false, + * cached data may be returned if available. + * @return Constraint[][] unique constraints for all tables in the database. + * Each array element is an array of [[Constraint]] or its child classes. + */ + public function getSchemaUniques($schema = '', $refresh = false) + { + return $this->getSchemaMetadata($schema, 'uniques', $refresh); + } + + /** + * Obtains the check constraints information for the named table. + * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. + * @param bool $refresh whether to reload the information even if it is found in the cache. + * @return CheckConstraint[] table check constraints. + */ + public function getTableChecks($name, $refresh = false) + { + return $this->getTableMetadata($name, 'checks', $refresh); + } + + /** + * Returns check constraints for all tables in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. + * @param bool $refresh whether to fetch the latest available table schemas. If this is false, + * cached data may be returned if available. + * @return CheckConstraint[][] check constraints for all tables in the database. + * Each array element is an array of [[CheckConstraint]] or its child classes. + */ + public function getSchemaChecks($schema = '', $refresh = false) + { + return $this->getSchemaMetadata($schema, 'checks', $refresh); + } + + /** + * Obtains the default value constraints information for the named table. + * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. + * @param bool $refresh whether to reload the information even if it is found in the cache. + * @return DefaultConstraint[] table default value constraints. + */ + public function getTableDefaultValues($name, $refresh = false) + { + return $this->getTableMetadata($name, 'defaultValues', $refresh); + } + + /** + * Returns default value constraints for all tables in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. + * @param bool $refresh whether to fetch the latest available table schemas. If this is false, + * cached data may be returned if available. + * @return DefaultConstraint[] default value constraints for all tables in the database. + * Each array element is an array of [[DefaultConstraint]] or its child classes. + */ + public function getSchemaDefaultValues($schema = '', $refresh = false) + { + return $this->getSchemaMetadata($schema, 'defaultValues', $refresh); + } +} diff --git a/framework/db/DefaultConstraint.php b/framework/db/DefaultConstraint.php new file mode 100644 index 0000000..1df6fad --- /dev/null +++ b/framework/db/DefaultConstraint.php @@ -0,0 +1,22 @@ + + * @since 2.0.13 + */ +class DefaultConstraint extends Constraint +{ + /** + * @var mixed default value as returned by the DBMS. + */ + public $value; +} diff --git a/framework/db/ForeignKeyConstraint.php b/framework/db/ForeignKeyConstraint.php new file mode 100644 index 0000000..6ceadee --- /dev/null +++ b/framework/db/ForeignKeyConstraint.php @@ -0,0 +1,38 @@ + + * @since 2.0.13 + */ +class ForeignKeyConstraint extends Constraint +{ + /** + * @var string|null referenced table schema name. + */ + public $foreignSchemaName; + /** + * @var string referenced table name. + */ + public $foreignTableName; + /** + * @var string[] list of referenced table column names. + */ + public $foreignColumnNames; + /** + * @var string|null referential action if rows in a referenced table are to be updated. + */ + public $onUpdate; + /** + * @var string|null referential action if rows in a referenced table are to be deleted. + */ + public $onDelete; +} diff --git a/framework/db/IndexConstraint.php b/framework/db/IndexConstraint.php new file mode 100644 index 0000000..f0c08ba --- /dev/null +++ b/framework/db/IndexConstraint.php @@ -0,0 +1,26 @@ + + * @since 2.0.13 + */ +class IndexConstraint extends Constraint +{ + /** + * @var bool whether the index is unique. + */ + public $isUnique; + /** + * @var bool whether the index was created for a primary key. + */ + public $isPrimary; +} diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index ae4ec26..bc667b4 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -82,6 +82,7 @@ class QueryBuilder extends \yii\base\Object */ protected $likeEscapeCharacter; + /** * Constructor. * @param Connection $connection the database connection. @@ -450,8 +451,8 @@ class QueryBuilder extends \yii\base\Object } return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT ' - . $this->db->quoteColumnName($name) . ' PRIMARY KEY (' - . implode(', ', $columns). ' )'; + . $this->db->quoteColumnName($name) . ' PRIMARY KEY (' + . implode(', ', $columns). ')'; } /** @@ -609,6 +610,110 @@ class QueryBuilder extends \yii\base\Object } /** + * Creates a SQL command for adding an unique constraint to an existing table. + * @param string $name the name of the unique constraint. + * The name will be properly quoted by the method. + * @param string $table the table that the unique constraint will be added to. + * The name will be properly quoted by the method. + * @param string|array $columns the name of the column to that the constraint will be added on. + * If there are multiple columns, separate them with commas. + * The name will be properly quoted by the method. + * @return string the SQL statement for adding an unique constraint to an existing table. + * @since 2.0.13 + */ + public function addUnique($name, $table, $columns) + { + if (is_string($columns)) { + $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY); + } + foreach ($columns as $i => $col) { + $columns[$i] = $this->db->quoteColumnName($col); + } + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT ' + . $this->db->quoteColumnName($name) . ' UNIQUE (' + . implode(', ', $columns). ')'; + } + + /** + * Creates a SQL command for dropping an unique constraint. + * @param string $name the name of the unique constraint to be dropped. + * The name will be properly quoted by the method. + * @param string $table the table whose unique constraint is to be dropped. + * The name will be properly quoted by the method. + * @return string the SQL statement for dropping an unique constraint. + * @since 2.0.13 + */ + public function dropUnique($name, $table) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) + . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name); + } + + /** + * Creates a SQL command for adding a check constraint to an existing table. + * @param string $name the name of the check constraint. + * The name will be properly quoted by the method. + * @param string $table the table that the check constraint will be added to. + * The name will be properly quoted by the method. + * @param string $check the SQL of the `CHECK` constraint. + * @return string the SQL statement for adding a check constraint to an existing table. + * @since 2.0.13 + */ + public function addCheck($name, $table, $check) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT ' + . $this->db->quoteColumnName($name) . ' CHECK (' . $this->db->quoteSql($check) . ')'; + } + + /** + * Creates a SQL command for dropping a check constraint. + * @param string $name the name of the check constraint to be dropped. + * The name will be properly quoted by the method. + * @param string $table the table whose check constraint is to be dropped. + * The name will be properly quoted by the method. + * @return string the SQL statement for dropping a check constraint. + * @since 2.0.13 + */ + public function dropCheck($name, $table) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) + . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name); + } + + /** + * Creates a SQL command for adding a default value constraint to an existing table. + * @param string $name the name of the default value constraint. + * The name will be properly quoted by the method. + * @param string $table the table that the default value constraint will be added to. + * The name will be properly quoted by the method. + * @param string $column the name of the column to that the constraint will be added on. + * The name will be properly quoted by the method. + * @param mixed $default default value. + * @return string the SQL statement for adding a default value constraint to an existing table. + * @throws NotSupportedException if this is not supported by the underlying DBMS. + * @since 2.0.13 + */ + public function addDefaultValue($name, $table, $column, $default) + { + throw new NotSupportedException($this->db->getDriverName() . ' does not support adding default value constraints.'); + } + + /** + * Creates a SQL command for dropping a default value constraint. + * @param string $name the name of the default value constraint to be dropped. + * The name will be properly quoted by the method. + * @param string $table the table whose default value constraint is to be dropped. + * The name will be properly quoted by the method. + * @return string the SQL statement for dropping a default value constraint. + * @throws NotSupportedException if this is not supported by the underlying DBMS. + * @since 2.0.13 + */ + public function dropDefaultValue($name, $table) + { + throw new NotSupportedException($this->db->getDriverName() . ' does not support dropping default value constraints.'); + } + + /** * Creates a SQL statement for resetting the sequence value of a table's primary key. * The sequence will be reset such that the primary key of the next new row inserted * will have the specified value or 1. diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 1557235..b1b5c96 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -8,6 +8,7 @@ namespace yii\db; use Yii; +use yii\base\InvalidConfigException; use yii\base\Object; use yii\base\NotSupportedException; use yii\base\InvalidCallException; @@ -33,6 +34,7 @@ use yii\caching\TagDependency; * syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is write-only. * * @author Qiang Xue + * @author Sergey Makinen * @since 2.0 */ abstract class Schema extends Object @@ -89,9 +91,9 @@ abstract class Schema extends Object */ private $_tableNames = []; /** - * @var array list of loaded table metadata (table name => TableSchema) + * @var array list of loaded table metadata (table name => metadata type => metadata). */ - private $_tables = []; + private $_tableMetadata = []; /** * @var QueryBuilder the query builder for this database */ @@ -99,109 +101,81 @@ abstract class Schema extends Object /** - * @return \yii\db\ColumnSchema - * @throws \yii\base\InvalidConfigException + * Resolves the table name and schema name (if any). + * @param string $name the table name + * @return TableSchema [[TableSchema]] with resolved table, schema, etc. names. + * @throws NotSupportedException if this method is not supported by the DBMS. + * @since 2.0.13 */ - protected function createColumnSchema() + protected function resolveTableName($name) { - return Yii::createObject($this->columnSchemaClass); + throw new NotSupportedException(get_class($this) . ' does not support resolving table names.'); } /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return null|TableSchema DBMS-dependent table metadata, null if the table does not exist. + * Returns all schema names in the database, including the default one but not system schemas. + * This method should be overridden by child classes in order to support this feature + * because the default implementation simply throws an exception. + * @return array all schema names in the database, except system schemas. + * @throws NotSupportedException if this method is not supported by the DBMS. + * @since 2.0.4 */ - abstract protected function loadTableSchema($name); + protected function findSchemaNames() + { + throw new NotSupportedException(get_class($this) . ' does not support fetching all schema names.'); + } /** - * Obtains the metadata for the named table. - * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. - * @param bool $refresh whether to reload the table schema even if it is found in the cache. - * @return null|TableSchema table metadata. Null if the named table does not exist. + * Returns all table names in the database. + * This method should be overridden by child classes in order to support this feature + * because the default implementation simply throws an exception. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @return array all table names in the database. The names have NO schema name prefix. + * @throws NotSupportedException if this method is not supported by the DBMS. */ - public function getTableSchema($name, $refresh = false) + protected function findTableNames($schema = '') { - if (array_key_exists($name, $this->_tables) && !$refresh) { - return $this->_tables[$name]; - } - - $db = $this->db; - $realName = $this->getRawTableName($name); - - if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { - /* @var $cache Cache */ - $cache = is_string($db->schemaCache) ? Yii::$app->get($db->schemaCache, false) : $db->schemaCache; - if ($cache instanceof Cache) { - $key = $this->getCacheKey($name); - if ($refresh || ($table = $cache->get($key)) === false) { - $this->_tables[$name] = $table = $this->loadTableSchema($realName); - if ($table !== null) { - $cache->set($key, $table, $db->schemaCacheDuration, new TagDependency([ - 'tags' => $this->getCacheTag(), - ])); - } - } else { - $this->_tables[$name] = $table; - } - - return $this->_tables[$name]; - } - } - - return $this->_tables[$name] = $this->loadTableSchema($realName); + throw new NotSupportedException(get_class($this) . ' does not support fetching all table names.'); } /** - * Returns the cache key for the specified table name. - * @param string $name the table name - * @return mixed the cache key + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist. */ - protected function getCacheKey($name) + abstract protected function loadTableSchema($name); + + /** + * @return ColumnSchema + * @throws InvalidConfigException + */ + protected function createColumnSchema() { - return [ - __CLASS__, - $this->db->dsn, - $this->db->username, - $name, - ]; + return Yii::createObject($this->columnSchemaClass); } /** - * Returns the cache tag name. - * This allows [[refresh()]] to invalidate all cached table schemas. - * @return string the cache tag name + * Obtains the metadata for the named table. + * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. + * @param bool $refresh whether to reload the table schema even if it is found in the cache. + * @return TableSchema|null table metadata. `null` if the named table does not exist. */ - protected function getCacheTag() + public function getTableSchema($name, $refresh = false) { - return md5(serialize([ - __CLASS__, - $this->db->dsn, - $this->db->username, - ])); + return $this->getTableMetadata($name, 'schema', $refresh); } /** * Returns the metadata for all tables in the database. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. - * @param bool $refresh whether to fetch the latest available table schemas. If this is false, + * @param bool $refresh whether to fetch the latest available table schemas. If this is `false`, * cached data may be returned if available. * @return TableSchema[] the metadata for all tables in the database. * Each array element is an instance of [[TableSchema]] or its child class. */ public function getTableSchemas($schema = '', $refresh = false) { - $tables = []; - foreach ($this->getTableNames($schema, $refresh) as $name) { - if ($schema !== '') { - $name = $schema . '.' . $name; - } - if (($table = $this->getTableSchema($name, $refresh)) !== null) { - $tables[] = $table; - } - } - - return $tables; + return $this->getSchemaMetadata($schema, 'schema', $refresh); } /** @@ -283,7 +257,7 @@ abstract class Schema extends Object TagDependency::invalidate($cache, $this->getCacheTag()); } $this->_tableNames = []; - $this->_tables = []; + $this->_tableMetadata = []; } /** @@ -295,7 +269,7 @@ abstract class Schema extends Object */ public function refreshTableSchema($name) { - unset($this->_tables[$name]); + unset($this->_tableMetadata[$name]); $this->_tableNames = []; /* @var $cache Cache */ $cache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache; @@ -330,32 +304,6 @@ abstract class Schema extends Object } /** - * Returns all schema names in the database, including the default one but not system schemas. - * This method should be overridden by child classes in order to support this feature - * because the default implementation simply throws an exception. - * @return array all schema names in the database, except system schemas - * @throws NotSupportedException if this method is called - * @since 2.0.4 - */ - protected function findSchemaNames() - { - throw new NotSupportedException(get_class($this) . ' does not support fetching all schema names.'); - } - - /** - * Returns all table names in the database. - * This method should be overridden by child classes in order to support this feature - * because the default implementation simply throws an exception. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. - * @throws NotSupportedException if this method is called - */ - protected function findTableNames($schema = '') - { - throw new NotSupportedException(get_class($this) . ' does not support fetching all table names.'); - } - - /** * Returns all unique indexes for the given table. * Each array element is of the following structure: * @@ -646,4 +594,162 @@ abstract class Schema extends Object $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i'; return preg_match($pattern, $sql) > 0; } + + /** + * Returns the cache key for the specified table name. + * @param string $name the table name + * @return mixed the cache key + */ + protected function getCacheKey($name) + { + return [ + __CLASS__, + $this->db->dsn, + $this->db->username, + $name, + ]; + } + + /** + * Returns the cache tag name. + * This allows [[refresh()]] to invalidate all cached table schemas. + * @return string the cache tag name + */ + protected function getCacheTag() + { + return md5(serialize([ + __CLASS__, + $this->db->dsn, + $this->db->username, + ])); + } + + /** + * Returns the metadata of the given type for the given table. + * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. + * @param string $type metadata type. + * @param bool $refresh whether to reload the table metadata even if it is found in the cache. + * @return mixed metadata. + * @since 2.0.13 + */ + protected function getTableMetadata($name, $type, $refresh) + { + $cache = null; + if ($this->db->enableSchemaCache && !in_array($name, $this->db->schemaCacheExclude, true)) { + $schemaCache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache; + if ($schemaCache instanceof Cache) { + $cache = $schemaCache; + } + } + if (!isset($this->_tableMetadata[$name]) || $refresh) { + $this->loadTableMetadataFromCache($cache, $name); + } + if (!array_key_exists($type, $this->_tableMetadata[$name])) { + $this->_tableMetadata[$name][$type] = $this->{'loadTable' . ucfirst($type)}($this->getRawTableName($name)); + } + $this->saveTableMetadataToCache($cache, $name); + return $this->_tableMetadata[$name][$type]; + } + + /** + * Returns the metadata of the given type for all tables in the given schema. + * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema name. + * @param string $type metadata type. + * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`, + * cached data may be returned if available. + * @return array array of metadata. + * @since 2.0.13 + */ + protected function getSchemaMetadata($schema, $type, $refresh) + { + $metadata = []; + $methodName = 'getTable' . ucfirst($type); + foreach ($this->getTableNames($schema, $refresh) as $name) { + if ($schema !== '') { + $name = $schema . '.' . $name; + } + $tableMetadata = $this->$methodName($name, $refresh); + if ($tableMetadata !== null) { + $metadata[] = $tableMetadata; + } + } + return $metadata; + } + + /** + * Sets the metadata of the given type for the given table. + * @param string $name table name. + * @param string $type metadata type. + * @param mixed $data metadata. + * @since 2.0.13 + */ + protected function setTableMetadata($name, $type, $data) + { + $this->_tableMetadata[$name][$type] = $data; + } + + /** + * Changes row's array key case to lower if PDO's one is set to uppercase. + * @param array $row row's array or an array of row's arrays. + * @param bool $multiple whether multiple rows or a single row passed. + * @return array normalized row or rows. + * @since 2.0.13 + */ + protected function normalizePdoRowKeyCase(array $row, $multiple) + { + if ($this->db->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_UPPER) { + return $row; + } + + if (!$multiple) { + return array_change_key_case($row, CASE_LOWER); + } + + return array_map(function (array $row) { + return array_change_key_case($row, CASE_LOWER); + }, $row); + } + + /** + * Tries to load and populate table metadata from cache. + * @param Cache|null $cache + * @param string $name + */ + private function loadTableMetadataFromCache($cache, $name) + { + if ($cache === null) { + $this->_tableMetadata[$name] = []; + return; + } + + $metadata = $cache->get($this->getCacheKey($name)); + if (!is_array($metadata) || !isset($metadata['cacheVersion']) || $metadata['cacheVersion'] !== 1) { + $this->_tableMetadata[$name] = []; + return; + } + + unset($metadata['cacheVersion']); + $this->_tableMetadata[$name] = $metadata; + } + + /** + * Saves table metadata to cache. + * @param Cache|null $cache + * @param string $name + */ + private function saveTableMetadataToCache($cache, $name) + { + if ($cache === null) { + return; + } + + $metadata = $this->_tableMetadata[$name]; + $metadata['cacheVersion'] = 1; + $cache->set( + $this->getCacheKey($name), + $metadata, + $this->db->schemaCacheDuration, + new TagDependency(['tags' => $this->getCacheTag()]) + ); + } } diff --git a/framework/db/SqlToken.php b/framework/db/SqlToken.php new file mode 100644 index 0000000..39faf91 --- /dev/null +++ b/framework/db/SqlToken.php @@ -0,0 +1,296 @@ + + * @since 2.0.13 + */ +class SqlToken extends Object implements \ArrayAccess +{ + const TYPE_CODE = 0; + const TYPE_STATEMENT = 1; + const TYPE_TOKEN = 2; + const TYPE_PARENTHESIS = 3; + const TYPE_KEYWORD = 4; + const TYPE_OPERATOR = 5; + const TYPE_IDENTIFIER = 6; + const TYPE_STRING_LITERAL = 7; + + /** + * @var int token type. + * It has to be one of the following constants: + * - [[TYPE_CODE]] + * - [[TYPE_STATEMENT]] + * - [[TYPE_TOKEN]] + * - [[TYPE_PARENTHESIS]] + * - [[TYPE_KEYWORD]] + * - [[TYPE_OPERATOR]] + * - [[TYPE_IDENTIFIER]] + * - [[TYPE_STRING_LITERAL]] + */ + public $type = self::TYPE_TOKEN; + /** + * @var string|null token content. + */ + public $content; + /** + * @var int original SQL token start position. + */ + public $startOffset; + /** + * @var int original SQL token end position. + */ + public $endOffset; + /** + * @var SqlToken parent token. + */ + public $parent; + + /** + * @var SqlToken[] + */ + private $_children = []; + + + /** + * Returns the SQL code representing the token. + * @return string SQL code. + */ + public function __toString() + { + return $this->getSql(); + } + + /** + * Returns whether there is a child token at the specified offset. + * This method is required by the SPL [[\ArrayAccess]] interface. + * It is implicitly called when you use something like `isset($token[$offset])`. + * @param int $offset child token offset. + * @return bool whether the token exists. + */ + public function offsetExists($offset) + { + return isset($this->_children[$this->calculateOffset($offset)]); + } + + /** + * Returns a child token at the specified offset. + * This method is required by the SPL [[\ArrayAccess]] interface. + * It is implicitly called when you use something like `$child = $token[$offset];`. + * @param int $offset child token offset. + * @return SqlToken|null the child token at the specified offset, `null` if there's no token. + */ + public function offsetGet($offset) + { + $offset = $this->calculateOffset($offset); + return isset($this->_children[$offset]) ? $this->_children[$offset] : null; + } + + /** + * Adds a child token to the token. + * This method is required by the SPL [[\ArrayAccess]] interface. + * It is implicitly called when you use something like `$token[$offset] = $child;`. + * @param int|null $offset child token offset. + * @param SqlToken $token token to be added. + */ + public function offsetSet($offset, $token) + { + $token->parent = $this; + if ($offset === null) { + $this->_children[] = $token; + } else { + $this->_children[$this->calculateOffset($offset)] = $token; + } + $this->updateCollectionOffsets(); + } + + /** + * Removes a child token at the specified offset. + * This method is required by the SPL [[\ArrayAccess]] interface. + * It is implicitly called when you use something like `unset($token[$offset])`. + * @param int $offset child token offset. + */ + public function offsetUnset($offset) + { + $offset = $this->calculateOffset($offset); + if (isset($this->_children[$offset])) { + array_splice($this->_children, $offset, 1); + } + $this->updateCollectionOffsets(); + } + + /** + * Returns child tokens. + * @return SqlToken[] child tokens. + */ + public function getChildren() + { + return $this->_children; + } + + /** + * Sets a list of child tokens. + * @param SqlToken[] $children child tokens. + */ + public function setChildren($children) + { + $this->_children = []; + foreach ($children as $child) { + $child->parent = $this; + $this->_children[] = $child; + } + $this->updateCollectionOffsets(); + } + + /** + * Returns whether the token represents a collection of tokens. + * @return bool whether the token represents a collection of tokens. + */ + public function getIsCollection() + { + return in_array($this->type, [ + self::TYPE_CODE, + self::TYPE_STATEMENT, + self::TYPE_PARENTHESIS, + ], true); + } + + /** + * Returns whether the token represents a collection of tokens and has non-zero number of children. + * @return bool whether the token has children. + */ + public function getHasChildren() + { + return $this->getIsCollection() && !empty($this->_children); + } + + /** + * Returns the SQL code representing the token. + * @return string SQL code. + */ + public function getSql() + { + $code = $this; + while ($code->parent !== null) { + $code = $code->parent; + } + return mb_substr($code->content, $this->startOffset, $this->endOffset - $this->startOffset, 'UTF-8'); + } + + /** + * @param string $pattern + * @param int $offset + * @param int|null $firstMatchIndex + * @param int|null $lastMatchIndex + * @return bool + */ + public function matches($pattern, $offset = 0, &$firstMatchIndex = null, &$lastMatchIndex = null) + { + $patternToken = (new \yii\db\sqlite\SqlTokenizer($pattern))->tokenize(); + if (!$patternToken->getHasChildren()) { + return false; + } + + $patternToken = $patternToken[0]; + return $this->tokensMatch($patternToken, $this, $offset, $firstMatchIndex, $lastMatchIndex); + } + + /** + * Tests the given token to match the specified pattern token. + * @param SqlToken $patternToken + * @param SqlToken $token + * @param int $offset + * @param int|null $firstMatchIndex + * @param int|null $lastMatchIndex + * @return bool + */ + private function tokensMatch(SqlToken $patternToken, SqlToken $token, $offset = 0, &$firstMatchIndex = null, &$lastMatchIndex = null) + { + if ( + $patternToken->getIsCollection() !== $token->getIsCollection() + || (!$patternToken->getIsCollection() && $patternToken->content !== $token->content) + ) { + return false; + } + + if ($patternToken->children === $token->children) { + $firstMatchIndex = $lastMatchIndex = $offset; + return true; + } + + $firstMatchIndex = $lastMatchIndex = null; + $wildcard = false; + for ($index = 0, $count = count($patternToken->children); $index < $count; $index++) { + if ($patternToken[$index]->content === 'any') { + $wildcard = true; + continue; + } + + for ($limit = $wildcard ? count($token->children) : $offset + 1; $offset < $limit; $offset++) { + if (!$wildcard && !isset($token[$offset])) { + break; + } + + if (!$this->tokensMatch($patternToken[$index],$token[$offset])) { + continue; + } + + if ($firstMatchIndex === null) { + $firstMatchIndex = $offset; + $lastMatchIndex = $offset; + } else { + $lastMatchIndex = $offset; + } + $wildcard = false; + $offset++; + continue 2; + } + return false; + } + return true; + } + + /** + * Returns an absolute offset in the children array. + * @param int $offset + * @return int + */ + private function calculateOffset($offset) + { + if ($offset >= 0) { + return $offset; + } + + return count($this->_children) + $offset; + } + + /** + * Updates token SQL code start and end offsets based on its children. + */ + private function updateCollectionOffsets() + { + if (!empty($this->_children)) { + $this->startOffset = reset($this->_children)->startOffset; + $this->endOffset = end($this->_children)->endOffset; + } + if ($this->parent !== null) { + $this->parent->updateCollectionOffsets(); + } + } +} diff --git a/framework/db/SqlTokenizer.php b/framework/db/SqlTokenizer.php new file mode 100644 index 0000000..4a8c03c --- /dev/null +++ b/framework/db/SqlTokenizer.php @@ -0,0 +1,378 @@ + + * @since 2.0.13 + */ +abstract class SqlTokenizer extends Object +{ + /** + * @var string SQL code. + */ + public $sql; + + /** + * @var int SQL code string length. + */ + protected $length; + /** + * @var int SQL code string current offset. + */ + protected $offset; + + /** + * @var \SplStack + */ + private $_tokenStack; + /** + * @var SqlToken + */ + private $_currentToken; + /** + * @var string[] + */ + private $_substrings; + /** + * @var string + */ + private $_buffer = ''; + /** + * @var SqlToken + */ + private $_token; + + + /** + * Constructor. + * @param string $sql SQL code to be tokenized. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($sql, $config = []) + { + $this->sql = $sql; + parent::__construct($config); + } + + /** + * Tokenizes and returns a code type token. + * @return SqlToken code type token. + */ + public function tokenize() + { + $this->length = mb_strlen($this->sql, 'UTF-8'); + $this->offset = 0; + $this->_substrings = []; + $this->_buffer = ''; + $this->_token = new SqlToken([ + 'type' => SqlToken::TYPE_CODE, + 'content' => $this->sql, + ]); + $this->_tokenStack = new \SplStack(); + $this->_tokenStack->push($this->_token); + $this->_token[] = new SqlToken(['type' => SqlToken::TYPE_STATEMENT]); + $this->_tokenStack->push($this->_token[0]); + $this->_currentToken = $this->_tokenStack->top(); + while (!$this->isEof()) { + if ($this->isWhitespace($length) || $this->isComment($length)) { + $this->addTokenFromBuffer(); + $this->advance($length); + continue; + } + + if ($this->tokenizeOperator($length) || $this->tokenizeDelimitedString($length)) { + $this->advance($length); + continue; + } + + $this->_buffer .= $this->substring(1); + $this->advance(1); + } + $this->addTokenFromBuffer(); + if ($this->_token->getHasChildren() && !$this->_token[-1]->getHasChildren()) { + unset($this->_token[-1]); + } + return $this->_token; + } + + /** + * Returns whether there's a whitespace at the current offset. + * If this methos returns `true`, it has to set the `$length` parameter to the length of the matched string. + * @param int $length length of the matched string. + * @return bool whether there's a whitespace at the current offset. + */ + protected abstract function isWhitespace(&$length); + + /** + * Returns whether there's a commentary at the current offset. + * If this methos returns `true`, it has to set the `$length` parameter to the length of the matched string. + * @param int $length length of the matched string. + * @return bool whether there's a commentary at the current offset. + */ + protected abstract function isComment(&$length); + + /** + * Returns whether there's an operator at the current offset. + * If this methos returns `true`, it has to set the `$length` parameter to the length of the matched string. + * It may also set `$content` to a string that will be used as a token content. + * @param int $length length of the matched string. + * @param string $content optional content instead of the matched string. + * @return bool whether there's an operator at the current offset. + */ + protected abstract function isOperator(&$length, &$content); + + /** + * Returns whether there's an identifier at the current offset. + * If this methos returns `true`, it has to set the `$length` parameter to the length of the matched string. + * It may also set `$content` to a string that will be used as a token content. + * @param int $length length of the matched string. + * @param string $content optional content instead of the matched string. + * @return bool whether there's an identifier at the current offset. + */ + protected abstract function isIdentifier(&$length, &$content); + + /** + * Returns whether there's a string literal at the current offset. + * If this methos returns `true`, it has to set the `$length` parameter to the length of the matched string. + * It may also set `$content` to a string that will be used as a token content. + * @param int $length length of the matched string. + * @param string $content optional content instead of the matched string. + * @return bool whether there's a string literal at the current offset. + */ + protected abstract function isStringLiteral(&$length, &$content); + + /** + * Returns whether the given string is a keyword. + * The method may set `$content` to a string that will be used as a token content. + * @param string $string string to be matched. + * @param string $content optional content instead of the matched string. + * @return bool whether the given string is a keyword. + */ + protected abstract function isKeyword($string, &$content); + + /** + * Returns whether the longest common prefix equals to the SQL code of the same length at the current offset. + * @param string[] $with strings to be tested. + * The method **will** modify this parameter to speed up lookups. + * @param bool $caseSensitive whether to perform a case sensitive comparison. + * @param int|null $length length of the matched string. + * @param string|null $content matched string. + * @return bool whether a match is found. + */ + protected function startsWithAnyLongest(array &$with, $caseSensitive, &$length = null, &$content = null) + { + if (empty($with)) { + return false; + } + + if (!is_array(reset($with))) { + usort($with, function ($string1, $string2) { + return mb_strlen($string2, 'UTF-8') - mb_strlen($string1, 'UTF-8'); + }); + $map = []; + foreach ($with as $string) { + $map[mb_strlen($string, 'UTF-8')][$caseSensitive ? $string : mb_strtoupper($string, 'UTF-8')] = true; + } + $with = $map; + } + foreach ($with as $testLength => $testValues) { + $content = $this->substring($testLength, $caseSensitive); + if (isset($testValues[$content])) { + $length = $testLength; + return true; + } + } + return false; + } + + /** + * Returns a string of the given length starting with the specified offset. + * @param int $length string length to be returned. + * @param bool $caseSensitive if it's `false`, the string will be uppercased. + * @param int|null $offset SQL code offset, defaults to current if `null` is passed. + * @return string result string, it may be empty if there's nothing to return. + */ + protected function substring($length, $caseSensitive = true, $offset = null) + { + if ($offset === null) { + $offset = $this->offset; + } + if ($offset + $length > $this->length) { + return ''; + } + + $cacheKey = $offset . ',' . $length; + if (!isset($this->_substrings[$cacheKey . ',1'])) { + $this->_substrings[$cacheKey . ',1'] = mb_substr($this->sql, $offset, $length, 'UTF-8'); + } + if (!$caseSensitive && !isset($this->_substrings[$cacheKey . ',0'])) { + $this->_substrings[$cacheKey . ',0'] = mb_strtoupper($this->_substrings[$cacheKey . ',1'], 'UTF-8'); + } + return $this->_substrings[$cacheKey . ',' . (int) $caseSensitive]; + } + + /** + * Returns an index after the given string in the SQL code starting with the specified offset. + * @param string $string string to be found. + * @param int|null $offset SQL code offset, defaults to current if `null` is passed. + * @return int index after the given string or end of string index. + */ + protected function indexAfter($string, $offset = null) + { + if ($offset === null) { + $offset = $this->offset; + } + if ($offset + mb_strlen($string, 'UTF-8') > $this->length) { + return $this->length; + } + + $afterIndexOf = mb_strpos($this->sql, $string, $offset, 'UTF-8'); + if ($afterIndexOf === false) { + $afterIndexOf = $this->length; + } else { + $afterIndexOf += mb_strlen($string, 'UTF-8'); + } + return $afterIndexOf; + } + + /** + * Determines whether there is a delimited string at the current offset and adds it to the token children. + * @param int $length + * @return bool + */ + private function tokenizeDelimitedString(&$length) + { + $isIdentifier = $this->isIdentifier($length, $content); + $isStringLiteral = !$isIdentifier && $this->isStringLiteral($length, $content); + if (!$isIdentifier && !$isStringLiteral) { + return false; + } + + $this->addTokenFromBuffer(); + $this->_currentToken[] = new SqlToken([ + 'type' => $isIdentifier ? SqlToken::TYPE_IDENTIFIER : SqlToken::TYPE_STRING_LITERAL, + 'content' => is_string($content) ? $content : $this->substring($length), + 'startOffset' => $this->offset, + 'endOffset' => $this->offset + $length, + ]); + return true; + } + + /** + * Determines whether there is an operator at the current offset and adds it to the token children. + * @param int $length + * @return bool + */ + private function tokenizeOperator(&$length) + { + if (!$this->isOperator($length, $content)) { + return false; + } + + $this->addTokenFromBuffer(); + switch ($this->substring($length)) { + case '(': + $this->_currentToken[] = new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => is_string($content) ? $content : $this->substring($length), + 'startOffset' => $this->offset, + 'endOffset' => $this->offset + $length, + ]); + $this->_currentToken[] = new SqlToken(['type' => SqlToken::TYPE_PARENTHESIS]); + $this->_tokenStack->push($this->_currentToken[-1]); + $this->_currentToken = $this->_tokenStack->top(); + break; + case ')': + $this->_tokenStack->pop(); + $this->_currentToken = $this->_tokenStack->top(); + $this->_currentToken[] = new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => $this->offset, + 'endOffset' => $this->offset + $length, + ]); + break; + case ';': + if (!$this->_currentToken->getHasChildren()) { + break; + } + + $this->_currentToken[] = new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => is_string($content) ? $content : $this->substring($length), + 'startOffset' => $this->offset, + 'endOffset' => $this->offset + $length, + ]); + $this->_tokenStack->pop(); + $this->_currentToken = $this->_tokenStack->top(); + $this->_currentToken[] = new SqlToken(['type' => SqlToken::TYPE_STATEMENT]); + $this->_tokenStack->push($this->_currentToken[-1]); + $this->_currentToken = $this->_tokenStack->top(); + break; + default: + $this->_currentToken[] = new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => is_string($content) ? $content : $this->substring($length), + 'startOffset' => $this->offset, + 'endOffset' => $this->offset + $length, + ]); + break; + } + return true; + } + + /** + * Determines a type of text in the buffer, tokenizes it and adds it to the token children. + */ + private function addTokenFromBuffer() + { + if ($this->_buffer === '') { + return; + } + + $isKeyword = $this->isKeyword($this->_buffer, $content); + $this->_currentToken[] = new SqlToken([ + 'type' => $isKeyword ? SqlToken::TYPE_KEYWORD : SqlToken::TYPE_TOKEN, + 'content' => is_string($content) ? $content : $this->_buffer, + 'startOffset' => $this->offset - mb_strlen($this->_buffer, 'UTF-8'), + 'endOffset' => $this->offset, + ]); + $this->_buffer = ''; + } + + /** + * Adds the specified length to the current offset. + * @param int $length + * @throws InvalidParamException + */ + private function advance($length) + { + if ($length <= 0) { + throw new InvalidParamException('Length must be greater than 0.'); + } + + $this->offset += $length; + $this->_substrings = []; + } + + /** + * Returns whether the SQL code is completely traversed. + * @return bool + */ + private function isEof() + { + return $this->offset >= $this->length; + } +} diff --git a/framework/db/cubrid/QueryBuilder.php b/framework/db/cubrid/QueryBuilder.php index ce3ffce..bbd688c 100644 --- a/framework/db/cubrid/QueryBuilder.php +++ b/framework/db/cubrid/QueryBuilder.php @@ -8,6 +8,7 @@ namespace yii\db\cubrid; use yii\base\InvalidParamException; +use yii\base\NotSupportedException; use yii\db\Exception; /** @@ -57,6 +58,7 @@ class QueryBuilder extends \yii\db\QueryBuilder '!' => '!!', ]; + /** * Creates a SQL statement for resetting the sequence value of a table's primary key. * The sequence will be reset such that the primary key of the next new row inserted @@ -118,6 +120,24 @@ class QueryBuilder extends \yii\db\QueryBuilder } /** + * @inheritDoc + * @throws NotSupportedException this is not supported by CUBRID. + */ + public function addCheck($name, $table, $check) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by CUBRID.'); + } + + /** + * @inheritDoc + * @throws NotSupportedException this is not supported by CUBRID. + */ + public function dropCheck($name, $table) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by CUBRID.'); + } + + /** * @inheritdoc * @since 2.0.8 */ diff --git a/framework/db/cubrid/Schema.php b/framework/db/cubrid/Schema.php index dfea42e..774965a 100644 --- a/framework/db/cubrid/Schema.php +++ b/framework/db/cubrid/Schema.php @@ -7,10 +7,15 @@ namespace yii\db\cubrid; +use yii\db\Constraint; +use yii\db\ConstraintFinderTrait; use yii\db\Expression; +use yii\db\ForeignKeyConstraint; +use yii\db\IndexConstraint; use yii\db\TableSchema; use yii\db\ColumnSchema; use yii\db\Transaction; +use yii\helpers\ArrayHelper; /** * Schema is the class for retrieving metadata from a CUBRID database (version 9.3.x and higher). @@ -20,6 +25,8 @@ use yii\db\Transaction; */ class Schema extends \yii\db\Schema { + use ConstraintFinderTrait; + /** * @var array mapping from physical column types (keys) to abstract column types (values) * Please refer to [CUBRID manual](http://www.cubrid.org/manual/91/en/sql/datatype.html) for @@ -74,48 +81,25 @@ class Schema extends \yii\db\Schema /** - * @inheritdoc + * @inheritDoc */ - public function releaseSavepoint($name) - { - // does nothing as cubrid does not support this - } - - /** - * Quotes a table name for use in a query. - * A simple table name has no schema prefix. - * @param string $name table name - * @return string the properly quoted table name - */ - public function quoteSimpleTableName($name) - { - return strpos($name, '"') !== false ? $name : '"' . $name . '"'; - } - - /** - * Quotes a column name for use in a query. - * A simple column name has no prefix. - * @param string $name column name - * @return string the properly quoted column name - */ - public function quoteSimpleColumnName($name) + protected function findTableNames($schema = '') { - return strpos($name, '"') !== false || $name === '*' ? $name : '"' . $name . '"'; - } + $pdo = $this->db->getSlavePdo(); + $tables = $pdo->cubrid_schema(\PDO::CUBRID_SCH_TABLE); + $tableNames = []; + foreach ($tables as $table) { + // do not list system tables + if ($table['TYPE'] != 0) { + $tableNames[] = $table['NAME']; + } + } - /** - * Creates a query builder for the CUBRID database. - * @return QueryBuilder query builder instance - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); + return $tableNames; } /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema driver dependent table metadata. Null if the table does not exist. + * @inheritDoc */ protected function loadTableSchema($name) { @@ -164,6 +148,123 @@ class Schema extends \yii\db\Schema } /** + * @inheritDoc + */ + protected function loadTablePrimaryKey($tableName) + { + $primaryKey = $this->db->getSlavePdo()->cubrid_schema(\PDO::CUBRID_SCH_PRIMARY_KEY, $tableName); + if (empty($primaryKey)) { + return null; + } + + ArrayHelper::multisort($primaryKey, 'KEY_SEQ', SORT_ASC, SORT_NUMERIC); + return new Constraint([ + 'name' => $primaryKey[0]['KEY_NAME'], + 'columnNames' => ArrayHelper::getColumn($primaryKey, 'ATTR_NAME'), + ]); + } + + /** + * @inheritDoc + */ + protected function loadTableForeignKeys($tableName) + { + static $actionTypes = [ + 0 => 'CASCADE', + 1 => 'RESTRICT', + 2 => 'NO ACTION', + 3 => 'SET NULL', + ]; + + $foreignKeys = $this->db->getSlavePdo()->cubrid_schema(\PDO::CUBRID_SCH_IMPORTED_KEYS, $tableName); + $foreignKeys = ArrayHelper::index($foreignKeys, null, 'FK_NAME'); + ArrayHelper::multisort($foreignKeys, 'KEY_SEQ', SORT_ASC, SORT_NUMERIC); + $result = []; + foreach ($foreignKeys as $name => $foreignKey) { + $result[] = new ForeignKeyConstraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($foreignKey, 'FKCOLUMN_NAME'), + 'foreignTableName' => $foreignKey[0]['PKTABLE_NAME'], + 'foreignColumnNames' => ArrayHelper::getColumn($foreignKey, 'PKCOLUMN_NAME'), + 'onDelete' => isset($actionTypes[$foreignKey[0]['DELETE_RULE']]) ? $actionTypes[$foreignKey[0]['DELETE_RULE']] : null, + 'onUpdate' => isset($actionTypes[$foreignKey[0]['UPDATE_RULE']]) ? $actionTypes[$foreignKey[0]['UPDATE_RULE']] : null, + ]); + } + return $result; + } + + /** + * @inheritDoc + */ + protected function loadTableIndexes($tableName) + { + return $this->loadTableConstraints($tableName, 'indexes'); + } + + /** + * @inheritDoc + */ + protected function loadTableUniques($tableName) + { + return $this->loadTableConstraints($tableName, 'uniques'); + } + + /** + * @inheritDoc + */ + protected function loadTableChecks($tableName) + { + return []; + } + + /** + * @inheritDoc + */ + protected function loadTableDefaultValues($tableName) + { + return []; + } + + /** + * @inheritdoc + */ + public function releaseSavepoint($name) + { + // does nothing as cubrid does not support this + } + + /** + * Quotes a table name for use in a query. + * A simple table name has no schema prefix. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteSimpleTableName($name) + { + return strpos($name, '"') !== false ? $name : '"' . $name . '"'; + } + + /** + * Quotes a column name for use in a query. + * A simple column name has no prefix. + * @param string $name column name + * @return string the properly quoted column name + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '"') !== false || $name === '*' ? $name : '"' . $name . '"'; + } + + /** + * Creates a query builder for the CUBRID database. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** * Loads the column information into a [[ColumnSchema]] object. * @param array $info column information * @return ColumnSchema the column schema object @@ -235,26 +336,6 @@ class Schema extends \yii\db\Schema } /** - * Returns all table names in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. - */ - protected function findTableNames($schema = '') - { - $pdo = $this->db->getSlavePdo(); - $tables = $pdo->cubrid_schema(\PDO::CUBRID_SCH_TABLE); - $tableNames = []; - foreach ($tables as $table) { - // do not list system tables - if ($table['TYPE'] != 0) { - $tableNames[] = $table['NAME']; - } - } - - return $tableNames; - } - - /** * Determines the PDO type for the given PHP data value. * @param mixed $data the data whose PDO type is to be determined * @return int the PDO type @@ -306,4 +387,41 @@ class Schema extends \yii\db\Schema { return new ColumnSchemaBuilder($type, $length, $this->db); } + + /** + * @param string $tableName + * @param string $returnType + * @return mixed + */ + private function loadTableConstraints($tableName, $returnType) + { + $constraints = $this->db->getSlavePdo()->cubrid_schema(\PDO::CUBRID_SCH_CONSTRAINT, $tableName); + $constraints = ArrayHelper::index($constraints, null, ['TYPE', 'NAME']); + ArrayHelper::multisort($columns, 'KEY_ORDER', SORT_ASC, SORT_NUMERIC); + $result = [ + 'indexes' => [], + 'uniques' => [], + ]; + foreach ($constraints as $type => $names) { + foreach ($names as $name => $constraint) { + $isUnique = in_array((int) $type, [0, 2], true); + $result['indexes'][] = new IndexConstraint([ + 'isPrimary' => (bool) $constraint[0]['PRIMARY_KEY'], + 'isUnique' => $isUnique, + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'ATTR_NAME'), + ]); + if ($isUnique) { + $result['uniques'][] = new Constraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'ATTR_NAME'), + ]); + } + } + } + foreach ($result as $type => $data) { + $this->setTableMetadata($tableName, $type, $data); + } + return $result[$returnType]; + } } diff --git a/framework/db/mssql/QueryBuilder.php b/framework/db/mssql/QueryBuilder.php index f8cfa33..3cbcc19 100644 --- a/framework/db/mssql/QueryBuilder.php +++ b/framework/db/mssql/QueryBuilder.php @@ -56,6 +56,7 @@ class QueryBuilder extends \yii\db\QueryBuilder '\\' => '[\\]', ]; + /** * @inheritdoc */ @@ -176,6 +177,25 @@ class QueryBuilder extends \yii\db\QueryBuilder } /** + * @inheritDoc + */ + public function addDefaultValue($name, $table, $column, $default) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT ' + . $this->db->quoteColumnName($name) . ' DEFAULT ' . $this->db->quoteValue($default) . ' FOR ' + . $this->db->quoteColumnName($column); + } + + /** + * @inheritDoc + */ + public function dropDefaultValue($name, $table) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) + . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name); + } + + /** * Creates a SQL statement for resetting the sequence value of a table's primary key. * The sequence will be reset such that the primary key of the next new row inserted * will have the specified value or 1. diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php index 82a8b53..82b2ebf 100644 --- a/framework/db/mssql/Schema.php +++ b/framework/db/mssql/Schema.php @@ -7,11 +7,18 @@ namespace yii\db\mssql; +use yii\db\CheckConstraint; use yii\db\ColumnSchema; +use yii\db\Constraint; +use yii\db\ConstraintFinderTrait; +use yii\db\DefaultConstraint; +use yii\db\ForeignKeyConstraint; +use yii\db\IndexConstraint; use yii\db\ViewFinderTrait; +use yii\helpers\ArrayHelper; /** - * Schema is the class for retrieving metadata from a MS SQL Server databases (version 2008 and above). + * Schema is the class for retrieving metadata from MS SQL Server databases (version 2008 and above). * * @author Timur Ruziev * @since 2.0 @@ -19,6 +26,7 @@ use yii\db\ViewFinderTrait; class Schema extends \yii\db\Schema { use ViewFinderTrait; + use ConstraintFinderTrait; /** * @var string the default schema used for the current session. @@ -73,6 +81,169 @@ class Schema extends \yii\db\Schema /** + * Resolves the table name and schema name (if any). + * @param string $name the table name + * @return TableSchema resolved table, schema, etc. names. + */ + protected function resolveTableName($name) + { + $resolvedName = new TableSchema(); + $parts = explode('.', str_replace(['[', ']'], '', $name)); + $partCount = count($parts); + if ($partCount === 4) { + // server name, catalog name, schema name and table name passed + $resolvedName->catalogName = $parts[1]; + $resolvedName->schemaName = $parts[2]; + $resolvedName->name = $parts[3]; + $resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name; + } elseif ($partCount === 3) { + // catalog name, schema name and table name passed + $resolvedName->catalogName = $parts[0]; + $resolvedName->schemaName = $parts[1]; + $resolvedName->name = $parts[2]; + $resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name; + } elseif ($partCount === 2) { + // only schema name and table name passed + $resolvedName->schemaName = $parts[0]; + $resolvedName->name = $parts[1]; + $resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name; + } else { + // only table name passed + $resolvedName->schemaName = $this->defaultSchema; + $resolvedName->fullName = $resolvedName->name = $parts[0]; + } + return $resolvedName; + } + + /** + * @inheritDoc + */ + protected function findSchemaNames() + { + $sql = <<db->createCommand($sql)->queryColumn(); + } + + /** + * @inheritDoc + */ + protected function findTableNames($schema = '') + { + if ($schema === '') { + $schema = $this->defaultSchema; + } + + $sql = <<db->createCommand($sql, [':schema' => $schema])->queryColumn(); + } + + /** + * @inheritDoc + */ + protected function loadTableSchema($name) + { + $table = new TableSchema(); + $this->resolveTableNames($table, $name); + $this->findPrimaryKeys($table); + if ($this->findColumns($table)) { + $this->findForeignKeys($table); + + return $table; + } else { + return null; + } + } + + /** + * @inheritDoc + */ + protected function loadTablePrimaryKey($tableName) + { + return $this->loadTableConstraints($tableName, 'primaryKey'); + } + + /** + * @inheritDoc + */ + protected function loadTableForeignKeys($tableName) + { + return $this->loadTableConstraints($tableName, 'foreignKeys'); + } + + /** + * @inheritDoc + */ + protected function loadTableIndexes($tableName) + { + static $sql = <<resolveTableName($tableName); + $indexes = $this->db->createCommand($sql, [ + ':fullName' => $resolvedName->fullName, + ])->queryAll(); + $indexes = $this->normalizePdoRowKeyCase($indexes, true); + $indexes = ArrayHelper::index($indexes, null, 'name'); + $result = []; + foreach ($indexes as $name => $index) { + $result[] = new IndexConstraint([ + 'isPrimary' => (bool) $index[0]['index_is_primary'], + 'isUnique' => (bool) $index[0]['index_is_unique'], + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($index, 'column_name'), + ]); + } + return $result; + } + + /** + * @inheritDoc + */ + protected function loadTableUniques($tableName) + { + return $this->loadTableConstraints($tableName, 'uniques'); + } + + /** + * @inheritDoc + */ + protected function loadTableChecks($tableName) + { + return $this->loadTableConstraints($tableName, 'checks'); + } + + /** + * @inheritDoc + */ + protected function loadTableDefaultValues($tableName) + { + return $this->loadTableConstraints($tableName, 'defaults'); + } + + /** * @inheritdoc */ public function createSavepoint($name) @@ -128,25 +299,6 @@ class Schema extends \yii\db\Schema } /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema|null driver dependent table metadata. Null if the table does not exist. - */ - public function loadTableSchema($name) - { - $table = new TableSchema(); - $this->resolveTableNames($table, $name); - $this->findPrimaryKeys($table); - if ($this->findColumns($table)) { - $this->findForeignKeys($table); - - return $table; - } else { - return null; - } - } - - /** * Resolves the table name and schema name (if any). * @param TableSchema $table the table metadata object * @param string $name the table name @@ -399,27 +551,6 @@ SQL; } /** - * Returns all table names in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. - */ - protected function findTableNames($schema = '') - { - if ($schema === '') { - $schema = $this->defaultSchema; - } - - $sql = <<db->createCommand($sql, [':schema' => $schema])->queryColumn(); - } - - /** * @inheritdoc */ protected function findViewNames($schema = '') @@ -461,4 +592,113 @@ SQL; } return $result; } + + /** + * @param string $tableName + * @param string $returnType + * @return mixed + */ + private function loadTableConstraints($tableName, $returnType) + { + static $sql = <<resolveTableName($tableName); + $constraints = $this->db->createCommand($sql, [ + ':fullName' => $resolvedName->fullName, + ])->queryAll(); + $constraints = $this->normalizePdoRowKeyCase($constraints, true); + $constraints = ArrayHelper::index($constraints, null, ['type', 'name']); + $result = [ + 'primaryKey' => null, + 'foreignKeys' => [], + 'uniques' => [], + 'checks' => [], + 'defaults' => [], + ]; + foreach ($constraints as $type => $names) { + foreach ($names as $name => $constraint) { + switch ($type) { + case 'PK': + $result['primaryKey'] = new Constraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + ]); + break; + case 'F': + $result['foreignKeys'][] = new ForeignKeyConstraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + 'foreignSchemaName' => $constraint[0]['foreign_table_schema'], + 'foreignTableName' => $constraint[0]['foreign_table_name'], + 'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'), + 'onDelete' => str_replace('_', '', $constraint[0]['on_delete']), + 'onUpdate' => str_replace('_', '', $constraint[0]['on_update']), + ]); + break; + case 'UQ': + $result['uniques'][] = new Constraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + ]); + break; + case 'C': + $result['checks'][] = new CheckConstraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + 'expression' => $constraint[0]['check_expr'] + ]); + break; + case 'D': + $result['defaults'][] = new DefaultConstraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + 'value' => $constraint[0]['default_expr'] + ]); + break; + } + } + } + foreach ($result as $type => $data) { + $this->setTableMetadata($tableName, $type, $data); + } + return $result[$returnType]; + } } diff --git a/framework/db/mysql/QueryBuilder.php b/framework/db/mysql/QueryBuilder.php index 2f2bb04..41b024b 100644 --- a/framework/db/mysql/QueryBuilder.php +++ b/framework/db/mysql/QueryBuilder.php @@ -8,6 +8,7 @@ namespace yii\db\mysql; use yii\base\InvalidParamException; +use yii\base\NotSupportedException; use yii\db\Exception; use yii\db\Expression; @@ -120,6 +121,24 @@ class QueryBuilder extends \yii\db\QueryBuilder } /** + * @inheritDoc + * @throws NotSupportedException this is not supported by MySQL. + */ + public function addCheck($name, $table, $check) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.'); + } + + /** + * @inheritDoc + * @throws NotSupportedException this is not supported by MySQL. + */ + public function dropCheck($name, $table) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.'); + } + + /** * Creates a SQL statement for resetting the sequence value of a table's primary key. * The sequence will be reset such that the primary key of the next new row inserted * will have the specified value or 1. diff --git a/framework/db/mysql/Schema.php b/framework/db/mysql/Schema.php index 4ea23fa..a3c3b85 100644 --- a/framework/db/mysql/Schema.php +++ b/framework/db/mysql/Schema.php @@ -7,9 +7,16 @@ namespace yii\db\mysql; +use yii\base\InvalidConfigException; +use yii\db\ColumnSchema; +use yii\db\Constraint; +use yii\db\ConstraintFinderTrait; +use yii\db\Exception; use yii\db\Expression; +use yii\db\ForeignKeyConstraint; +use yii\db\IndexConstraint; use yii\db\TableSchema; -use yii\db\ColumnSchema; +use yii\helpers\ArrayHelper; /** * Schema is the class for retrieving metadata from a MySQL database (version 4.1.x and 5.x). @@ -19,6 +26,14 @@ use yii\db\ColumnSchema; */ class Schema extends \yii\db\Schema { + use ConstraintFinderTrait; + + /** + * @var bool whether MySQL used is older than 5.1. + */ + private $_oldMysql; + + /** * @var array mapping from physical column types (keys) to abstract column types (values) */ @@ -53,6 +68,128 @@ class Schema extends \yii\db\Schema 'varbinary' => self::TYPE_BINARY, ]; + /** + * @inheritDoc + */ + protected function resolveTableName($name) + { + $resolvedName = new TableSchema(); + $parts = explode('.', str_replace('`', '', $name)); + if (isset($parts[1])) { + $resolvedName->schemaName = $parts[0]; + $resolvedName->name = $parts[1]; + } else { + $resolvedName->schemaName = $this->defaultSchema; + $resolvedName->name = $name; + } + $resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name; + return $resolvedName; + } + + /** + * @inheritDoc + */ + protected function findTableNames($schema = '') + { + $sql = 'SHOW TABLES'; + if ($schema !== '') { + $sql .= ' FROM ' . $this->quoteSimpleTableName($schema); + } + + return $this->db->createCommand($sql)->queryColumn(); + } + + /** + * @inheritDoc + */ + protected function loadTableSchema($name) + { + $table = new TableSchema; + $this->resolveTableNames($table, $name); + + if ($this->findColumns($table)) { + $this->findConstraints($table); + + return $table; + } else { + return null; + } + } + + /** + * @inheritDoc + */ + protected function loadTablePrimaryKey($tableName) + { + return $this->loadTableConstraints($tableName, 'primaryKey'); + } + + /** + * @inheritDoc + */ + protected function loadTableForeignKeys($tableName) + { + return $this->loadTableConstraints($tableName, 'foreignKeys'); + } + + /** + * @inheritDoc + */ + protected function loadTableIndexes($tableName) + { + static $sql = <<resolveTableName($tableName); + $indexes = $this->db->createCommand($sql, [ + ':schemaName' => $resolvedName->schemaName, + ':tableName' => $resolvedName->name, + ])->queryAll(); + $indexes = $this->normalizePdoRowKeyCase($indexes, true); + $indexes = ArrayHelper::index($indexes, null, 'name'); + $result = []; + foreach ($indexes as $name => $index) { + $result[] = new IndexConstraint([ + 'isPrimary' => (bool) $index[0]['index_is_primary'], + 'isUnique' => (bool) $index[0]['index_is_unique'], + 'name' => $name !== 'PRIMARY' ? $name : null, + 'columnNames' => ArrayHelper::getColumn($index, 'column_name'), + ]); + } + return $result; + } + + /** + * @inheritDoc + */ + protected function loadTableUniques($tableName) + { + return $this->loadTableConstraints($tableName, 'uniques'); + } + + /** + * @inheritDoc + */ + protected function loadTableChecks($tableName) + { + return []; + } + + /** + * @inheritDoc + */ + protected function loadTableDefaultValues($tableName) + { + return []; + } /** * Quotes a table name for use in a query. @@ -86,25 +223,6 @@ class Schema extends \yii\db\Schema } /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema driver dependent table metadata. Null if the table does not exist. - */ - protected function loadTableSchema($name) - { - $table = new TableSchema; - $this->resolveTableNames($table, $name); - - if ($this->findColumns($table)) { - $this->findConstraints($table); - - return $table; - } else { - return null; - } - } - - /** * Resolves the table name and schema name (if any). * @param TableSchema $table the table metadata object * @param string $name the table name @@ -338,25 +456,101 @@ SQL; } /** - * Returns all table names in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. + * @inheritdoc */ - protected function findTableNames($schema = '') + public function createColumnSchemaBuilder($type, $length = null) { - $sql = 'SHOW TABLES'; - if ($schema !== '') { - $sql .= ' FROM ' . $this->quoteSimpleTableName($schema); - } + return new ColumnSchemaBuilder($type, $length, $this->db); + } - return $this->db->createCommand($sql)->queryColumn(); + /** + * @return bool whether the version of the MySQL being used is older than 5.1. + * @throws InvalidConfigException + * @throws Exception + * @since 2.0.13 + */ + protected function isOldMysql() + { + if ($this->_oldMysql === null) { + $version = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION); + $this->_oldMysql = version_compare($version, '5.1', '<='); + } + return $this->_oldMysql; } /** - * @inheritdoc + * @param string $tableName + * @param string $returnType + * @return mixed */ - public function createColumnSchemaBuilder($type, $length = null) + private function loadTableConstraints($tableName, $returnType) { - return new ColumnSchemaBuilder($type, $length, $this->db); + static $sql = <<resolveTableName($tableName); + $constraints = $this->db->createCommand($sql, [ + ':schemaName' => $resolvedName->schemaName, + ':tableName' => $resolvedName->name, + ])->queryAll(); + $constraints = $this->normalizePdoRowKeyCase($constraints, true); + $constraints = ArrayHelper::index($constraints, null, ['type', 'name']); + $result = [ + 'primaryKey' => null, + 'foreignKeys' => [], + 'uniques' => [], + ]; + foreach ($constraints as $type => $names) { + foreach ($names as $name => $constraint) { + switch ($type) { + case 'PRIMARY KEY': + $result['primaryKey'] = new Constraint([ + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + ]); + break; + case 'FOREIGN KEY': + $result['foreignKeys'][] = new ForeignKeyConstraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + 'foreignSchemaName' => $constraint[0]['foreign_table_schema'], + 'foreignTableName' => $constraint[0]['foreign_table_name'], + 'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'), + 'onDelete' => $constraint[0]['on_delete'], + 'onUpdate' => $constraint[0]['on_update'], + ]); + break; + case 'UNIQUE': + $result['uniques'][] = new Constraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + ]); + break; + } + } + } + foreach ($result as $type => $data) { + $this->setTableMetadata($tableName, $type, $data); + } + return $result[$returnType]; } } diff --git a/framework/db/oci/QueryBuilder.php b/framework/db/oci/QueryBuilder.php index 57b3110..3a5c813 100644 --- a/framework/db/oci/QueryBuilder.php +++ b/framework/db/oci/QueryBuilder.php @@ -61,6 +61,7 @@ class QueryBuilder extends \yii\db\QueryBuilder '!' => '!!', ]; + /** * @inheritdoc */ diff --git a/framework/db/oci/Schema.php b/framework/db/oci/Schema.php index 28d2b2f..897dbc6 100644 --- a/framework/db/oci/Schema.php +++ b/framework/db/oci/Schema.php @@ -8,10 +8,16 @@ namespace yii\db\oci; use yii\base\InvalidCallException; +use yii\db\CheckConstraint; use yii\db\ColumnSchema; use yii\db\Connection; +use yii\db\Constraint; +use yii\db\ConstraintFinderTrait; use yii\db\Expression; +use yii\db\ForeignKeyConstraint; +use yii\db\IndexConstraint; use yii\db\TableSchema; +use yii\helpers\ArrayHelper; /** * Schema is the class for retrieving metadata from an Oracle database @@ -24,6 +30,8 @@ use yii\db\TableSchema; */ class Schema extends \yii\db\Schema { + use ConstraintFinderTrait; + /** * @var array map of DB errors and corresponding exceptions * If left part is found in DB error message exception class from the right part is used. @@ -45,6 +53,180 @@ class Schema extends \yii\db\Schema } /** + * @inheritDoc + */ + protected function resolveTableName($name) + { + $resolvedName = new TableSchema(); + $parts = explode('.', str_replace('"', '', $name)); + if (isset($parts[1])) { + $resolvedName->schemaName = $parts[0]; + $resolvedName->name = $parts[1]; + } else { + $resolvedName->schemaName = $this->defaultSchema; + $resolvedName->name = $name; + } + $resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name; + return $resolvedName; + } + + /** + * @inheritDoc + */ + protected function findSchemaNames() + { + $sql = <<db->createCommand($sql)->queryColumn(); + } + + /** + * @inheritDoc + */ + protected function findTableNames($schema = '') + { + if ($schema === '') { + $sql = <<db->createCommand($sql); + } else { + $sql = <<db->createCommand($sql, [':schema' => $schema]); + } + + $rows = $command->queryAll(); + $names = []; + foreach ($rows as $row) { + if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_LOWER) { + $row = array_change_key_case($row, CASE_UPPER); + } + $names[] = $row['TABLE_NAME']; + } + return $names; + } + + /** + * @inheritDoc + */ + protected function loadTableSchema($name) + { + $table = new TableSchema(); + $this->resolveTableNames($table, $name); + if ($this->findColumns($table)) { + $this->findConstraints($table); + return $table; + } else { + return null; + } + } + + /** + * @inheritDoc + */ + protected function loadTablePrimaryKey($tableName) + { + return $this->loadTableConstraints($tableName, 'primaryKey'); + } + + /** + * @inheritDoc + */ + protected function loadTableForeignKeys($tableName) + { + return $this->loadTableConstraints($tableName, 'foreignKeys'); + } + + /** + * @inheritDoc + */ + protected function loadTableIndexes($tableName) + { + static $sql = <<resolveTableName($tableName); + $indexes = $this->db->createCommand($sql, [ + ':schemaName' => $resolvedName->schemaName, + ':tableName' => $resolvedName->name, + ])->queryAll(); + $indexes = $this->normalizePdoRowKeyCase($indexes, true); + $indexes = ArrayHelper::index($indexes, null, 'name'); + $result = []; + foreach ($indexes as $name => $index) { + $result[] = new IndexConstraint([ + 'isPrimary' => (bool) $index[0]['index_is_primary'], + 'isUnique' => (bool) $index[0]['index_is_unique'], + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($index, 'column_name'), + ]); + } + return $result; + } + + /** + * @inheritDoc + */ + protected function loadTableUniques($tableName) + { + return $this->loadTableConstraints($tableName, 'uniques'); + } + + /** + * @inheritDoc + */ + protected function loadTableChecks($tableName) + { + return $this->loadTableConstraints($tableName, 'checks'); + } + + /** + * @inheritDoc + */ + protected function loadTableDefaultValues($tableName) + { + return []; + } + + /** * @inheritdoc */ public function releaseSavepoint($name) @@ -77,23 +259,6 @@ class Schema extends \yii\db\Schema } /** - * @inheritdoc - */ - public function loadTableSchema($name) - { - $table = new TableSchema(); - $this->resolveTableNames($table, $name); - - if ($this->findColumns($table)) { - $this->findConstraints($table); - - return $table; - } else { - return null; - } - } - - /** * Resolves the table name and schema name (if any). * * @param TableSchema $table the table metadata object @@ -321,67 +486,6 @@ SQL; } /** - * @inheritdoc - */ - protected function findSchemaNames() - { - $sql = <<db->createCommand($sql)->queryColumn(); - } - - /** - * @inheritdoc - */ - protected function findTableNames($schema = '') - { - if ($schema === '') { - $sql = <<db->createCommand($sql); - } else { - $sql = <<db->createCommand($sql, [':schema' => $schema]); - } - - $rows = $command->queryAll(); - $names = []; - foreach ($rows as $row) { - if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_LOWER) { - $row = array_change_key_case($row, CASE_UPPER); - } - $names[] = $row['TABLE_NAME']; - } - return $names; - } - - /** * Returns all unique indexes for the given table. * Each array element is of the following structure: * @@ -523,4 +627,88 @@ SQL; return $result; } + + /** + * @param string $tableName + * @param string $returnType + * @return mixed + */ + private function loadTableConstraints($tableName, $returnType) + { + static $sql = <<resolveTableName($tableName); + $constraints = $this->db->createCommand($sql, [ + ':schemaName' => $resolvedName->schemaName, + ':tableName' => $resolvedName->name, + ])->queryAll(); + $constraints = $this->normalizePdoRowKeyCase($constraints, true); + $constraints = ArrayHelper::index($constraints, null, ['type', 'name']); + $result = [ + 'primaryKey' => null, + 'foreignKeys' => [], + 'uniques' => [], + 'checks' => [], + ]; + foreach ($constraints as $type => $names) { + foreach ($names as $name => $constraint) { + switch ($type) { + case 'P': + $result['primaryKey'] = new Constraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + ]); + break; + case 'R': + $result['foreignKeys'][] = new ForeignKeyConstraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + 'foreignSchemaName' => $constraint[0]['foreign_table_schema'], + 'foreignTableName' => $constraint[0]['foreign_table_name'], + 'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'), + 'onDelete' => $constraint[0]['on_delete'], + 'onUpdate' => null, + ]); + break; + case 'U': + $result['uniques'][] = new Constraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + ]); + break; + case 'C': + $result['checks'][] = new CheckConstraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + 'expression' => $constraint[0]['check_expr'] + ]); + break; + } + } + } + foreach ($result as $type => $data) { + $this->setTableMetadata($tableName, $type, $data); + } + return $result[$returnType]; + } } diff --git a/framework/db/pgsql/Schema.php b/framework/db/pgsql/Schema.php index 3eb4f72..9178118 100644 --- a/framework/db/pgsql/Schema.php +++ b/framework/db/pgsql/Schema.php @@ -7,23 +7,28 @@ namespace yii\db\pgsql; +use yii\db\CheckConstraint; +use yii\db\Constraint; +use yii\db\ConstraintFinderTrait; use yii\db\Expression; +use yii\db\ForeignKeyConstraint; +use yii\db\IndexConstraint; use yii\db\TableSchema; use yii\db\ColumnSchema; use yii\db\ViewFinderTrait; +use yii\helpers\ArrayHelper; /** * Schema is the class for retrieving metadata from a PostgreSQL database * (version 9.x and above). * - * @property string[] $viewNames All view names in the database. This property is read-only. - * * @author Gevik Babakhani * @since 2.0 */ class Schema extends \yii\db\Schema { use ViewFinderTrait; + use ConstraintFinderTrait; /** * @var string the default schema used for the current session. @@ -115,51 +120,59 @@ class Schema extends \yii\db\Schema /** - * Creates a query builder for the PostgreSQL database. - * @return QueryBuilder query builder instance - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); - } - - /** - * Resolves the table name and schema name (if any). - * @param TableSchema $table the table metadata object - * @param string $name the table name + * @inheritDoc */ - protected function resolveTableNames($table, $name) + protected function resolveTableName($name) { + $resolvedName = new TableSchema(); $parts = explode('.', str_replace('"', '', $name)); - if (isset($parts[1])) { - $table->schemaName = $parts[0]; - $table->name = $parts[1]; + $resolvedName->schemaName = $parts[0]; + $resolvedName->name = $parts[1]; } else { - $table->schemaName = $this->defaultSchema; - $table->name = $name; + $resolvedName->schemaName = $this->defaultSchema; + $resolvedName->name = $name; } + $resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name; + return $resolvedName; + } - $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name; + /** + * @inheritDoc + */ + protected function findSchemaNames() + { + $sql = <<db->createCommand($sql)->queryColumn(); } /** - * Quotes a table name for use in a query. - * A simple table name has no schema prefix. - * @param string $name table name - * @return string the properly quoted table name + * @inheritDoc */ - public function quoteSimpleTableName($name) + protected function findTableNames($schema = '') { - return strpos($name, '"') !== false ? $name : '"' . $name . '"'; + if ($schema === '') { + $schema = $this->defaultSchema; + } + $sql = <<db->createCommand($sql, [':schemaName' => $schema])->queryColumn(); } /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema|null driver dependent table metadata. Null if the table does not exist. + * @inheritDoc */ - public function loadTableSchema($name) + protected function loadTableSchema($name) { $table = new TableSchema(); $this->resolveTableNames($table, $name); @@ -172,41 +185,126 @@ class Schema extends \yii\db\Schema } /** - * Returns all schema names in the database, including the default one but not system schemas. - * This method should be overridden by child classes in order to support this feature - * because the default implementation simply throws an exception. - * @return array all schema names in the database, except system schemas - * @since 2.0.4 + * @inheritDoc */ - protected function findSchemaNames() + protected function loadTablePrimaryKey($tableName) { - $sql = <<loadTableConstraints($tableName, 'primaryKey'); + } + + /** + * @inheritDoc + */ + protected function loadTableForeignKeys($tableName) + { + return $this->loadTableConstraints($tableName, 'foreignKeys'); + } + + /** + * @inheritDoc + */ + protected function loadTableIndexes($tableName) + { + static $sql = <<db->createCommand($sql)->queryColumn(); + + $resolvedName = $this->resolveTableName($tableName); + $indexes = $this->db->createCommand($sql, [ + ':schemaName' => $resolvedName->schemaName, + ':tableName' => $resolvedName->name, + ])->queryAll(); + $indexes = $this->normalizePdoRowKeyCase($indexes, true); + $indexes = ArrayHelper::index($indexes, null, 'name'); + $result = []; + foreach ($indexes as $name => $index) { + $result[] = new IndexConstraint([ + 'isPrimary' => (bool) $index[0]['index_is_primary'], + 'isUnique' => (bool) $index[0]['index_is_unique'], + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($index, 'column_name'), + ]); + } + return $result; } /** - * Returns all table names in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. + * @inheritDoc */ - protected function findTableNames($schema = '') + protected function loadTableUniques($tableName) { - if ($schema === '') { - $schema = $this->defaultSchema; + return $this->loadTableConstraints($tableName, 'uniques'); + } + + /** + * @inheritDoc + */ + protected function loadTableChecks($tableName) + { + return $this->loadTableConstraints($tableName, 'checks'); + } + + /** + * @inheritDoc + */ + protected function loadTableDefaultValues($tableName) + { + return []; + } + + /** + * Creates a query builder for the PostgreSQL database. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Resolves the table name and schema name (if any). + * @param TableSchema $table the table metadata object + * @param string $name the table name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace('"', '', $name)); + + if (isset($parts[1])) { + $table->schemaName = $parts[0]; + $table->name = $parts[1]; + } else { + $table->schemaName = $this->defaultSchema; + $table->name = $name; } - $sql = <<db->createCommand($sql, [':schemaName' => $schema])->queryColumn(); + + $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name; + } + + /** + * Quotes a table name for use in a query. + * A simple table name has no schema prefix. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteSimpleTableName($name) + { + return strpos($name, '"') !== false ? $name : '"' . $name . '"'; } /** @@ -508,4 +606,101 @@ SQL; return !$command->pdoStatement->rowCount() ? false : $result; } + + /** + * @param string $tableName + * @param string $returnType + * @return mixed + */ + private function loadTableConstraints($tableName, $returnType) + { + static $sql = << 'NO ACTION', + 'r' => 'RESTRICT', + 'c' => 'CASCADE', + 'n' => 'SET NULL', + 'd' => 'SET DEFAULT', + ]; + + $resolvedName = $this->resolveTableName($tableName); + $constraints = $this->db->createCommand($sql, [ + ':schemaName' => $resolvedName->schemaName, + ':tableName' => $resolvedName->name, + ])->queryAll(); + $constraints = $this->normalizePdoRowKeyCase($constraints, true); + $constraints = ArrayHelper::index($constraints, null, ['type', 'name']); + $result = [ + 'primaryKey' => null, + 'foreignKeys' => [], + 'uniques' => [], + 'checks' => [], + ]; + foreach ($constraints as $type => $names) { + foreach ($names as $name => $constraint) { + switch ($type) { + case 'p': + $result['primaryKey'] = new Constraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + ]); + break; + case 'f': + $result['foreignKeys'][] = new ForeignKeyConstraint([ + 'name' => $name, + 'columnNames' => array_keys(array_count_values(ArrayHelper::getColumn($constraint, 'column_name'))), + 'foreignSchemaName' => $constraint[0]['foreign_table_schema'], + 'foreignTableName' => $constraint[0]['foreign_table_name'], + 'foreignColumnNames' => array_keys(array_count_values(ArrayHelper::getColumn($constraint, 'foreign_column_name'))), + 'onDelete' => isset($actionTypes[$constraint[0]['on_delete']]) ? $actionTypes[$constraint[0]['on_delete']] : null, + 'onUpdate' => isset($actionTypes[$constraint[0]['on_update']]) ? $actionTypes[$constraint[0]['on_update']] : null, + ]); + break; + case 'u': + $result['uniques'][] = new Constraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + ]); + break; + case 'c': + $result['checks'][] = new CheckConstraint([ + 'name' => $name, + 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), + 'expression' => $constraint[0]['check_expr'] + ]); + break; + } + } + } + foreach ($result as $type => $data) { + $this->setTableMetadata($tableName, $type, $data); + } + return $result[$returnType]; + } } diff --git a/framework/db/sqlite/QueryBuilder.php b/framework/db/sqlite/QueryBuilder.php index 3cdb28a..c41c764 100644 --- a/framework/db/sqlite/QueryBuilder.php +++ b/framework/db/sqlite/QueryBuilder.php @@ -53,6 +53,7 @@ class QueryBuilder extends \yii\db\QueryBuilder */ protected $likeEscapeCharacter = '\\'; + /** * Generates a batch INSERT SQL statement. * For example, @@ -300,6 +301,60 @@ class QueryBuilder extends \yii\db\QueryBuilder } /** + * @inheritDoc + * @throws NotSupportedException this is not supported by SQLite. + */ + public function addUnique($name, $table, $columns) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } + + /** + * @inheritDoc + * @throws NotSupportedException this is not supported by SQLite. + */ + public function dropUnique($name, $table) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } + + /** + * @inheritDoc + * @throws NotSupportedException this is not supported by SQLite. + */ + public function addCheck($name, $table, $check) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } + + /** + * @inheritDoc + * @throws NotSupportedException this is not supported by SQLite. + */ + public function dropCheck($name, $table) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } + + /** + * @inheritDoc + * @throws NotSupportedException this is not supported by SQLite. + */ + public function addDefaultValue($name, $table, $column, $default) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } + + /** + * @inheritDoc + * @throws NotSupportedException this is not supported by SQLite. + */ + public function dropDefaultValue($name, $table) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } + + /** * @inheritdoc * @throws NotSupportedException * @since 2.0.8 diff --git a/framework/db/sqlite/Schema.php b/framework/db/sqlite/Schema.php index 1b58c33..d886f23 100644 --- a/framework/db/sqlite/Schema.php +++ b/framework/db/sqlite/Schema.php @@ -8,10 +8,16 @@ namespace yii\db\sqlite; use yii\base\NotSupportedException; +use yii\db\CheckConstraint; +use yii\db\Constraint; +use yii\db\ConstraintFinderTrait; use yii\db\Expression; +use yii\db\ForeignKeyConstraint; +use yii\db\IndexConstraint; use yii\db\TableSchema; use yii\db\ColumnSchema; use yii\db\Transaction; +use yii\helpers\ArrayHelper; /** * Schema is the class for retrieving metadata from a SQLite (2/3) database. @@ -24,6 +30,8 @@ use yii\db\Transaction; */ class Schema extends \yii\db\Schema { + use ConstraintFinderTrait; + /** * @var array mapping from physical column types (keys) to abstract column types (values) */ @@ -58,6 +66,120 @@ class Schema extends \yii\db\Schema 'enum' => self::TYPE_STRING, ]; + /** + * @inheritDoc + */ + protected function findTableNames($schema = '') + { + $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name"; + return $this->db->createCommand($sql)->queryColumn(); + } + + /** + * @inheritDoc + */ + protected function loadTableSchema($name) + { + $table = new TableSchema; + $table->name = $name; + $table->fullName = $name; + + if ($this->findColumns($table)) { + $this->findConstraints($table); + + return $table; + } else { + return null; + } + } + + /** + * @inheritDoc + */ + protected function loadTablePrimaryKey($tableName) + { + return $this->loadTableConstraints($tableName, 'primaryKey'); + } + + /** + * @inheritDoc + */ + protected function loadTableForeignKeys($tableName) + { + $foreignKeys = $this->db->createCommand('PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')')->queryAll(); + $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true); + $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table'); + ArrayHelper::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC); + $result = []; + foreach ($foreignKeys as $table => $foreignKey) { + $result[] = new ForeignKeyConstraint([ + 'columnNames' => ArrayHelper::getColumn($foreignKey, 'from'), + 'foreignTableName' => $table, + 'foreignColumnNames' => ArrayHelper::getColumn($foreignKey, 'to'), + 'onDelete' => isset($foreignKey[0]['on_delete']) ? $foreignKey[0]['on_delete'] : null, + 'onUpdate' => isset($foreignKey[0]['on_update']) ? $foreignKey[0]['on_update'] : null, + ]); + } + return $result; + } + + /** + * @inheritDoc + */ + protected function loadTableIndexes($tableName) + { + return $this->loadTableConstraints($tableName, 'indexes'); + } + + /** + * @inheritDoc + */ + protected function loadTableUniques($tableName) + { + return $this->loadTableConstraints($tableName, 'uniques'); + } + + /** + * @inheritDoc + */ + protected function loadTableChecks($tableName) + { + $sql = $this->db->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [ + ':tableName' => $tableName, + ])->queryScalar(); + $code = (new SqlTokenizer($sql))->tokenize(); + if (!$code[0]->matches('any CREATE any TABLE any()', 0, $firstMatchIndex, $lastMatchIndex)) { + return []; + } + + $createTableToken = $code[0][$lastMatchIndex - 1]; + $result = []; + $offset = 0; + while (true) { + if (!$createTableToken->matches('any CHECK()', $offset, $firstMatchIndex, $offset)) { + break; + } + + $checkSql = $createTableToken[$offset - 1]->getSql(); + $name = null; + if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches('CONSTRAINT any', $firstMatchIndex - 2)) { + $name = $createTableToken[$firstMatchIndex - 1]->content; + } + $result[] = new CheckConstraint([ + 'name' => $name, + 'expression' => $checkSql, + ]); + } + return $result; + } + + /** + * @inheritDoc + */ + protected function loadTableDefaultValues($tableName) + { + return []; + } /** * Quotes a table name for use in a query. @@ -101,38 +223,6 @@ class Schema extends \yii\db\Schema } /** - * Returns all table names in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. - */ - protected function findTableNames($schema = '') - { - $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name"; - - return $this->db->createCommand($sql)->queryColumn(); - } - - /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema driver dependent table metadata. Null if the table does not exist. - */ - protected function loadTableSchema($name) - { - $table = new TableSchema; - $table->name = $name; - $table->fullName = $name; - - if ($this->findColumns($table)) { - $this->findConstraints($table); - - return $table; - } else { - return null; - } - } - - /** * Collects the table column metadata. * @param TableSchema $table the table metadata * @return bool whether the table exists in the database @@ -290,4 +380,45 @@ class Schema extends \yii\db\Schema throw new NotSupportedException(get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.'); } } + + /** + * @param string $tableName + * @param string $returnType + * @return mixed + */ + private function loadTableConstraints($tableName, $returnType) + { + $indexes = $this->db->createCommand('PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')')->queryAll(); + $indexes = $this->normalizePdoRowKeyCase($indexes, true); + $result = [ + 'primaryKey' => null, + 'indexes' => [], + 'uniques' => [], + ]; + foreach ($indexes as $index) { + $columns = $this->db->createCommand('PRAGMA INDEX_INFO (' . $this->quoteValue($index['name']) . ')')->queryAll(); + $columns = $this->normalizePdoRowKeyCase($columns, true); + ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC); + $result['indexes'][] = new IndexConstraint([ + 'isPrimary' => $index['origin'] === 'pk', + 'isUnique' => (bool) $index['unique'], + 'name' => $index['name'], + 'columnNames' => ArrayHelper::getColumn($columns, 'name'), + ]); + if ($index['origin'] === 'u') { + $result['uniques'][] = new Constraint([ + 'name' => $index['name'], + 'columnNames' => ArrayHelper::getColumn($columns, 'name'), + ]); + } elseif ($index['origin'] === 'pk') { + $result['primaryKey'] = new Constraint([ + 'columnNames' => ArrayHelper::getColumn($columns, 'name'), + ]); + } + } + foreach ($result as $type => $data) { + $this->setTableMetadata($tableName, $type, $data); + } + return $result[$returnType]; + } } diff --git a/framework/db/sqlite/SqlTokenizer.php b/framework/db/sqlite/SqlTokenizer.php new file mode 100644 index 0000000..6f83ee6 --- /dev/null +++ b/framework/db/sqlite/SqlTokenizer.php @@ -0,0 +1,288 @@ + + * @since 2.0.13 + */ +class SqlTokenizer extends \yii\db\SqlTokenizer +{ + /** + * @inheritDoc + */ + protected function isWhitespace(&$length) + { + static $whitespaces = [ + "\f" => true, + "\n" => true, + "\r" => true, + "\t" => true, + ' ' => true, + ]; + + $length = 1; + return isset($whitespaces[$this->substring($length)]); + } + + /** + * @inheritDoc + */ + protected function isComment(&$length) + { + static $comments = [ + '--' => true, + '/*' => true, + ]; + + $length = 2; + if (!isset($comments[$this->substring($length)])) { + return false; + } + + if ($this->substring($length) === '--') { + $length = $this->indexAfter("\n") - $this->offset; + } else { + $length = $this->indexAfter('*/') - $this->offset; + } + return true; + } + + /** + * @inheritDoc + */ + protected function isOperator(&$length, &$content) + { + static $operators = [ + '!=', + '%', + '&', + '(', + ')', + '*', + '+', + ',', + '-', + '.', + '/', + ';', + '<', + '<<', + '<=', + '<>', + '=', + '==', + '>', + '>=', + '>>', + '|', + '||', + '~', + ]; + + return $this->startsWithAnyLongest($operators, true, $length); + } + + /** + * @inheritDoc + */ + protected function isIdentifier(&$length, &$content) + { + static $identifierDelimiters = [ + '"' => '"', + '[' => ']', + '`' => '`', + ]; + + if (!isset($identifierDelimiters[$this->substring(1)])) { + return false; + } + + $delimiter = $identifierDelimiters[$this->substring(1)]; + $offset = $this->offset; + while (true) { + $offset = $this->indexAfter($delimiter, $offset + 1); + if ($delimiter === ']' || $this->substring(1, true, $offset) !== $delimiter) { + break; + } + } + $length = $offset - $this->offset; + $content = $this->substring($length - 2,true, $this->offset + 1); + if ($delimiter !== ']') { + $content = strtr($content, ["$delimiter$delimiter" => $delimiter]); + } + return true; + } + + /** + * @inheritDoc + */ + protected function isStringLiteral(&$length, &$content) + { + if ($this->substring(1) !== "'") { + return false; + } + + $offset = $this->offset; + while (true) { + $offset = $this->indexAfter("'", $offset + 1); + if ($this->substring(1, true, $offset) !== "'") { + break; + } + } + $length = $offset - $this->offset; + $content = strtr($this->substring($length - 2,true, $this->offset + 1), ["''" => "'"]); + return true; + } + + /** + * @inheritDoc + */ + protected function isKeyword($string, &$content) + { + static $keywords = [ + 'ABORT' => true, + 'ACTION' => true, + 'ADD' => true, + 'AFTER' => true, + 'ALL' => true, + 'ALTER' => true, + 'ANALYZE' => true, + 'AND' => true, + 'AS' => true, + 'ASC' => true, + 'ATTACH' => true, + 'AUTOINCREMENT' => true, + 'BEFORE' => true, + 'BEGIN' => true, + 'BETWEEN' => true, + 'BY' => true, + 'CASCADE' => true, + 'CASE' => true, + 'CAST' => true, + 'CHECK' => true, + 'COLLATE' => true, + 'COLUMN' => true, + 'COMMIT' => true, + 'CONFLICT' => true, + 'CONSTRAINT' => true, + 'CREATE' => true, + 'CROSS' => true, + 'CURRENT_DATE' => true, + 'CURRENT_TIME' => true, + 'CURRENT_TIMESTAMP' => true, + 'DATABASE' => true, + 'DEFAULT' => true, + 'DEFERRABLE' => true, + 'DEFERRED' => true, + 'DELETE' => true, + 'DESC' => true, + 'DETACH' => true, + 'DISTINCT' => true, + 'DROP' => true, + 'EACH' => true, + 'ELSE' => true, + 'END' => true, + 'ESCAPE' => true, + 'EXCEPT' => true, + 'EXCLUSIVE' => true, + 'EXISTS' => true, + 'EXPLAIN' => true, + 'FAIL' => true, + 'FOR' => true, + 'FOREIGN' => true, + 'FROM' => true, + 'FULL' => true, + 'GLOB' => true, + 'GROUP' => true, + 'HAVING' => true, + 'IF' => true, + 'IGNORE' => true, + 'IMMEDIATE' => true, + 'IN' => true, + 'INDEX' => true, + 'INDEXED' => true, + 'INITIALLY' => true, + 'INNER' => true, + 'INSERT' => true, + 'INSTEAD' => true, + 'INTERSECT' => true, + 'INTO' => true, + 'IS' => true, + 'ISNULL' => true, + 'JOIN' => true, + 'KEY' => true, + 'LEFT' => true, + 'LIKE' => true, + 'LIMIT' => true, + 'MATCH' => true, + 'NATURAL' => true, + 'NO' => true, + 'NOT' => true, + 'NOTNULL' => true, + 'NULL' => true, + 'OF' => true, + 'OFFSET' => true, + 'ON' => true, + 'OR' => true, + 'ORDER' => true, + 'OUTER' => true, + 'PLAN' => true, + 'PRAGMA' => true, + 'PRIMARY' => true, + 'QUERY' => true, + 'RAISE' => true, + 'RECURSIVE' => true, + 'REFERENCES' => true, + 'REGEXP' => true, + 'REINDEX' => true, + 'RELEASE' => true, + 'RENAME' => true, + 'REPLACE' => true, + 'RESTRICT' => true, + 'RIGHT' => true, + 'ROLLBACK' => true, + 'ROW' => true, + 'SAVEPOINT' => true, + 'SELECT' => true, + 'SET' => true, + 'TABLE' => true, + 'TEMP' => true, + 'TEMPORARY' => true, + 'THEN' => true, + 'TO' => true, + 'TRANSACTION' => true, + 'TRIGGER' => true, + 'UNION' => true, + 'UNIQUE' => true, + 'UPDATE' => true, + 'USING' => true, + 'VACUUM' => true, + 'VALUES' => true, + 'VIEW' => true, + 'VIRTUAL' => true, + 'WHEN' => true, + 'WHERE' => true, + 'WITH' => true, + 'WITHOUT' => true, + ]; + + $string = mb_strtoupper($string, 'UTF-8'); + if (!isset($keywords[$string])) { + return false; + } + + $content = $string; + return true; + } +} diff --git a/tests/data/cubrid.sql b/tests/data/cubrid.sql index fa133f2..5d222a3 100644 --- a/tests/data/cubrid.sql +++ b/tests/data/cubrid.sql @@ -20,6 +20,10 @@ DROP TABLE IF EXISTS "animal"; DROP TABLE IF EXISTS "default_pk"; DROP TABLE IF EXISTS "document"; DROP VIEW IF EXISTS "animal_view"; +DROP TABLE IF EXISTS "T_constraints_4"; +DROP TABLE IF EXISTS "T_constraints_3"; +DROP TABLE IF EXISTS "T_constraints_2"; +DROP TABLE IF EXISTS "T_constraints_1"; CREATE TABLE "constraints" ( @@ -213,3 +217,42 @@ CREATE TABLE `bit_values` ( ); INSERT INTO `bit_values` (id, val) VALUES (1, b'0'), (2, b'1'); + +CREATE TABLE "T_constraints_1" +( + "C_id" INT NOT NULL PRIMARY KEY, + "C_not_null" INT NOT NULL, + "C_check" VARCHAR(255) NULL CHECK ("C_check" <> ''), + "C_unique" INT NOT NULL, + "C_default" INT NOT NULL DEFAULT 0, + CONSTRAINT "CN_unique" UNIQUE ("C_unique") +); + +CREATE TABLE "T_constraints_2" +( + "C_id_1" INT NOT NULL, + "C_id_2" INT NOT NULL, + "C_index_1" INT NULL, + "C_index_2_1" INT NULL, + "C_index_2_2" INT NULL, + CONSTRAINT "CN_constraints_2_multi" UNIQUE ("C_index_2_1", "C_index_2_2"), + CONSTRAINT "CN_pk" PRIMARY KEY ("C_id_1", "C_id_2") +); + +CREATE INDEX "CN_constraints_2_single" ON "T_constraints_2" ("C_index_1"); + +CREATE TABLE "T_constraints_3" +( + "C_id" INT NOT NULL, + "C_fk_id_1" INT NOT NULL, + "C_fk_id_2" INT NOT NULL, + CONSTRAINT "CN_constraints_3" FOREIGN KEY ("C_fk_id_1", "C_fk_id_2") REFERENCES "T_constraints_2" ("C_id_1", "C_id_2") ON DELETE RESTRICT ON UPDATE RESTRICT +); + +CREATE TABLE "T_constraints_4" +( + "C_id" INT NOT NULL PRIMARY KEY, + "C_col_1" INT NULL, + "C_col_2" INT NOT NULL, + CONSTRAINT "CN_constraints_4" UNIQUE ("C_col_1", "C_col_2") +); diff --git a/tests/data/mssql.sql b/tests/data/mssql.sql index 9a5733d..58c5802 100644 --- a/tests/data/mssql.sql +++ b/tests/data/mssql.sql @@ -13,6 +13,10 @@ IF OBJECT_ID('[dbo].[animal]', 'U') IS NOT NULL DROP TABLE [dbo].[animal]; IF OBJECT_ID('[dbo].[default_pk]', 'U') IS NOT NULL DROP TABLE [dbo].[default_pk]; IF OBJECT_ID('[dbo].[document]', 'U') IS NOT NULL DROP TABLE [dbo].[document]; IF OBJECT_ID('[dbo].[animal_view]', 'V') IS NOT NULL DROP VIEW [dbo].[animal_view]; +IF OBJECT_ID('[T_constraints_4]', 'U') IS NOT NULL DROP TABLE [T_constraints_4]; +IF OBJECT_ID('[T_constraints_3]', 'U') IS NOT NULL DROP TABLE [T_constraints_3]; +IF OBJECT_ID('[T_constraints_2]', 'U') IS NOT NULL DROP TABLE [T_constraints_2]; +IF OBJECT_ID('[T_constraints_1]', 'U') IS NOT NULL DROP TABLE [T_constraints_1]; CREATE TABLE [dbo].[profile] ( [id] [int] IDENTITY NOT NULL, @@ -205,3 +209,42 @@ CREATE TABLE [dbo].[bit_values] ( ); INSERT INTO [dbo].[bit_values] ([val]) VALUES (0), (1); + +CREATE TABLE [T_constraints_1] +( + [C_id] INT NOT NULL IDENTITY PRIMARY KEY, + [C_not_null] INT NOT NULL, + [C_check] VARCHAR(255) NULL CHECK ([C_check] <> ''), + [C_unique] INT NOT NULL, + [C_default] INT NOT NULL DEFAULT 0, + CONSTRAINT [CN_unique] UNIQUE ([C_unique]) +); + +CREATE TABLE [T_constraints_2] +( + [C_id_1] INT NOT NULL, + [C_id_2] INT NOT NULL, + [C_index_1] INT NULL, + [C_index_2_1] INT NULL, + [C_index_2_2] INT NULL, + CONSTRAINT [CN_constraints_2_multi] UNIQUE ([C_index_2_1], [C_index_2_2]), + CONSTRAINT [CN_pk] PRIMARY KEY ([C_id_1], [C_id_2]) +); + +CREATE INDEX [CN_constraints_2_single] ON [T_constraints_2] ([C_index_1]); + +CREATE TABLE [T_constraints_3] +( + [C_id] INT NOT NULL, + [C_fk_id_1] INT NOT NULL, + [C_fk_id_2] INT NOT NULL, + CONSTRAINT [CN_constraints_3] FOREIGN KEY ([C_fk_id_1], [C_fk_id_2]) REFERENCES [T_constraints_2] ([C_id_1], [C_id_2]) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE [T_constraints_4] +( + [C_id] INT NOT NULL IDENTITY PRIMARY KEY, + [C_col_1] INT NULL, + [C_col_2] INT NOT NULL, + CONSTRAINT [CN_constraints_4] UNIQUE ([C_col_1], [C_col_2]) +); diff --git a/tests/data/mysql.sql b/tests/data/mysql.sql index 83afc85..3aec022 100644 --- a/tests/data/mysql.sql +++ b/tests/data/mysql.sql @@ -21,6 +21,10 @@ DROP TABLE IF EXISTS `default_pk` CASCADE; DROP TABLE IF EXISTS `document` CASCADE; DROP TABLE IF EXISTS `comment` CASCADE; DROP VIEW IF EXISTS `animal_view`; +DROP TABLE IF EXISTS `T_constraints_4` CASCADE; +DROP TABLE IF EXISTS `T_constraints_3` CASCADE; +DROP TABLE IF EXISTS `T_constraints_2` CASCADE; +DROP TABLE IF EXISTS `T_constraints_1` CASCADE; CREATE TABLE `constraints` ( @@ -257,3 +261,46 @@ CREATE TABLE `bit_values` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `bit_values` (id, val) VALUES (1, b'0'), (2, b'1'); + +CREATE TABLE `T_constraints_1` +( + `C_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `C_not_null` INT NOT NULL, + `C_check` VARCHAR(255) NULL CHECK (`C_check` <> ''), + `C_unique` INT NOT NULL, + `C_default` INT NOT NULL DEFAULT 0, + CONSTRAINT `CN_unique` UNIQUE (`C_unique`) +) +ENGINE = 'InnoDB' DEFAULT CHARSET = 'utf8'; + +CREATE TABLE `T_constraints_2` +( + `C_id_1` INT NOT NULL, + `C_id_2` INT NOT NULL, + `C_index_1` INT NULL, + `C_index_2_1` INT NULL, + `C_index_2_2` INT NULL, + CONSTRAINT `CN_constraints_2_multi` UNIQUE (`C_index_2_1`, `C_index_2_2`), + CONSTRAINT `CN_pk` PRIMARY KEY (`C_id_1`, `C_id_2`) +) +ENGINE = 'InnoDB' DEFAULT CHARSET = 'utf8'; + +CREATE INDEX `CN_constraints_2_single` ON `T_constraints_2` (`C_index_1`); + +CREATE TABLE `T_constraints_3` +( + `C_id` INT NOT NULL, + `C_fk_id_1` INT NOT NULL, + `C_fk_id_2` INT NOT NULL, + CONSTRAINT `CN_constraints_3` FOREIGN KEY (`C_fk_id_1`, `C_fk_id_2`) REFERENCES `T_constraints_2` (`C_id_1`, `C_id_2`) ON DELETE CASCADE ON UPDATE CASCADE +) +ENGINE = 'InnoDB' DEFAULT CHARSET = 'utf8'; + +CREATE TABLE `T_constraints_4` +( + `C_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `C_col_1` INT NULL, + `C_col_2` INT NOT NULL, + CONSTRAINT `CN_constraints_4` UNIQUE (`C_col_1`, `C_col_2`) +) +ENGINE = 'InnoDB' DEFAULT CHARSET = 'utf8'; diff --git a/tests/data/oci.sql b/tests/data/oci.sql index 5c18cf4..bc06fac 100644 --- a/tests/data/oci.sql +++ b/tests/data/oci.sql @@ -23,6 +23,10 @@ BEGIN EXECUTE IMMEDIATE 'DROP VIEW "animal_view"'; EXCEPTION WHEN OTHERS THEN IF BEGIN EXECUTE IMMEDIATE 'DROP TABLE "validator_main"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP TABLE "validator_ref"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP TABLE "bit_values"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END; -- +BEGIN EXECUTE IMMEDIATE 'DROP TABLE "T_constraints_4"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- +BEGIN EXECUTE IMMEDIATE 'DROP TABLE "T_constraints_3"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- +BEGIN EXECUTE IMMEDIATE 'DROP TABLE "T_constraints_2"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- +BEGIN EXECUTE IMMEDIATE 'DROP TABLE "T_constraints_1"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP SEQUENCE "profile_SEQ"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -2289 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP SEQUENCE "customer_SEQ"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -2289 THEN RAISE; END IF; END;-- @@ -199,6 +203,45 @@ CREATE TABLE "bit_values" ( CONSTRAINT "bit_values_val" CHECK ("val" IN ('1','0')) ); +CREATE TABLE "T_constraints_1" +( + "C_id" INT NOT NULL PRIMARY KEY, + "C_not_null" INT NOT NULL, + "C_check" VARCHAR(255) NULL CHECK ("C_check" <> ''), + "C_unique" INT NOT NULL, + "C_default" INT DEFAULT 0 NOT NULL, + CONSTRAINT "CN_unique" UNIQUE ("C_unique") +); + +CREATE TABLE "T_constraints_2" +( + "C_id_1" INT NOT NULL, + "C_id_2" INT NOT NULL, + "C_index_1" INT NULL, + "C_index_2_1" INT NULL, + "C_index_2_2" INT NULL, + CONSTRAINT "CN_constraints_2_multi" UNIQUE ("C_index_2_1", "C_index_2_2"), + CONSTRAINT "CN_pk" PRIMARY KEY ("C_id_1", "C_id_2") +); + +CREATE INDEX "CN_constraints_2_single" ON "T_constraints_2" ("C_index_1"); + +CREATE TABLE "T_constraints_3" +( + "C_id" INT NOT NULL, + "C_fk_id_1" INT NOT NULL, + "C_fk_id_2" INT NOT NULL, + CONSTRAINT "CN_constraints_3" FOREIGN KEY ("C_fk_id_1", "C_fk_id_2") REFERENCES "T_constraints_2" ("C_id_1", "C_id_2") ON DELETE CASCADE +); + +CREATE TABLE "T_constraints_4" +( + "C_id" INT NOT NULL PRIMARY KEY, + "C_col_1" INT NULL, + "C_col_2" INT NOT NULL, + CONSTRAINT "CN_constraints_4" UNIQUE ("C_col_1", "C_col_2") +); + /** * (Postgres-)Database Schema for validator tests */ diff --git a/tests/data/postgres.sql b/tests/data/postgres.sql index 6defb4c..5f2ba0d 100644 --- a/tests/data/postgres.sql +++ b/tests/data/postgres.sql @@ -23,6 +23,11 @@ DROP TABLE IF EXISTS "default_pk" CASCADE; DROP TABLE IF EXISTS "document" CASCADE; DROP TABLE IF EXISTS "comment" CASCADE; DROP VIEW IF EXISTS "animal_view"; +DROP TABLE IF EXISTS "T_constraints_4"; +DROP TABLE IF EXISTS "T_constraints_3"; +DROP TABLE IF EXISTS "T_constraints_2"; +DROP TABLE IF EXISTS "T_constraints_1"; + DROP SCHEMA IF EXISTS "schema1" CASCADE; DROP SCHEMA IF EXISTS "schema2" CASCADE; @@ -259,3 +264,42 @@ CREATE TABLE "bit_values" ( ); INSERT INTO "bit_values" (id, val) VALUES (1, '0'), (2, '1'); + +CREATE TABLE "T_constraints_1" +( + "C_id" INT NOT NULL PRIMARY KEY, + "C_not_null" INT NOT NULL, + "C_check" VARCHAR(255) NULL CHECK ("C_check" <> ''), + "C_unique" INT NOT NULL, + "C_default" INT NOT NULL DEFAULT 0, + CONSTRAINT "CN_unique" UNIQUE ("C_unique") +); + +CREATE TABLE "T_constraints_2" +( + "C_id_1" INT NOT NULL, + "C_id_2" INT NOT NULL, + "C_index_1" INT NULL, + "C_index_2_1" INT NULL, + "C_index_2_2" INT NULL, + CONSTRAINT "CN_constraints_2_multi" UNIQUE ("C_index_2_1", "C_index_2_2"), + CONSTRAINT "CN_pk" PRIMARY KEY ("C_id_1", "C_id_2") +); + +CREATE INDEX "CN_constraints_2_single" ON "T_constraints_2" ("C_index_1"); + +CREATE TABLE "T_constraints_3" +( + "C_id" INT NOT NULL, + "C_fk_id_1" INT NOT NULL, + "C_fk_id_2" INT NOT NULL, + CONSTRAINT "CN_constraints_3" FOREIGN KEY ("C_fk_id_1", "C_fk_id_2") REFERENCES "T_constraints_2" ("C_id_1", "C_id_2") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "T_constraints_4" +( + "C_id" INT NOT NULL PRIMARY KEY, + "C_col_1" INT NULL, + "C_col_2" INT NOT NULL, + CONSTRAINT "CN_constraints_4" UNIQUE ("C_col_1", "C_col_2") +); diff --git a/tests/data/sqlite.sql b/tests/data/sqlite.sql index 324635d..bc8c86a 100644 --- a/tests/data/sqlite.sql +++ b/tests/data/sqlite.sql @@ -19,6 +19,10 @@ DROP TABLE IF EXISTS "animal"; DROP TABLE IF EXISTS "default_pk"; DROP TABLE IF EXISTS "document"; DROP VIEW IF EXISTS "animal_view"; +DROP TABLE IF EXISTS "T_constraints_4"; +DROP TABLE IF EXISTS "T_constraints_3"; +DROP TABLE IF EXISTS "T_constraints_2"; +DROP TABLE IF EXISTS "T_constraints_1"; CREATE TABLE "profile" ( id INTEGER NOT NULL, @@ -227,3 +231,42 @@ CREATE TABLE "bit_values" ( INSERT INTO "bit_values" (id, val) VALUES (1, 0); INSERT INTO "bit_values" (id, val) VALUES (2, 1); + +CREATE TABLE "T_constraints_1" +( + "C_id" INT NOT NULL PRIMARY KEY, + "C_not_null" INT NOT NULL, + "C_check" VARCHAR(255) NULL CHECK ("C_check" <> ''), + "C_unique" INT NOT NULL, + "C_default" INT NOT NULL DEFAULT 0, + CONSTRAINT "CN_unique" UNIQUE ("C_unique") +); + +CREATE TABLE "T_constraints_2" +( + "C_id_1" INT NOT NULL, + "C_id_2" INT NOT NULL, + "C_index_1" INT NULL, + "C_index_2_1" INT NULL, + "C_index_2_2" INT NULL, + CONSTRAINT "CN_pk" PRIMARY KEY ("C_id_1", "C_id_2"), + CONSTRAINT "CN_constraints_2_multi" UNIQUE ("C_index_2_1", "C_index_2_2") +); + +CREATE INDEX "CN_constraints_2_single" ON "T_constraints_2" ("C_index_1"); + +CREATE TABLE "T_constraints_3" +( + "C_id" INT NOT NULL, + "C_fk_id_1" INT NOT NULL, + "C_fk_id_2" INT NOT NULL, + CONSTRAINT "CN_constraints_3" FOREIGN KEY ("C_fk_id_1", "C_fk_id_2") REFERENCES "T_constraints_2" ("C_id_1", "C_id_2") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "T_constraints_4" +( + "C_id" INT NOT NULL PRIMARY KEY, + "C_col_1" INT NULL, + "C_col_2" INT NOT NULL, + CONSTRAINT "CN_constraints_4" UNIQUE ("C_col_1", "C_col_2") +); diff --git a/tests/framework/db/AnyCaseValue.php b/tests/framework/db/AnyCaseValue.php new file mode 100644 index 0000000..0d52eba --- /dev/null +++ b/tests/framework/db/AnyCaseValue.php @@ -0,0 +1,23 @@ +value = array_map('strtolower', $value); + } else { + $this->value = strtolower($value); + } + parent::__construct($config); + } +} diff --git a/tests/framework/db/AnyValue.php b/tests/framework/db/AnyValue.php new file mode 100644 index 0000000..5d65fd2 --- /dev/null +++ b/tests/framework/db/AnyValue.php @@ -0,0 +1,19 @@ +expectException('\yii\db\IntegrityException'); diff --git a/tests/framework/db/CompareValue.php b/tests/framework/db/CompareValue.php new file mode 100644 index 0000000..0700576 --- /dev/null +++ b/tests/framework/db/CompareValue.php @@ -0,0 +1,9 @@ +assertEquals($expectedParams, $params); } - public function testAddDropPrimaryKey() + public function primaryKeysProvider() { - $tableName = 'constraints'; - $pkeyName = $tableName . "_pkey"; + $tableName = 'T_constraints_1'; + $name = 'CN_pk'; + return [ + 'drop' => [ + "ALTER TABLE {{{$tableName}}} DROP CONSTRAINT [[$name]]", + function (QueryBuilder $qb) use ($tableName, $name) { + return $qb->dropPrimaryKey($name, $tableName); + } + ], + 'add' => [ + "ALTER TABLE {{{$tableName}}} ADD CONSTRAINT [[$name]] PRIMARY KEY ([[C_id_1]])", + function (QueryBuilder $qb) use ($tableName, $name) { + return $qb->addPrimaryKey($name, $tableName, 'C_id_1'); + } + ], + 'add (2 columns)' => [ + "ALTER TABLE {{{$tableName}}} ADD CONSTRAINT [[$name]] PRIMARY KEY ([[C_id_1]], [[C_id_2]])", + function (QueryBuilder $qb) use ($tableName, $name) { + return $qb->addPrimaryKey($name, $tableName, 'C_id_1, C_id_2'); + } + ], + ]; + } - // ADD - $qb = $this->getQueryBuilder(); - $qb->db->createCommand()->addPrimaryKey($pkeyName, $tableName, ['id'])->execute(); - $tableSchema = $qb->db->getSchema()->getTableSchema($tableName); - $this->assertCount(1, $tableSchema->primaryKey); + /** + * @dataProvider primaryKeysProvider + */ + public function testAddDropPrimaryKey($sql, \Closure $builder) + { + $this->assertSame($this->getConnection(false)->quoteSql($sql), $builder($this->getQueryBuilder(false))); + } + + public function foreignKeysProvider() + { + $tableName = 'T_constraints_3'; + $name = 'CN_constraints_3'; + $pkTableName = 'T_constraints_2'; + return [ + 'drop' => [ + "ALTER TABLE {{{$tableName}}} DROP CONSTRAINT [[$name]]", + function (QueryBuilder $qb) use ($tableName, $name) { + return $qb->dropForeignKey($name, $tableName); + } + ], + 'add' => [ + "ALTER TABLE {{{$tableName}}} ADD CONSTRAINT [[$name]] FOREIGN KEY ([[C_fk_id_1]]) REFERENCES {{{$pkTableName}}} ([[C_id_1]]) ON DELETE CASCADE ON UPDATE CASCADE", + function (QueryBuilder $qb) use ($tableName, $name, $pkTableName) { + return $qb->addForeignKey($name, $tableName, 'C_fk_id_1', $pkTableName, 'C_id_1', 'CASCADE', 'CASCADE'); + } + ], + 'add (2 columns)' => [ + "ALTER TABLE {{{$tableName}}} ADD CONSTRAINT [[$name]] FOREIGN KEY ([[C_fk_id_1]], [[C_fk_id_2]]) REFERENCES {{{$pkTableName}}} ([[C_id_1]], [[C_id_2]]) ON DELETE CASCADE ON UPDATE CASCADE", + function (QueryBuilder $qb) use ($tableName, $name, $pkTableName) { + return $qb->addForeignKey($name, $tableName, 'C_fk_id_1, C_fk_id_2', $pkTableName, 'C_id_1, C_id_2', 'CASCADE', 'CASCADE'); + } + ], + ]; + } - // DROP - $qb->db->createCommand()->dropPrimaryKey($pkeyName, $tableName)->execute(); - $qb = $this->getQueryBuilder(); // resets the schema - $tableSchema = $qb->db->getSchema()->getTableSchema($tableName); - $this->assertCount(0, $tableSchema->primaryKey); + /** + * @dataProvider foreignKeysProvider + */ + public function testAddDropForeignKey($sql, \Closure $builder) + { + $this->assertSame($this->getConnection(false)->quoteSql($sql), $builder($this->getQueryBuilder(false))); + } - // ADD (2 columns) - $qb = $this->getQueryBuilder(); - $qb->db->createCommand()->addPrimaryKey($pkeyName, $tableName, 'id, field1')->execute(); - $tableSchema = $qb->db->getSchema()->getTableSchema($tableName); - $this->assertCount(2, $tableSchema->primaryKey); - - // DROP (2 columns) - $qb->db->createCommand()->dropPrimaryKey($pkeyName, $tableName)->execute(); - $qb = $this->getQueryBuilder(); // resets the schema - $tableSchema = $qb->db->getSchema()->getTableSchema($tableName); - $this->assertCount(0, $tableSchema->primaryKey); + public function indexesProvider() + { + $tableName = 'T_constraints_2'; + $name1 = 'CN_constraints_2_single'; + $name2 = 'CN_constraints_2_multi'; + return [ + 'drop' => [ + "DROP INDEX [[$name1]] ON {{{$tableName}}}", + function (QueryBuilder $qb) use ($tableName, $name1) { + return $qb->dropIndex($name1, $tableName); + } + ], + 'create' => [ + "CREATE INDEX [[$name1]] ON {{{$tableName}}} ([[C_index_1]])", + function (QueryBuilder $qb) use ($tableName, $name1) { + return $qb->createIndex($name1, $tableName, 'C_index_1'); + } + ], + 'create (2 columns)' => [ + "CREATE INDEX [[$name2]] ON {{{$tableName}}} ([[C_index_2_1]], [[C_index_2_2]])", + function (QueryBuilder $qb) use ($tableName, $name2) { + return $qb->createIndex($name2, $tableName, 'C_index_2_1, C_index_2_2'); + } + ], + 'create unique' => [ + "CREATE UNIQUE INDEX [[$name1]] ON {{{$tableName}}} ([[C_index_1]])", + function (QueryBuilder $qb) use ($tableName, $name1) { + return $qb->createIndex($name1, $tableName, 'C_index_1', true); + } + ], + 'create unique (2 columns)' => [ + "CREATE UNIQUE INDEX [[$name2]] ON {{{$tableName}}} ([[C_index_2_1]], [[C_index_2_2]])", + function (QueryBuilder $qb) use ($tableName, $name2) { + return $qb->createIndex($name2, $tableName, 'C_index_2_1, C_index_2_2', true); + } + ], + ]; + } + + /** + * @dataProvider indexesProvider + */ + public function testCreateDropIndex($sql, \Closure $builder) + { + $this->assertSame($this->getConnection(false)->quoteSql($sql), $builder($this->getQueryBuilder(false))); + } + + public function uniquesProvider() + { + $tableName1 = 'T_constraints_1'; + $name1 = 'CN_unique'; + $tableName2 = 'T_constraints_2'; + $name2 = 'CN_constraints_2_multi'; + return [ + 'drop' => [ + "ALTER TABLE {{{$tableName1}}} DROP CONSTRAINT [[$name1]]", + function (QueryBuilder $qb) use ($tableName1, $name1) { + return $qb->dropUnique($name1, $tableName1); + } + ], + 'add' => [ + "ALTER TABLE {{{$tableName1}}} ADD CONSTRAINT [[$name1]] UNIQUE ([[C_unique]])", + function (QueryBuilder $qb) use ($tableName1, $name1) { + return $qb->addUnique($name1, $tableName1, 'C_unique'); + } + ], + 'add (2 columns)' => [ + "ALTER TABLE {{{$tableName2}}} ADD CONSTRAINT [[$name2]] UNIQUE ([[C_index_2_1]], [[C_index_2_2]])", + function (QueryBuilder $qb) use ($tableName2, $name2) { + return $qb->addUnique($name2, $tableName2, 'C_index_2_1, C_index_2_2'); + } + ], + ]; + } + + /** + * @dataProvider uniquesProvider + */ + public function testAddDropUnique($sql, \Closure $builder) + { + $this->assertSame($this->getConnection(false)->quoteSql($sql), $builder($this->getQueryBuilder(false))); + } + + public function checksProvider() + { + $tableName = 'T_constraints_1'; + $name = 'CN_check'; + return [ + 'drop' => [ + "ALTER TABLE {{{$tableName}}} DROP CONSTRAINT [[$name]]", + function (QueryBuilder $qb) use ($tableName, $name) { + return $qb->dropCheck($name, $tableName); + } + ], + 'add' => [ + "ALTER TABLE {{{$tableName}}} ADD CONSTRAINT [[$name]] CHECK ([[C_not_null]] > 100)", + function (QueryBuilder $qb) use ($tableName, $name) { + return $qb->addCheck($name, $tableName, '[[C_not_null]] > 100'); + } + ], + ]; + } + + /** + * @dataProvider checksProvider + */ + public function testAddDropCheck($sql, \Closure $builder) + { + $this->assertSame($this->getConnection(false)->quoteSql($sql), $builder($this->getQueryBuilder(false))); + } + + public function defaultValuesProvider() + { + $tableName = 'T_constraints_1'; + $name = 'CN_default'; + return [ + 'drop' => [ + "ALTER TABLE {{{$tableName}}} DROP CONSTRAINT [[$name]]", + function (QueryBuilder $qb) use ($tableName, $name) { + return $qb->dropDefaultValue($name, $tableName); + } + ], + 'add' => [ + "ALTER TABLE {{{$tableName}}} ADD CONSTRAINT [[$name]] DEFAULT 0 FOR [[C_default]]", + function (QueryBuilder $qb) use ($tableName, $name) { + return $qb->addDefaultValue($name, $tableName, 'C_default', 0); + } + ], + ]; + } + + /** + * @dataProvider defaultValuesProvider + */ + public function testAddDropDefaultValue($sql, \Closure $builder) + { + $this->assertSame($this->getConnection(false)->quoteSql($sql), $builder($this->getQueryBuilder(false))); } public function existsParamsProvider() diff --git a/tests/framework/db/SchemaTest.php b/tests/framework/db/SchemaTest.php index f80d7ae..b3ff881 100644 --- a/tests/framework/db/SchemaTest.php +++ b/tests/framework/db/SchemaTest.php @@ -4,8 +4,12 @@ namespace yiiunit\framework\db; use PDO; use yii\caching\FileCache; +use yii\db\CheckConstraint; use yii\db\ColumnSchema; +use yii\db\Constraint; use yii\db\Expression; +use yii\db\ForeignKeyConstraint; +use yii\db\IndexConstraint; use yii\db\Schema; abstract class SchemaTest extends DatabaseTestCase @@ -426,4 +430,229 @@ abstract class SchemaTest extends DatabaseTestCase 'someCol2Unique' => ['someCol2'], ], $uniqueIndexes); } + + public function testContraintTablesExistance() + { + $tableNames = [ + 'T_constraints_1', + 'T_constraints_2', + 'T_constraints_3', + 'T_constraints_4', + ]; + $schema = $this->getConnection()->getSchema(); + foreach ($tableNames as $tableName) { + $tableSchema = $schema->getTableSchema($tableName); + $this->assertInstanceOf('yii\db\TableSchema', $tableSchema, $tableName); + } + } + + public function constraintsProvider() + { + return [ + '1: primary key' => ['T_constraints_1', 'primaryKey', new Constraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_id'], + ])], + '1: check' => ['T_constraints_1', 'checks', [ + new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_check'], + 'expression' => "C_check <> ''" + ]) + ]], + '1: unique' => ['T_constraints_1', 'uniques', [ + new Constraint([ + 'name' => 'CN_unique', + 'columnNames' => ['C_unique'], + ]) + ]], + '1: index' => ['T_constraints_1', 'indexes', [ + new IndexConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_id'], + 'isUnique' => true, + 'isPrimary' => true, + ]), + new IndexConstraint([ + 'name' => 'CN_unique', + 'columnNames' => ['C_unique'], + 'isPrimary' => false, + 'isUnique' => true, + ]) + ]], + '1: default' => ['T_constraints_1', 'defaultValues', []], + + '2: primary key' => ['T_constraints_2', 'primaryKey', new Constraint([ + 'name' => 'CN_pk', + 'columnNames' => ['C_id_1', 'C_id_2'], + ])], + '2: unique' => ['T_constraints_2', 'uniques', [ + new Constraint([ + 'name' => 'CN_constraints_2_multi', + 'columnNames' => ['C_index_2_1', 'C_index_2_2'], + ]) + ]], + '2: index' => ['T_constraints_2', 'indexes', [ + new IndexConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_id_1', 'C_id_2'], + 'isUnique' => true, + 'isPrimary' => true, + ]), + new IndexConstraint([ + 'name' => 'CN_constraints_2_single', + 'columnNames' => ['C_index_1'], + 'isPrimary' => false, + 'isUnique' => false, + ]), + new IndexConstraint([ + 'name' => 'CN_constraints_2_multi', + 'columnNames' => ['C_index_2_1', 'C_index_2_2'], + 'isPrimary' => false, + 'isUnique' => true, + ]) + ]], + '2: check' => ['T_constraints_2', 'checks', []], + '2: default' => ['T_constraints_2', 'defaultValues', []], + + '3: primary key' => ['T_constraints_3', 'primaryKey', null], + '3: foreign key' => ['T_constraints_3', 'foreignKeys', [ + new ForeignKeyConstraint([ + 'name' => 'CN_constraints_3', + 'columnNames' => ['C_fk_id_1', 'C_fk_id_2'], + 'foreignTableName' => 'T_constraints_2', + 'foreignColumnNames' => ['C_id_1', 'C_id_2'], + 'onDelete' => 'CASCADE', + 'onUpdate' => 'CASCADE', + ]) + ]], + '3: unique' => ['T_constraints_3', 'uniques', []], + '3: index' => ['T_constraints_3', 'indexes', [ + new IndexConstraint([ + 'name' => 'CN_constraints_3', + 'columnNames' => ['C_fk_id_1', 'C_fk_id_2'], + 'isUnique' => false, + 'isPrimary' => false, + ]), + ]], + '3: check' => ['T_constraints_3', 'checks', []], + '3: default' => ['T_constraints_3', 'defaultValues', []], + + '4: primary key' => ['T_constraints_4', 'primaryKey', new Constraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_id'], + ])], + '4: unique' => ['T_constraints_4', 'uniques', [ + new Constraint([ + 'name' => 'CN_constraints_4', + 'columnNames' => ['C_col_1', 'C_col_2'], + ]) + ]], + '4: check' => ['T_constraints_4', 'checks', []], + '4: default' => ['T_constraints_4', 'defaultValues', []], + ]; + } + + /** + * @dataProvider constraintsProvider + * @depends testContraintTablesExistance + */ + public function testTableSchemaConstraints($tableName, $type, $expected) + { + $constraints = $this->getConnection(false)->getSchema()->{'getTable' . ucfirst($type)}($tableName); + $this->assertMetadataEquals($expected, $constraints); + } + + /** + * @dataProvider constraintsProvider + * @depends testTableSchemaConstraints + */ + public function testTableSchemaConstraintsWithPdoUpperCase($tableName, $type, $expected) + { + $connection = $this->getConnection(false); + $connection->getSlavePdo()->setAttribute(PDO::ATTR_CASE, PDO::CASE_UPPER); + $constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true); + $this->assertMetadataEquals($expected, $constraints); + } + + /** + * @dataProvider constraintsProvider + * @depends testTableSchemaConstraints + */ + public function testTableSchemaConstraintsWithPdoLowerCase($tableName, $type, $expected) + { + $connection = $this->getConnection(false); + $connection->getSlavePdo()->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); + $constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true); + $this->assertMetadataEquals($expected, $constraints); + } + + private function assertMetadataEquals($expected, $actual) + { + $this->assertInternalType(strtolower(gettype($expected)), $actual); + if (is_array($expected)) { + $this->normalizeArrayKeys($expected, false); + $this->normalizeArrayKeys($actual, false); + } + $this->normalizeConstraints($expected, $actual); + if (is_array($expected)) { + $this->normalizeArrayKeys($expected, true); + $this->normalizeArrayKeys($actual, true); + } + $this->assertEquals($expected, $actual); + } + + private function normalizeArrayKeys(array &$array, $caseSensitive) + { + $newArray = []; + foreach ($array as $value) { + if ($value instanceof Constraint) { + $key = (array) $value; + unset($key['name']); + foreach ($key as $keyName => $keyValue) { + if ($keyValue instanceof AnyCaseValue) { + $key[$keyName] = $keyValue->value; + } elseif ($keyValue instanceof AnyValue) { + $key[$keyName] = '[AnyValue]'; + } + } + ksort($key, SORT_STRING); + $newArray[$caseSensitive ? json_encode($key) : strtolower(json_encode($key))] = $value; + } else { + $newArray[] = $value; + } + } + ksort($newArray, SORT_STRING); + $array = $newArray; + } + + private function normalizeConstraints(&$expected, &$actual) + { + if (is_array($expected)) { + foreach ($expected as $key => $value) { + if (!$value instanceof Constraint || !isset($actual[$key]) || !$actual[$key] instanceof Constraint) { + continue; + } + + $this->normalizeConstraintPair($value, $actual[$key]); + } + } elseif ($expected instanceof Constraint && $actual instanceof Constraint) { + $this->normalizeConstraintPair($expected, $actual); + } + } + + private function normalizeConstraintPair(Constraint $expectedConstraint, Constraint $actualConstraint) + { + if ($expectedConstraint::className() !== $actualConstraint::className()) { + return; + } + + foreach (array_keys((array) $expectedConstraint) as $name) { + if ($expectedConstraint->$name instanceof AnyValue) { + $actualConstraint->$name = $expectedConstraint->$name; + } elseif ($expectedConstraint->$name instanceof AnyCaseValue) { + $actualConstraint->$name = new AnyCaseValue($actualConstraint->$name); + } + } + } } diff --git a/tests/framework/db/cubrid/QueryBuilderTest.php b/tests/framework/db/cubrid/QueryBuilderTest.php index 40cda79..91b1274 100644 --- a/tests/framework/db/cubrid/QueryBuilderTest.php +++ b/tests/framework/db/cubrid/QueryBuilderTest.php @@ -29,6 +29,16 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest return array_merge(parent::columnTypes(), []); } + public function checksProvider() + { + $this->markTestSkipped('Adding/dropping check constraints is not supported in CUBRID.'); + } + + public function defaultValuesProvider() + { + $this->markTestSkipped('Adding/dropping default constraints is not supported in CUBRID.'); + } + public function testResetSequence() { $qb = $this->getQueryBuilder(); diff --git a/tests/framework/db/cubrid/SchemaTest.php b/tests/framework/db/cubrid/SchemaTest.php index 712f7a8..507e7d3 100644 --- a/tests/framework/db/cubrid/SchemaTest.php +++ b/tests/framework/db/cubrid/SchemaTest.php @@ -3,6 +3,7 @@ namespace yiiunit\framework\db\cubrid; use yii\db\Expression; +use yiiunit\framework\db\AnyCaseValue; /** * @group db @@ -26,7 +27,6 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest [$fp = fopen(__FILE__, 'rb'), \PDO::PARAM_LOB], ]; - /* @var $schema Schema */ $schema = $this->getConnection()->schema; foreach ($values as $value) { @@ -70,4 +70,64 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest $columns['ts_default']['defaultValue'] = new Expression('SYS_TIMESTAMP'); return $columns; } + + public function constraintsProvider() + { + $result = parent::constraintsProvider(); + foreach ($result as $name => $constraints) { + $result[$name][2] = $this->convertPropertiesToAnycase($constraints[2]); + } + $result['1: check'][2] = []; + unset($result['1: index'][2][0]); + + unset($result['2: index'][2][0]); + + $result['3: foreign key'][2][0]->onDelete = 'RESTRICT'; + $result['3: foreign key'][2][0]->onUpdate = 'RESTRICT'; + $result['3: index'][2] = []; + return $result; + } + + /** + * @dataProvider constraintsProvider + * @depends testTableSchemaConstraints + */ + public function testTableSchemaConstraintsWithPdoUpperCase($tableName, $type, $expected) + { + $this->markTestSkipped('This test hangs on CUBRID.'); + } + + /** + * @dataProvider constraintsProvider + * @depends testTableSchemaConstraints + */ + public function testTableSchemaConstraintsWithPdoLowerCase($tableName, $type, $expected) + { + $this->markTestSkipped('This test hangs on CUBRID.'); + } + + /** + * @param array|object|string $object + * @param bool $isProperty + * @return array|object|string + */ + private function convertPropertiesToAnycase($object, $isProperty = false) + { + if (!$isProperty && is_array($object)) { + $result = []; + foreach ($object as $name => $value) { + $result[] = $this->convertPropertiesToAnycase($value); + } + return $result; + } + + if (is_object($object)) { + foreach (array_keys((array) $object) as $name) { + $object->$name = $this->convertPropertiesToAnycase($object->$name, true); + } + } elseif (is_array($object) || is_string($object)) { + $object = new AnyCaseValue($object); + } + return $object; + } } diff --git a/tests/framework/db/mssql/SchemaTest.php b/tests/framework/db/mssql/SchemaTest.php index 83b192a..617fedf 100644 --- a/tests/framework/db/mssql/SchemaTest.php +++ b/tests/framework/db/mssql/SchemaTest.php @@ -2,6 +2,9 @@ namespace yiiunit\framework\db\mssql; +use yii\db\DefaultConstraint; +use yiiunit\framework\db\AnyValue; + /** * @group db * @group mssql @@ -9,4 +12,19 @@ namespace yiiunit\framework\db\mssql; class SchemaTest extends \yiiunit\framework\db\SchemaTest { public $driverName = 'sqlsrv'; + + public function constraintsProvider() + { + $result = parent::constraintsProvider(); + $result['1: check'][2][0]->expression = '([C_check]<>\'\')'; + $result['1: default'][2][] = new DefaultConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_default'], + 'value' => '((0))', + ]); + + $result['3: foreign key'][2][0]->foreignSchemaName = 'dbo'; + $result['3: index'][2] = []; + return $result; + } } diff --git a/tests/framework/db/mysql/QueryBuilderTest.php b/tests/framework/db/mysql/QueryBuilderTest.php index 57c4244..528b702 100644 --- a/tests/framework/db/mysql/QueryBuilderTest.php +++ b/tests/framework/db/mysql/QueryBuilderTest.php @@ -57,6 +57,42 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest ]); } + public function primaryKeysProvider() + { + $result = parent::primaryKeysProvider(); + $result['drop'][0] = 'ALTER TABLE {{T_constraints_1}} DROP PRIMARY KEY'; + $result['add'][0] = 'ALTER TABLE {{T_constraints_1}} ADD CONSTRAINT [[CN_pk]] PRIMARY KEY ([[C_id_1]])'; + $result['add (2 columns)'][0] = 'ALTER TABLE {{T_constraints_1}} ADD CONSTRAINT [[CN_pk]] PRIMARY KEY ([[C_id_1]], [[C_id_2]])'; + return $result; + } + + public function foreignKeysProvider() + { + $result = parent::foreignKeysProvider(); + $result['drop'][0] = 'ALTER TABLE {{T_constraints_3}} DROP FOREIGN KEY [[CN_constraints_3]]'; + return $result; + } + + public function indexesProvider() + { + $result = parent::indexesProvider(); + $result['create'][0] = 'ALTER TABLE {{T_constraints_2}} ADD INDEX [[CN_constraints_2_single]] ([[C_index_1]])'; + $result['create (2 columns)'][0] = 'ALTER TABLE {{T_constraints_2}} ADD INDEX [[CN_constraints_2_multi]] ([[C_index_2_1]], [[C_index_2_2]])'; + $result['create unique'][0] = 'ALTER TABLE {{T_constraints_2}} ADD UNIQUE INDEX [[CN_constraints_2_single]] ([[C_index_1]])'; + $result['create unique (2 columns)'][0] = 'ALTER TABLE {{T_constraints_2}} ADD UNIQUE INDEX [[CN_constraints_2_multi]] ([[C_index_2_1]], [[C_index_2_2]])'; + return $result; + } + + public function checksProvider() + { + $this->markTestSkipped('Adding/dropping check constraints is not supported in MySQL.'); + } + + public function defaultValuesProvider() + { + $this->markTestSkipped('Adding/dropping default constraints is not supported in MySQL.'); + } + public function testResetSequence() { $qb = $this->getQueryBuilder(); diff --git a/tests/framework/db/mysql/SchemaTest.php b/tests/framework/db/mysql/SchemaTest.php index b103632..a6b61d0 100644 --- a/tests/framework/db/mysql/SchemaTest.php +++ b/tests/framework/db/mysql/SchemaTest.php @@ -2,6 +2,8 @@ namespace yiiunit\framework\db\mysql; +use yiiunit\framework\db\AnyCaseValue; + /** * @group db * @group mysql @@ -9,4 +11,16 @@ namespace yiiunit\framework\db\mysql; class SchemaTest extends \yiiunit\framework\db\SchemaTest { public $driverName = 'mysql'; + + public function constraintsProvider() + { + $result = parent::constraintsProvider(); + $result['1: check'][2] = []; + + $result['2: primary key'][2]->name = null; + + // Work aroung bug in MySQL 5.1 - it creates only this table in lowercase. O_o + $result['3: foreign key'][2][0]->foreignTableName = new AnyCaseValue('T_constraints_2'); + return $result; + } } diff --git a/tests/framework/db/oci/QueryBuilderTest.php b/tests/framework/db/oci/QueryBuilderTest.php index aec87da..a70d17d 100644 --- a/tests/framework/db/oci/QueryBuilderTest.php +++ b/tests/framework/db/oci/QueryBuilderTest.php @@ -2,6 +2,7 @@ namespace yiiunit\framework\db\oci; +use yii\db\oci\QueryBuilder; use yii\db\oci\Schema; use yiiunit\data\base\TraversableObject; @@ -35,6 +36,44 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest ]); } + public function foreignKeysProvider() + { + $tableName = 'T_constraints_3'; + $name = 'CN_constraints_3'; + $pkTableName = 'T_constraints_2'; + return [ + 'drop' => [ + "ALTER TABLE {{{$tableName}}} DROP CONSTRAINT [[$name]]", + function (QueryBuilder $qb) use ($tableName, $name) { + return $qb->dropForeignKey($name, $tableName); + } + ], + 'add' => [ + "ALTER TABLE {{{$tableName}}} ADD CONSTRAINT [[$name]] FOREIGN KEY ([[C_fk_id_1]]) REFERENCES {{{$pkTableName}}} ([[C_id_1]]) ON DELETE CASCADE", + function (QueryBuilder $qb) use ($tableName, $name, $pkTableName) { + return $qb->addForeignKey($name, $tableName, 'C_fk_id_1', $pkTableName, 'C_id_1', 'CASCADE'); + } + ], + 'add (2 columns)' => [ + "ALTER TABLE {{{$tableName}}} ADD CONSTRAINT [[$name]] FOREIGN KEY ([[C_fk_id_1]], [[C_fk_id_2]]) REFERENCES {{{$pkTableName}}} ([[C_id_1]], [[C_id_2]]) ON DELETE CASCADE", + function (QueryBuilder $qb) use ($tableName, $name, $pkTableName) { + return $qb->addForeignKey($name, $tableName, 'C_fk_id_1, C_fk_id_2', $pkTableName, 'C_id_1, C_id_2', 'CASCADE'); + } + ], + ]; + } + + public function indexesProvider() + { + $result = parent::indexesProvider(); + $result['drop'][0] = 'DROP INDEX [[CN_constraints_2_single]]'; + return $result; + } + + public function testAddDropDefaultValue($sql, \Closure $builder) + { + $this->markTestSkipped('Adding/dropping default constraints is not supported in Oracle.'); + } public function testCommentColumn() { diff --git a/tests/framework/db/oci/SchemaTest.php b/tests/framework/db/oci/SchemaTest.php index 8198230..3b5a1c2 100644 --- a/tests/framework/db/oci/SchemaTest.php +++ b/tests/framework/db/oci/SchemaTest.php @@ -2,8 +2,8 @@ namespace yiiunit\framework\db\oci; -use yii\db\Expression; -use yii\db\oci\Schema; +use yii\db\CheckConstraint; +use yiiunit\framework\db\AnyValue; /** * @group db @@ -93,4 +93,72 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest $table = $this->getConnection(false)->schema->getTableSchema('order', true); $this->assertFalse($table->columns['id']->autoIncrement); } + + public function constraintsProvider() + { + $result = parent::constraintsProvider(); + $result['1: check'][2][0]->expression = '"C_check" <> \'\''; + $result['1: check'][2][] = new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_id'], + 'expression' => '"C_id" IS NOT NULL', + ]); + $result['1: check'][2][] = new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_not_null'], + 'expression' => '"C_not_null" IS NOT NULL', + ]); + $result['1: check'][2][] = new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_unique'], + 'expression' => '"C_unique" IS NOT NULL', + ]); + $result['1: check'][2][] = new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_default'], + 'expression' => '"C_default" IS NOT NULL', + ]); + + $result['2: check'][2][] = new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_id_1'], + 'expression' => '"C_id_1" IS NOT NULL', + ]); + $result['2: check'][2][] = new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_id_2'], + 'expression' => '"C_id_2" IS NOT NULL', + ]); + + $result['3: foreign key'][2][0]->foreignSchemaName = 'SYSTEM'; + $result['3: foreign key'][2][0]->onUpdate = null; + $result['3: index'][2] = []; + $result['3: check'][2][] = new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_fk_id_1'], + 'expression' => '"C_fk_id_1" IS NOT NULL', + ]); + $result['3: check'][2][] = new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_fk_id_2'], + 'expression' => '"C_fk_id_2" IS NOT NULL', + ]); + $result['3: check'][2][] = new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_id'], + 'expression' => '"C_id" IS NOT NULL', + ]); + + $result['4: check'][2][] = new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_id'], + 'expression' => '"C_id" IS NOT NULL', + ]); + $result['4: check'][2][] = new CheckConstraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['C_col_2'], + 'expression' => '"C_col_2" IS NOT NULL', + ]); + return $result; + } } diff --git a/tests/framework/db/pgsql/QueryBuilderTest.php b/tests/framework/db/pgsql/QueryBuilderTest.php index 508734f..7dcb0bf 100644 --- a/tests/framework/db/pgsql/QueryBuilderTest.php +++ b/tests/framework/db/pgsql/QueryBuilderTest.php @@ -95,6 +95,18 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest $this->assertEquals($expected, $sql); } + public function indexesProvider() + { + $result = parent::indexesProvider(); + $result['drop'][0] = 'DROP INDEX [[CN_constraints_2_single]]'; + return $result; + } + + public function defaultValuesProvider() + { + $this->markTestSkipped('Adding/dropping default constraints is not supported in PostgreSQL.'); + } + public function testCommentColumn() { $qb = $this->getQueryBuilder(); diff --git a/tests/framework/db/pgsql/SchemaTest.php b/tests/framework/db/pgsql/SchemaTest.php index f4451dd..a696a93 100644 --- a/tests/framework/db/pgsql/SchemaTest.php +++ b/tests/framework/db/pgsql/SchemaTest.php @@ -3,7 +3,6 @@ namespace yiiunit\framework\db\pgsql; use yii\db\Expression; -use yii\db\pgsql\Schema; use yiiunit\data\ar\ActiveRecord; use yiiunit\data\ar\Type; @@ -83,7 +82,6 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest public function testCompositeFk() { - /* @var $schema Schema */ $schema = $this->getConnection()->schema; $table = $schema->getTableSchema('composite_fk'); @@ -109,7 +107,6 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest [$fp = fopen(__FILE__, 'rb'), \PDO::PARAM_LOB], ]; - /* @var $schema Schema */ $schema = $this->getConnection()->schema; foreach ($values as $value) { @@ -120,7 +117,6 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest public function testBooleanDefaultValues() { - /* @var $schema Schema */ $schema = $this->getConnection()->schema; $table = $schema->getTableSchema('bool_values'); @@ -194,4 +190,14 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest $this->assertEquals('numeric', $column->dbType); $this->assertEquals(0, $column->defaultValue); } + + public function constraintsProvider() + { + $result = parent::constraintsProvider(); + $result['1: check'][2][0]->expression = '(("C_check")::text <> \'\'::text)'; + + $result['3: foreign key'][2][0]->foreignSchemaName = 'public'; + $result['3: index'][2] = []; + return $result; + } } diff --git a/tests/framework/db/sqlite/QueryBuilderTest.php b/tests/framework/db/sqlite/QueryBuilderTest.php index 28446b8..4d2c6f2 100644 --- a/tests/framework/db/sqlite/QueryBuilderTest.php +++ b/tests/framework/db/sqlite/QueryBuilderTest.php @@ -46,9 +46,36 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest ]); } - public function testAddDropPrimaryKey() + public function primaryKeysProvider() { - $this->markTestSkipped('Comments are not supported in SQLite'); + $this->markTestSkipped('Adding/dropping primary keys is not supported in SQLite.'); + } + + public function foreignKeysProvider() + { + $this->markTestSkipped('Adding/dropping foreign keys is not supported in SQLite.'); + } + + public function indexesProvider() + { + $result = parent::indexesProvider(); + $result['drop'][0] = 'DROP INDEX [[CN_constraints_2_single]]'; + return $result; + } + + public function uniquesProvider() + { + $this->markTestSkipped('Adding/dropping unique constraints is not supported in SQLite.'); + } + + public function checksProvider() + { + $this->markTestSkipped('Adding/dropping check constraints is not supported in SQLite.'); + } + + public function defaultValuesProvider() + { + $this->markTestSkipped('Adding/dropping default constraints is not supported in SQLite.'); } public function testCommentColumn() diff --git a/tests/framework/db/sqlite/SchemaTest.php b/tests/framework/db/sqlite/SchemaTest.php index 425fd66..2b625c4 100644 --- a/tests/framework/db/sqlite/SchemaTest.php +++ b/tests/framework/db/sqlite/SchemaTest.php @@ -2,6 +2,8 @@ namespace yiiunit\framework\db\sqlite; +use yiiunit\framework\db\AnyValue; + /** * @group db * @group sqlite @@ -31,7 +33,6 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest public function testCompositeFk() { - /* @var $schema Schema */ $schema = $this->getConnection()->schema; $table = $schema->getTableSchema('composite_fk'); @@ -42,4 +43,25 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest $this->assertEquals('order_id', $table->foreignKeys[0]['order_id']); $this->assertEquals('item_id', $table->foreignKeys[0]['item_id']); } + + public function constraintsProvider() + { + $result = parent::constraintsProvider(); + $result['1: primary key'][2]->name = null; + $result['1: check'][2][0]->columnNames = null; + $result['1: check'][2][0]->expression = '"C_check" <> \'\''; + $result['1: unique'][2][0]->name = AnyValue::getInstance(); + $result['1: index'][2][1]->name = AnyValue::getInstance(); + + $result['2: primary key'][2]->name = null; + $result['2: unique'][2][0]->name = AnyValue::getInstance(); + $result['2: index'][2][2]->name = AnyValue::getInstance(); + + $result['3: foreign key'][2][0]->name = null; + $result['3: index'][2] = []; + + $result['4: primary key'][2]->name = null; + $result['4: unique'][2][0]->name = AnyValue::getInstance(); + return $result; + } } diff --git a/tests/framework/db/sqlite/SqlTokenizerTest.php b/tests/framework/db/sqlite/SqlTokenizerTest.php new file mode 100644 index 0000000..93376b2 --- /dev/null +++ b/tests/framework/db/sqlite/SqlTokenizerTest.php @@ -0,0 +1,1138 @@ + [ + << '''' and not(chktest=='foo')), + -- CONSTRAINT `ch` CHECK -- (`col_1` <> 0 and col_2 <> 1)) +CONSTRAINT `ch2` CHECK -- (`col_1` <> 0 and col_2 <> -1)) +(`col_1` <> 41 and not (col_2 == 'ั‚ะตั''ั‚'))); +CREATE TABLE t300(id INTEGER PRIMARY KEY); +CREATE TABLE t301( + id INTEGER PRIMARY KEY, + c1 INTEGER NOT NULL, + c2 INTEGER NOT NULL, + c3 BOOLEAN NOT NULL DEFAULT 0, + FOREIGN KEY(c1) REFERENCES t300(id) ON DELETE CASCADE ON UPDATE RESTRICT + /* no comma */ + FOREIGN KEY(c2) REFERENCES t300(id) ON DELETE CASCADE ON UPDATE RESTRICT + /* no comma */ + UNIQUE(c1, c2) +); +PRAGMA foreign_key_list(t301); +SELECT*from/*foo*/`T_constraints_1`WHERE not`C_check`='foo''bar'--bar +;;;;;;;;;/* +SQL +, + new SqlToken([ + 'type' => SqlToken::TYPE_CODE, + 'content' => 'CREATE TABLE `constraints_test_1` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `col_1` integer, + `col_2` integer NOT NULL, `chktest` text NOT NULL DEFAULT \'none\' CHECK([chktest] <> \'\'\'\' and not(chktest==\'foo\')), + -- CONSTRAINT `ch` CHECK -- (`col_1` <> 0 and col_2 <> 1)) +CONSTRAINT `ch2` CHECK -- (`col_1` <> 0 and col_2 <> -1)) +(`col_1` <> 41 and not (col_2 == \'ั‚ะตั\'\'ั‚\'))); +CREATE TABLE t300(id INTEGER PRIMARY KEY); +CREATE TABLE t301( + id INTEGER PRIMARY KEY, + c1 INTEGER NOT NULL, + c2 INTEGER NOT NULL, + c3 BOOLEAN NOT NULL DEFAULT 0, + FOREIGN KEY(c1) REFERENCES t300(id) ON DELETE CASCADE ON UPDATE RESTRICT + /* no comma */ + FOREIGN KEY(c2) REFERENCES t300(id) ON DELETE CASCADE ON UPDATE RESTRICT + /* no comma */ + UNIQUE(c1, c2) +); +PRAGMA foreign_key_list(t301); +SELECT*from/*foo*/`T_constraints_1`WHERE not`C_check`=\'foo\'\'bar\'--bar +;;;;;;;;;/*', + 'startOffset' => 0, + 'endOffset' => 875, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_STATEMENT, + 'content' => null, + 'startOffset' => 0, + 'endOffset' => 383, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CREATE', + 'startOffset' => 0, + 'endOffset' => 6, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'TABLE', + 'startOffset' => 7, + 'endOffset' => 12, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'constraints_test_1', + 'startOffset' => 13, + 'endOffset' => 33, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 34, + 'endOffset' => 35, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 37, + 'endOffset' => 381, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'id', + 'startOffset' => 37, + 'endOffset' => 41, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'integer', + 'startOffset' => 42, + 'endOffset' => 49, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'PRIMARY', + 'startOffset' => 50, + 'endOffset' => 57, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'KEY', + 'startOffset' => 58, + 'endOffset' => 61, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'AUTOINCREMENT', + 'startOffset' => 62, + 'endOffset' => 75, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 76, + 'endOffset' => 79, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 80, + 'endOffset' => 84, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 84, + 'endOffset' => 85, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'col_1', + 'startOffset' => 87, + 'endOffset' => 94, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'integer', + 'startOffset' => 95, + 'endOffset' => 102, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 102, + 'endOffset' => 103, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'col_2', + 'startOffset' => 105, + 'endOffset' => 112, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'integer', + 'startOffset' => 113, + 'endOffset' => 120, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 121, + 'endOffset' => 124, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 125, + 'endOffset' => 129, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 129, + 'endOffset' => 130, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'chktest', + 'startOffset' => 131, + 'endOffset' => 140, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'text', + 'startOffset' => 141, + 'endOffset' => 145, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 146, + 'endOffset' => 149, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 150, + 'endOffset' => 154, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'DEFAULT', + 'startOffset' => 155, + 'endOffset' => 162, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STRING_LITERAL, + 'content' => 'none', + 'startOffset' => 163, + 'endOffset' => 169, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CHECK', + 'startOffset' => 170, + 'endOffset' => 175, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 175, + 'endOffset' => 176, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 176, + 'endOffset' => 217, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'chktest', + 'startOffset' => 176, + 'endOffset' => 185, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '<>', + 'startOffset' => 186, + 'endOffset' => 188, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STRING_LITERAL, + 'content' => '\'', + 'startOffset' => 189, + 'endOffset' => 193, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'AND', + 'startOffset' => 194, + 'endOffset' => 197, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 198, + 'endOffset' => 201, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 201, + 'endOffset' => 202, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 202, + 'endOffset' => 216, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'chktest', + 'startOffset' => 202, + 'endOffset' => 209, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '==', + 'startOffset' => 209, + 'endOffset' => 211, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STRING_LITERAL, + 'content' => 'foo', + 'startOffset' => 211, + 'endOffset' => 216, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 216, + 'endOffset' => 217, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 217, + 'endOffset' => 218, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 218, + 'endOffset' => 219, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CONSTRAINT', + 'startOffset' => 280, + 'endOffset' => 290, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'ch2', + 'startOffset' => 291, + 'endOffset' => 296, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CHECK', + 'startOffset' => 297, + 'endOffset' => 302, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 338, + 'endOffset' => 339, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 339, + 'endOffset' => 380, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'col_1', + 'startOffset' => 339, + 'endOffset' => 346, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '<>', + 'startOffset' => 347, + 'endOffset' => 349, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => '41', + 'startOffset' => 350, + 'endOffset' => 352, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'AND', + 'startOffset' => 353, + 'endOffset' => 356, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 357, + 'endOffset' => 360, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 361, + 'endOffset' => 362, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 362, + 'endOffset' => 379, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'col_2', + 'startOffset' => 362, + 'endOffset' => 367, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '==', + 'startOffset' => 368, + 'endOffset' => 370, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STRING_LITERAL, + 'content' => 'ั‚ะตั\'ั‚', + 'startOffset' => 371, + 'endOffset' => 379, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 379, + 'endOffset' => 380, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 380, + 'endOffset' => 381, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 381, + 'endOffset' => 382, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ';', + 'startOffset' => 382, + 'endOffset' => 383, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STATEMENT, + 'content' => null, + 'startOffset' => 384, + 'endOffset' => 426, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CREATE', + 'startOffset' => 384, + 'endOffset' => 390, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'TABLE', + 'startOffset' => 391, + 'endOffset' => 396, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 't300', + 'startOffset' => 397, + 'endOffset' => 401, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 401, + 'endOffset' => 402, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 402, + 'endOffset' => 424, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'id', + 'startOffset' => 402, + 'endOffset' => 404, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'INTEGER', + 'startOffset' => 405, + 'endOffset' => 412, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'PRIMARY', + 'startOffset' => 413, + 'endOffset' => 420, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'KEY', + 'startOffset' => 421, + 'endOffset' => 424, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 424, + 'endOffset' => 425, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ';', + 'startOffset' => 425, + 'endOffset' => 426, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STATEMENT, + 'content' => null, + 'startOffset' => 427, + 'endOffset' => 772, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CREATE', + 'startOffset' => 427, + 'endOffset' => 433, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'TABLE', + 'startOffset' => 434, + 'endOffset' => 439, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 't301', + 'startOffset' => 440, + 'endOffset' => 444, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 444, + 'endOffset' => 445, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 450, + 'endOffset' => 769, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'id', + 'startOffset' => 450, + 'endOffset' => 452, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'INTEGER', + 'startOffset' => 453, + 'endOffset' => 460, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'PRIMARY', + 'startOffset' => 461, + 'endOffset' => 468, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'KEY', + 'startOffset' => 469, + 'endOffset' => 472, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 472, + 'endOffset' => 473, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c1', + 'startOffset' => 478, + 'endOffset' => 480, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'INTEGER', + 'startOffset' => 481, + 'endOffset' => 488, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 489, + 'endOffset' => 492, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 493, + 'endOffset' => 497, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 497, + 'endOffset' => 498, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c2', + 'startOffset' => 503, + 'endOffset' => 505, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'INTEGER', + 'startOffset' => 506, + 'endOffset' => 513, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 514, + 'endOffset' => 517, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 518, + 'endOffset' => 522, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 522, + 'endOffset' => 523, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c3', + 'startOffset' => 528, + 'endOffset' => 530, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'BOOLEAN', + 'startOffset' => 531, + 'endOffset' => 538, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 539, + 'endOffset' => 542, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 543, + 'endOffset' => 547, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'DEFAULT', + 'startOffset' => 548, + 'endOffset' => 555, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => '0', + 'startOffset' => 556, + 'endOffset' => 557, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 557, + 'endOffset' => 558, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'FOREIGN', + 'startOffset' => 563, + 'endOffset' => 570, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'KEY', + 'startOffset' => 571, + 'endOffset' => 574, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 574, + 'endOffset' => 575, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 575, + 'endOffset' => 577, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c1', + 'startOffset' => 575, + 'endOffset' => 577, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 577, + 'endOffset' => 578, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'REFERENCES', + 'startOffset' => 579, + 'endOffset' => 589, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 't300', + 'startOffset' => 590, + 'endOffset' => 594, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 594, + 'endOffset' => 595, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 595, + 'endOffset' => 597, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'id', + 'startOffset' => 595, + 'endOffset' => 597, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 597, + 'endOffset' => 598, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'ON', + 'startOffset' => 599, + 'endOffset' => 601, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'DELETE', + 'startOffset' => 602, + 'endOffset' => 608, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CASCADE', + 'startOffset' => 609, + 'endOffset' => 616, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'ON', + 'startOffset' => 617, + 'endOffset' => 619, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'UPDATE', + 'startOffset' => 620, + 'endOffset' => 626, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'RESTRICT', + 'startOffset' => 627, + 'endOffset' => 635, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'FOREIGN', + 'startOffset' => 659, + 'endOffset' => 666, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'KEY', + 'startOffset' => 667, + 'endOffset' => 670, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 670, + 'endOffset' => 671, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 671, + 'endOffset' => 673, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c2', + 'startOffset' => 671, + 'endOffset' => 673, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 673, + 'endOffset' => 674, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'REFERENCES', + 'startOffset' => 675, + 'endOffset' => 685, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 't300', + 'startOffset' => 686, + 'endOffset' => 690, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 690, + 'endOffset' => 691, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 691, + 'endOffset' => 693, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'id', + 'startOffset' => 691, + 'endOffset' => 693, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 693, + 'endOffset' => 694, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'ON', + 'startOffset' => 695, + 'endOffset' => 697, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'DELETE', + 'startOffset' => 698, + 'endOffset' => 704, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CASCADE', + 'startOffset' => 705, + 'endOffset' => 712, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'ON', + 'startOffset' => 713, + 'endOffset' => 715, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'UPDATE', + 'startOffset' => 716, + 'endOffset' => 722, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'RESTRICT', + 'startOffset' => 723, + 'endOffset' => 731, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'UNIQUE', + 'startOffset' => 755, + 'endOffset' => 761, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 761, + 'endOffset' => 762, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 762, + 'endOffset' => 768, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c1', + 'startOffset' => 762, + 'endOffset' => 764, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 764, + 'endOffset' => 765, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c2', + 'startOffset' => 766, + 'endOffset' => 768, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 768, + 'endOffset' => 769, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 770, + 'endOffset' => 771, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ';', + 'startOffset' => 771, + 'endOffset' => 772, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STATEMENT, + 'content' => null, + 'startOffset' => 773, + 'endOffset' => 803, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'PRAGMA', + 'startOffset' => 773, + 'endOffset' => 779, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'foreign_key_list', + 'startOffset' => 780, + 'endOffset' => 796, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 796, + 'endOffset' => 797, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 797, + 'endOffset' => 801, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 't301', + 'startOffset' => 797, + 'endOffset' => 801, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 801, + 'endOffset' => 802, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ';', + 'startOffset' => 802, + 'endOffset' => 803, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STATEMENT, + 'content' => null, + 'startOffset' => 804, + 'endOffset' => 875, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'SELECT', + 'startOffset' => 804, + 'endOffset' => 810, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '*', + 'startOffset' => 810, + 'endOffset' => 811, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'FROM', + 'startOffset' => 811, + 'endOffset' => 815, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'T_constraints_1', + 'startOffset' => 822, + 'endOffset' => 839, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'WHERE', + 'startOffset' => 839, + 'endOffset' => 844, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 845, + 'endOffset' => 848, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'C_check', + 'startOffset' => 848, + 'endOffset' => 857, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '=', + 'startOffset' => 857, + 'endOffset' => 858, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STRING_LITERAL, + 'content' => 'foo\'bar', + 'startOffset' => 858, + 'endOffset' => 868, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ';', + 'startOffset' => 874, + 'endOffset' => 875, + ]), + ], + ]), + ], + ]), + ] + ]; + } + + /** + * @dataProvider sqlProvider + */ + public function testTokenizer($sql, SqlToken $expectedToken) + { + $actualToken = (new SqlTokenizer($sql))->tokenize(); + $this->assertEquals($expectedToken, $actualToken); + } + + /** + * Use this to export SqlToken for tests. + * @param SqlToken $token + * @return array + */ + private function exportToken(SqlToken $token) + { + $result = get_object_vars($token); + unset($result['parent']); + $result['children'] = array_map(function (SqlToken $token) { + return $this->exportToken($token); + }, $token->children); + return $result; + } +}