Browse Source

Merge PR #14015 branch 'db-constraints' from sergeymakinen/yii2

Implement retrieving DBMS constraints

* db-constraints2: (21 commits)
  CHANGELOG for #14105
  added missing abstract methods to ConstraintFinderTrait
  avoid dependency of SqlTokenizer on sqlite implementation
  Mention an usage magic in descriptions
  Fix an unknown variable usage bug
  updated phpdoc
  make schema cache version a constant
  Have I fixed these phpdocs? Let’s see…
  Fixed phpdoc [skip ci]
  Mark not supported constraint retrieving methods
  Add PHPDoc
  Fix typo [skip ci]
  Rename DefaultConstraint to DefaultValueConstraint
  Fix imports
  Fix merging issues & CS
  Add Command tests
  Fix constraint tests
  Disable column comment test on old CUBRID
  Fix dropping unique/indexes
  Fix schema caching on commands
  ...
tags/2.0.13
Carsten Brandt 7 years ago
parent
commit
e0dde88b87
  1. 2
      framework/CHANGELOG.md
  2. 22
      framework/db/CheckConstraint.php
  3. 107
      framework/db/Command.php
  4. 28
      framework/db/Constraint.php
  5. 236
      framework/db/ConstraintFinderTrait.php
  6. 22
      framework/db/DefaultValueConstraint.php
  7. 38
      framework/db/ForeignKeyConstraint.php
  8. 26
      framework/db/IndexConstraint.php
  9. 108
      framework/db/QueryBuilder.php
  10. 323
      framework/db/Schema.php
  11. 309
      framework/db/SqlToken.php
  12. 389
      framework/db/SqlTokenizer.php
  13. 36
      framework/db/cubrid/QueryBuilder.php
  14. 239
      framework/db/cubrid/Schema.php
  15. 19
      framework/db/mssql/QueryBuilder.php
  16. 327
      framework/db/mssql/Schema.php
  17. 27
      framework/db/mysql/QueryBuilder.php
  18. 262
      framework/db/mysql/Schema.php
  19. 351
      framework/db/oci/Schema.php
  20. 315
      framework/db/pgsql/Schema.php
  21. 54
      framework/db/sqlite/QueryBuilder.php
  22. 206
      framework/db/sqlite/Schema.php
  23. 288
      framework/db/sqlite/SqlTokenizer.php
  24. 43
      tests/data/cubrid.sql
  25. 43
      tests/data/mssql.sql
  26. 47
      tests/data/mysql.sql
  27. 43
      tests/data/oci.sql
  28. 44
      tests/data/postgres.sql
  29. 43
      tests/data/sqlite.sql
  30. 28
      tests/framework/db/AnyCaseValue.php
  31. 24
      tests/framework/db/AnyValue.php
  32. 157
      tests/framework/db/CommandTest.php
  33. 14
      tests/framework/db/CompareValue.php
  34. 226
      tests/framework/db/QueryBuilderTest.php
  35. 248
      tests/framework/db/SchemaTest.php
  36. 5
      tests/framework/db/cubrid/CommandTest.php
  37. 21
      tests/framework/db/cubrid/QueryBuilderTest.php
  38. 58
      tests/framework/db/cubrid/SchemaTest.php
  39. 23
      tests/framework/db/mssql/CommandTest.php
  40. 24
      tests/framework/db/mssql/SchemaTest.php
  41. 5
      tests/framework/db/mysql/CommandTest.php
  42. 43
      tests/framework/db/mysql/QueryBuilderTest.php
  43. 18
      tests/framework/db/mysql/SchemaTest.php
  44. 39
      tests/framework/db/oci/QueryBuilderTest.php
  45. 71
      tests/framework/db/oci/SchemaTest.php
  46. 12
      tests/framework/db/pgsql/QueryBuilderTest.php
  47. 14
      tests/framework/db/pgsql/SchemaTest.php
  48. 20
      tests/framework/db/sqlite/CommandTest.php
  49. 31
      tests/framework/db/sqlite/QueryBuilderTest.php
  50. 24
      tests/framework/db/sqlite/SchemaTest.php
  51. 1143
      tests/framework/db/sqlite/SqlTokenizerTest.php

2
framework/CHANGELOG.md

@ -25,6 +25,8 @@ Yii Framework 2 Change Log
- Bug #14341: Fixed regression in error handling introduced by fixing #14264 (samdark)
- Enh #13378: Added skipOnEmpty option to SluggableBehaviour (andrewnester)
- Enh #14389: Optimize `Validator::validateAttributes()` by calling `attributeNames()` only once (nicdnep)
- Enh #14105: Implemented a solution for retrieving DBMS constraints in `yii\db\Schema` (sergeymakinen)
2.0.12 June 05, 2017
--------------------

22
framework/db/CheckConstraint.php

@ -0,0 +1,22 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
/**
* CheckConstraint represents the metadata of a table `CHECK` constraint.
*
* @author Sergey Makinen <sergey@makinen.ru>
* @since 2.0.13
*/
class CheckConstraint extends Constraint
{
/**
* @var string the SQL of the `CHECK` constraint.
*/
public $expression;
}

107
framework/db/Command.php

@ -723,7 +723,7 @@ class Command extends Component
{
$sql = $this->db->getQueryBuilder()->createIndex($name, $table, $columns, $unique);
return $this->setSql($sql);
return $this->setSql($sql)->requireTableSchemaRefresh($table);
}
/**
@ -736,7 +736,110 @@ class Command extends Component
{
$sql = $this->db->getQueryBuilder()->dropIndex($name, $table);
return $this->setSql($sql);
return $this->setSql($sql)->requireTableSchemaRefresh($table);
}
/**
* 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)->requireTableSchemaRefresh($table);
}
/**
* 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)->requireTableSchemaRefresh($table);
}
/**
* 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 $expression the SQL of the `CHECK` constraint.
* @return $this the command object itself.
* @since 2.0.13
*/
public function addCheck($name, $table, $expression)
{
$sql = $this->db->getQueryBuilder()->addCheck($name, $table, $expression);
return $this->setSql($sql)->requireTableSchemaRefresh($table);
}
/**
* 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)->requireTableSchemaRefresh($table);
}
/**
* 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 $value default value.
* @return $this the command object itself.
* @since 2.0.13
*/
public function addDefaultValue($name, $table, $column, $value)
{
$sql = $this->db->getQueryBuilder()->addDefaultValue($name, $table, $column, $value);
return $this->setSql($sql)->requireTableSchemaRefresh($table);
}
/**
* 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)->requireTableSchemaRefresh($table);
}
/**

28
framework/db/Constraint.php

@ -0,0 +1,28 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
use yii\base\Object;
/**
* Constraint represents the metadata of a table constraint.
*
* @author Sergey Makinen <sergey@makinen.ru>
* @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;
}

236
framework/db/ConstraintFinderTrait.php

@ -0,0 +1,236 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
/**
* ConstraintFinderTrait provides methods for getting a table constraint information.
*
* @property CheckConstraint[][] $schemaChecks Check constraints for all tables in the database.
* Each array element is an array of [[CheckConstraint]] or its child classes. This property is read-only.
* @property DefaultValueConstraint[] $schemaDefaultValues Default value constraints for all tables in the database.
* Each array element is an array of [[DefaultValueConstraint]] or its child classes. This property is read-only.
* @property ForeignKeyConstraint[][] $schemaForeignKeys Foreign keys for all tables in the database. Each
* array element is an array of [[ForeignKeyConstraint]] or its child classes. This property is read-only.
* @property IndexConstraint[][] $schemaIndexes Indexes for all tables in the database. Each array element is
* an array of [[IndexConstraint]] or its child classes. This property is read-only.
* @property Constraint[] $schemaPrimaryKeys Primary keys for all tables in the database. Each array element
* is an instance of [[Constraint]] or its child class. This property is read-only.
* @property IndexConstraint[][] $schemaUniques Unique constraints for all tables in the database.
* Each array element is an array of [[IndexConstraint]] or its child classes. This property is read-only.
*
* @author Sergey Makinen <sergey@makinen.ru>
* @since 2.0.13
*/
trait ConstraintFinderTrait
{
/**
* 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.
*/
abstract protected function getTableMetadata($name, $type, $refresh);
/**
* 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.
*/
abstract protected function getSchemaMetadata($schema, $type, $refresh);
/**
* 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.
*/
abstract protected function loadTablePrimaryKey($tableName);
/**
* Loads all foreign keys for the given table.
* @param string $tableName table name.
* @return ForeignKeyConstraint[] foreign keys for the given table.
*/
abstract protected function loadTableForeignKeys($tableName);
/**
* Loads all indexes for the given table.
* @param string $tableName table name.
* @return IndexConstraint[] indexes for the given table.
*/
abstract protected function loadTableIndexes($tableName);
/**
* Loads all unique constraints for the given table.
* @param string $tableName table name.
* @return Constraint[] unique constraints for the given table.
*/
abstract protected function loadTableUniques($tableName);
/**
* Loads all check constraints for the given table.
* @param string $tableName table name.
* @return CheckConstraint[] check constraints for the given table.
*/
abstract protected function loadTableChecks($tableName);
/**
* Loads all default value constraints for the given table.
*
* @param string $tableName table name.
* @return DefaultValueConstraint[] default value constraints for the given table.
*/
abstract protected 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 DefaultValueConstraint[] 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 DefaultValueConstraint[] default value constraints for all tables in the database.
* Each array element is an array of [[DefaultValueConstraint]] or its child classes.
*/
public function getSchemaDefaultValues($schema = '', $refresh = false)
{
return $this->getSchemaMetadata($schema, 'defaultValues', $refresh);
}
}

22
framework/db/DefaultValueConstraint.php

@ -0,0 +1,22 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
/**
* DefaultValueConstraint represents the metadata of a table `DEFAULT` constraint.
*
* @author Sergey Makinen <sergey@makinen.ru>
* @since 2.0.13
*/
class DefaultValueConstraint extends Constraint
{
/**
* @var mixed default value as returned by the DBMS.
*/
public $value;
}

38
framework/db/ForeignKeyConstraint.php

@ -0,0 +1,38 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
/**
* ForeignKeyConstraint represents the metadata of a table `FOREIGN KEY` constraint.
*
* @author Sergey Makinen <sergey@makinen.ru>
* @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;
}

26
framework/db/IndexConstraint.php

@ -0,0 +1,26 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
/**
* IndexConstraint represents the metadata of a table `INDEX` constraint.
*
* @author Sergey Makinen <sergey@makinen.ru>
* @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;
}

108
framework/db/QueryBuilder.php

@ -451,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) . ')';
}
/**
@ -610,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 $expression 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, $expression)
{
return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
. $this->db->quoteColumnName($name) . ' CHECK (' . $this->db->quoteSql($expression) . ')';
}
/**
* 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 $value 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, $value)
{
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.

323
framework/db/Schema.php

@ -9,8 +9,10 @@ namespace yii\db;
use Yii;
use yii\base\InvalidCallException;
use yii\base\InvalidConfigException;
use yii\base\NotSupportedException;
use yii\base\Object;
use yii\caching\Cache;
use yii\caching\CacheInterface;
use yii\caching\TagDependency;
@ -33,6 +35,7 @@ use yii\caching\TagDependency;
* syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is write-only.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Sergey Makinen <sergey@makinen.ru>
* @since 2.0
*/
abstract class Schema extends Object
@ -60,6 +63,12 @@ abstract class Schema extends Object
const TYPE_MONEY = 'money';
/**
* Schema cache version, to detect incompatibilities in cached values when the
* data format of the cache changes.
*/
const SCHEMA_CACHE_VERSION = 1;
/**
* @var Connection the database connection
*/
public $db;
@ -89,9 +98,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 +108,83 @@ 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 CacheInterface */
$cache = is_string($db->schemaCache) ? Yii::$app->get($db->schemaCache, false) : $db->schemaCache;
if ($cache instanceof CacheInterface) {
$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);
/**
* Creates a column schema for the database.
* This method may be overridden by child classes to create a DBMS-specific column schema.
* @return ColumnSchema column schema instance.
* @throws InvalidConfigException if a column schema class cannot be created.
*/
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 +266,7 @@ abstract class Schema extends Object
TagDependency::invalidate($cache, $this->getCacheTag());
}
$this->_tableNames = [];
$this->_tables = [];
$this->_tableMetadata = [];
}
/**
@ -295,7 +278,7 @@ abstract class Schema extends Object
*/
public function refreshTableSchema($name)
{
unset($this->_tables[$name]);
unset($this->_tableMetadata[$name]);
$this->_tableNames = [];
/* @var $cache CacheInterface */
$cache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
@ -330,32 +313,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:
*
@ -645,4 +602,166 @@ 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.
* If there's no metadata in the cache, this method will call
* a `'loadTable' . ucfirst($type)` named method with the table name to obtain the metadata.
* @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 ($refresh || !isset($this->_tableMetadata[$name])) {
$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.
* This method will call a `'getTable' . ucfirst($type)` named method with the table name
* and the refresh flag to obtain the metadata.
* @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_map(function (array $row) {
return array_change_key_case($row, CASE_LOWER);
}, $row);
}
return array_change_key_case($row, CASE_LOWER);
}
/**
* 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'] !== static::SCHEMA_CACHE_VERSION) {
$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'] = static::SCHEMA_CACHE_VERSION;
$cache->set(
$this->getCacheKey($name),
$metadata,
$this->db->schemaCacheDuration,
new TagDependency(['tags' => $this->getCacheTag()])
);
}
}

309
framework/db/SqlToken.php

@ -0,0 +1,309 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
use yii\base\Object;
/**
* SqlToken represents SQL tokens produced by [[SqlTokenizer]] or its child classes.
*
* @property SqlToken[] $children Child tokens.
* @property bool $hasChildren Whether the token has children. This property is read-only.
* @property bool $isCollection Whether the token represents a collection of tokens. This property is
* read-only.
* @property string $sql SQL code. This property is read-only.
*
* @author Sergey Makinen <sergey@makinen.ru>
* @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[] token children.
*/
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');
}
/**
* Returns whether this token (including its children) matches the specified "pattern" SQL code.
*
* Usage Example:
*
* ```php
* $patternToken = (new \yii\db\sqlite\SqlTokenizer('SELECT any FROM any'))->tokenize();
* if ($sqlToken->matches($patternToken, 0, $firstMatchIndex, $lastMatchIndex)) {
* // ...
* }
* ```
*
* @param SqlToken $patternToken tokenized SQL code to match against. In addition to normal SQL, the
* `any` keyword is supported which will match any number of keywords, identifiers, whitespaces.
* @param int $offset token children offset to start lookup with.
* @param int|null $firstMatchIndex token children offset where a successful match begins.
* @param int|null $lastMatchIndex token children offset where a successful match ends.
* @return bool whether this token matches the pattern SQL code.
*/
public function matches(SqlToken $patternToken, $offset = 0, &$firstMatchIndex = null, &$lastMatchIndex = null)
{
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++) {
// Here we iterate token by token with an exception of "any" that toggles
// an iteration until we matched with a next pattern token or EOF.
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();
}
}
}

389
framework/db/SqlTokenizer.php

@ -0,0 +1,389 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
use yii\base\Component;
use yii\base\InvalidParamException;
/**
* SqlTokenizer splits an SQL query into individual SQL tokens.
*
* It can be used to obtain an addition information from an SQL code.
*
* Usage example:
*
* ```php
* $tokenizer = new SqlTokenizer("SELECT * FROM user WHERE id = 1");
* $root = $tokeinzer->tokenize();
* $sqlTokens = $root->getChildren();
* ```
*
* Tokens are instances of [[SqlToken]].
*
* @author Sergey Makinen <sergey@makinen.ru>
* @since 2.0.13
*/
abstract class SqlTokenizer extends Component
{
/**
* @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 stack of active tokens.
*/
private $_tokenStack;
/**
* @var SqlToken active token. It's usually a top of the token stack.
*/
private $_currentToken;
/**
* @var string[] cached substrings.
*/
private $_substrings;
/**
* @var string current buffer value.
*/
private $_buffer = '';
/**
* @var SqlToken resulting token of a last [[tokenize()]] call.
*/
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.
*/
abstract protected 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.
*/
abstract protected 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.
*/
abstract protected 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.
*/
abstract protected 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.
*/
abstract protected 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.
*/
abstract protected 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;
}
}

36
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;
/**
@ -119,6 +120,41 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
* @inheritDoc
* @see http://www.cubrid.org/manual/93/en/sql/schema/table.html#drop-index-clause
*/
public function dropIndex($name, $table)
{
/** @var Schema $schema */
$schema = $this->db->getSchema();
foreach ($schema->getTableUniques($table) as $unique) {
if ($unique->name === $name) {
return $this->dropUnique($name, $table);
}
}
return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table);
}
/**
* @inheritDoc
* @throws NotSupportedException this is not supported by CUBRID.
*/
public function addCheck($name, $table, $expression)
{
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
*/

239
framework/db/cubrid/Schema.php

@ -7,10 +7,15 @@
namespace yii\db\cubrid;
use yii\db\ColumnSchema;
use yii\base\NotSupportedException;
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\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,9 +148,128 @@ 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
* @throws NotSupportedException if this method is called.
*/
protected function loadTableChecks($tableName)
{
throw new NotSupportedException('CUBRID does not support check constraints.');
}
/**
* @inheritDoc
* @throws NotSupportedException if this method is called.
*/
protected function loadTableDefaultValues($tableName)
{
throw new NotSupportedException('CUBRID does not support default value constraints.');
}
/**
* @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
* @return \yii\db\ColumnSchema the column schema object
*/
protected function loadColumnSchema($info)
{
@ -235,26 +338,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 +389,44 @@ class Schema extends \yii\db\Schema
{
return new ColumnSchemaBuilder($type, $length, $this->db);
}
/**
* Loads multiple types of constraints and returns the specified ones.
* @param string $tableName table name.
* @param string $returnType return type:
* - indexes
* - uniques
* @return mixed constraints.
*/
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($constraints, '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];
}
}

19
framework/db/mssql/QueryBuilder.php

@ -177,6 +177,25 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
* @inheritDoc
*/
public function addDefaultValue($name, $table, $column, $value)
{
return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
. $this->db->quoteColumnName($name) . ' DEFAULT ' . $this->db->quoteValue($value) . ' 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.

327
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\DefaultValueConstraint;
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 <resurtm@gmail.com>
* @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,168 @@ 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 = <<<SQL
SELECT ns.nspname AS schema_name
FROM pg_namespace ns
WHERE ns.nspname != 'information_schema' AND ns.nspname NOT LIKE 'pg_%'
ORDER BY ns.nspname
SQL;
return $this->db->createCommand($sql)->queryColumn();
}
/**
* @inheritDoc
*/
protected function findTableNames($schema = '')
{
if ($schema === '') {
$schema = $this->defaultSchema;
}
$sql = <<<SQL
SELECT [t].[table_name]
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW')
ORDER BY [t].[table_name]
SQL;
return $this->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;
}
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 = <<<SQL
SELECT
[i].[name] AS [name],
[iccol].[name] AS [column_name],
[i].[is_unique] AS [index_is_unique],
[i].[is_primary_key] AS [index_is_primary]
FROM [sys].[indexes] AS [i]
INNER JOIN [sys].[index_columns] AS [ic]
ON [ic].[object_id] = [i].[object_id] AND [ic].[index_id] = [i].[index_id]
INNER JOIN [sys].[columns] AS [iccol]
ON [iccol].[object_id] = [ic].[object_id] AND [iccol].[column_id] = [ic].[column_id]
WHERE [i].[object_id] = OBJECT_ID(:fullName)
ORDER BY [ic].[key_ordinal] ASC
SQL;
$resolvedName = $this->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 +298,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 +550,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 = <<<SQL
SELECT [t].[table_name]
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW')
ORDER BY [t].[table_name]
SQL;
return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
}
/**
* @inheritdoc
*/
protected function findViewNames($schema = '')
@ -461,4 +591,119 @@ SQL;
}
return $result;
}
/**
* Loads multiple types of constraints and returns the specified ones.
* @param string $tableName table name.
* @param string $returnType return type:
* - primaryKey
* - foreignKeys
* - uniques
* - checks
* - defaults
* @return mixed constraints.
*/
private function loadTableConstraints($tableName, $returnType)
{
static $sql = <<<SQL
SELECT
[o].[name] AS [name],
COALESCE([ccol].[name], [dcol].[name], [fccol].[name], [kiccol].[name]) AS [column_name],
RTRIM([o].[type]) AS [type],
OBJECT_SCHEMA_NAME([f].[referenced_object_id]) AS [foreign_table_schema],
OBJECT_NAME([f].[referenced_object_id]) AS [foreign_table_name],
[ffccol].[name] AS [foreign_column_name],
[f].[update_referential_action_desc] AS [on_update],
[f].[delete_referential_action_desc] AS [on_delete],
[c].[definition] AS [check_expr],
[d].[definition] AS [default_expr]
FROM (SELECT OBJECT_ID(:fullName) AS [object_id]) AS [t]
INNER JOIN [sys].[objects] AS [o]
ON [o].[parent_object_id] = [t].[object_id] AND [o].[type] IN ('PK', 'UQ', 'C', 'D', 'F')
LEFT JOIN [sys].[check_constraints] AS [c]
ON [c].[object_id] = [o].[object_id]
LEFT JOIN [sys].[columns] AS [ccol]
ON [ccol].[object_id] = [c].[parent_object_id] AND [ccol].[column_id] = [c].[parent_column_id]
LEFT JOIN [sys].[default_constraints] AS [d]
ON [d].[object_id] = [o].[object_id]
LEFT JOIN [sys].[columns] AS [dcol]
ON [dcol].[object_id] = [d].[parent_object_id] AND [dcol].[column_id] = [d].[parent_column_id]
LEFT JOIN [sys].[key_constraints] AS [k]
ON [k].[object_id] = [o].[object_id]
LEFT JOIN [sys].[index_columns] AS [kic]
ON [kic].[object_id] = [k].[parent_object_id] AND [kic].[index_id] = [k].[unique_index_id]
LEFT JOIN [sys].[columns] AS [kiccol]
ON [kiccol].[object_id] = [kic].[object_id] AND [kiccol].[column_id] = [kic].[column_id]
LEFT JOIN [sys].[foreign_keys] AS [f]
ON [f].[object_id] = [o].[object_id]
LEFT JOIN [sys].[foreign_key_columns] AS [fc]
ON [fc].[constraint_object_id] = [o].[object_id]
LEFT JOIN [sys].[columns] AS [fccol]
ON [fccol].[object_id] = [fc].[parent_object_id] AND [fccol].[column_id] = [fc].[parent_column_id]
LEFT JOIN [sys].[columns] AS [ffccol]
ON [ffccol].[object_id] = [fc].[referenced_object_id] AND [ffccol].[column_id] = [fc].[referenced_column_id]
ORDER BY [kic].[key_ordinal] ASC, [fc].[constraint_column_id] ASC
SQL;
$resolvedName = $this->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 DefaultValueConstraint([
'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];
}
}

27
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,32 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
* @inheritDoc
*/
public function dropUnique($name, $table)
{
return $this->dropIndex($name, $table);
}
/**
* @inheritDoc
* @throws NotSupportedException this is not supported by MySQL.
*/
public function addCheck($name, $table, $expression)
{
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.

262
framework/db/mysql/Schema.php

@ -7,9 +7,17 @@
namespace yii\db\mysql;
use yii\base\InvalidConfigException;
use yii\base\NotSupportedException;
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\helpers\ArrayHelper;
/**
* Schema is the class for retrieving metadata from a MySQL database (version 4.1.x and 5.x).
@ -19,6 +27,14 @@ use yii\db\TableSchema;
*/
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 +69,129 @@ 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;
}
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 = <<<SQL
SELECT
`s`.`INDEX_NAME` AS `name`,
`s`.`COLUMN_NAME` AS `column_name`,
`s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
`s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
FROM `information_schema`.`STATISTICS` AS `s`
WHERE `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND `s`.`TABLE_NAME` = :tableName
ORDER BY `s`.`SEQ_IN_INDEX` ASC
SQL;
$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 !== 'PRIMARY' ? $name : null,
'columnNames' => ArrayHelper::getColumn($index, 'column_name'),
]);
}
return $result;
}
/**
* @inheritDoc
*/
protected function loadTableUniques($tableName)
{
return $this->loadTableConstraints($tableName, 'uniques');
}
/**
* @inheritDoc
* @throws NotSupportedException if this method is called.
*/
protected function loadTableChecks($tableName)
{
throw new NotSupportedException('MySQL does not support check constraints.');
}
/**
* @inheritDoc
* @throws NotSupportedException if this method is called.
*/
protected function loadTableDefaultValues($tableName)
{
throw new NotSupportedException('MySQL does not support default value constraints.');
}
/**
* Quotes a table name for use in a query.
@ -86,25 +225,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 +458,105 @@ 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
* Loads multiple types of constraints and returns the specified ones.
* @param string $tableName table name.
* @param string $returnType return type:
* - primaryKey
* - foreignKeys
* - uniques
* @return mixed constraints.
*/
public function createColumnSchemaBuilder($type, $length = null)
private function loadTableConstraints($tableName, $returnType)
{
return new ColumnSchemaBuilder($type, $length, $this->db);
static $sql = <<<SQL
SELECT DISTINCT
`kcu`.`CONSTRAINT_NAME` AS `name`,
`kcu`.`COLUMN_NAME` AS `column_name`,
`tc`.`CONSTRAINT_TYPE` AS `type`,
CASE
WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = `sch`.`name` THEN NULL
ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
END AS `foreign_table_schema`,
`kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
`kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
`rc`.`UPDATE_RULE` AS `on_update`,
`rc`.`DELETE_RULE` AS `on_delete`
FROM (SELECT DATABASE() AS `name`) AS `sch`
INNER JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
ON `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, `sch`.`name`) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName
LEFT JOIN `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
ON `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
LEFT JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
ON `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
ORDER BY `kcu`.`ORDINAL_POSITION` ASC
SQL;
$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' => [],
];
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];
}
}

351
framework/db/oci/Schema.php

@ -8,10 +8,17 @@
namespace yii\db\oci;
use yii\base\InvalidCallException;
use yii\base\NotSupportedException;
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 +31,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 +54,181 @@ 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 = <<<SQL
SELECT
USERNAME
FROM DBA_USERS U
WHERE
EXISTS (SELECT 1 FROM DBA_OBJECTS O WHERE O.OWNER = U.USERNAME)
AND DEFAULT_TABLESPACE NOT IN ('SYSTEM','SYSAUX')
SQL;
return $this->db->createCommand($sql)->queryColumn();
}
/**
* @inheritDoc
*/
protected function findTableNames($schema = '')
{
if ($schema === '') {
$sql = <<<SQL
SELECT
TABLE_NAME
FROM USER_TABLES
UNION ALL
SELECT
VIEW_NAME AS TABLE_NAME
FROM USER_VIEWS
UNION ALL
SELECT
MVIEW_NAME AS TABLE_NAME
FROM USER_MVIEWS
ORDER BY TABLE_NAME
SQL;
$command = $this->db->createCommand($sql);
} else {
$sql = <<<SQL
SELECT
OBJECT_NAME AS TABLE_NAME
FROM ALL_OBJECTS
WHERE
OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
AND OWNER = :schema
ORDER BY OBJECT_NAME
SQL;
$command = $this->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;
}
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 = <<<SQL
SELECT
/*+ PUSH_PRED("ui") PUSH_PRED("uicol") PUSH_PRED("uc") */
"ui"."INDEX_NAME" AS "name",
"uicol"."COLUMN_NAME" AS "column_name",
CASE "ui"."UNIQUENESS" WHEN 'UNIQUE' THEN 1 ELSE 0 END AS "index_is_unique",
CASE WHEN "uc"."CONSTRAINT_NAME" IS NOT NULL THEN 1 ELSE 0 END AS "index_is_primary"
FROM "SYS"."USER_INDEXES" "ui"
LEFT JOIN "SYS"."USER_IND_COLUMNS" "uicol"
ON "uicol"."INDEX_NAME" = "ui"."INDEX_NAME"
LEFT JOIN "SYS"."USER_CONSTRAINTS" "uc"
ON "uc"."OWNER" = "ui"."TABLE_OWNER" AND "uc"."CONSTRAINT_NAME" = "ui"."INDEX_NAME" AND "uc"."CONSTRAINT_TYPE" = 'P'
WHERE "ui"."TABLE_OWNER" = :schemaName AND "ui"."TABLE_NAME" = :tableName
ORDER BY "uicol"."COLUMN_POSITION" ASC
SQL;
$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;
}
/**
* @inheritDoc
*/
protected function loadTableUniques($tableName)
{
return $this->loadTableConstraints($tableName, 'uniques');
}
/**
* @inheritDoc
*/
protected function loadTableChecks($tableName)
{
return $this->loadTableConstraints($tableName, 'checks');
}
/**
* @inheritDoc
* @throws NotSupportedException if this method is called.
*/
protected function loadTableDefaultValues($tableName)
{
throw new NotSupportedException('Oracle does not support default value constraints.');
}
/**
* @inheritdoc
*/
public function releaseSavepoint($name)
@ -77,23 +261,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
@ -320,67 +487,6 @@ SQL;
}
/**
* @inheritdoc
*/
protected function findSchemaNames()
{
$sql = <<<SQL
SELECT
USERNAME
FROM DBA_USERS U
WHERE
EXISTS (SELECT 1 FROM DBA_OBJECTS O WHERE O.OWNER = U.USERNAME)
AND DEFAULT_TABLESPACE NOT IN ('SYSTEM','SYSAUX')
SQL;
return $this->db->createCommand($sql)->queryColumn();
}
/**
* @inheritdoc
*/
protected function findTableNames($schema = '')
{
if ($schema === '') {
$sql = <<<SQL
SELECT
TABLE_NAME
FROM USER_TABLES
UNION ALL
SELECT
VIEW_NAME AS TABLE_NAME
FROM USER_VIEWS
UNION ALL
SELECT
MVIEW_NAME AS TABLE_NAME
FROM USER_MVIEWS
ORDER BY TABLE_NAME
SQL;
$command = $this->db->createCommand($sql);
} else {
$sql = <<<SQL
SELECT
OBJECT_NAME AS TABLE_NAME
FROM ALL_OBJECTS
WHERE
OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
AND OWNER = :schema
ORDER BY OBJECT_NAME
SQL;
$command = $this->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:
*
@ -522,4 +628,93 @@ SQL;
return $result;
}
/**
* Loads multiple types of constraints and returns the specified ones.
* @param string $tableName table name.
* @param string $returnType return type:
* - primaryKey
* - foreignKeys
* - uniques
* - checks
* @return mixed constraints.
*/
private function loadTableConstraints($tableName, $returnType)
{
static $sql = <<<SQL
SELECT
/*+ PUSH_PRED("uc") PUSH_PRED("uccol") PUSH_PRED("fuc") */
"uc"."CONSTRAINT_NAME" AS "name",
"uccol"."COLUMN_NAME" AS "column_name",
"uc"."CONSTRAINT_TYPE" AS "type",
"fuc"."OWNER" AS "foreign_table_schema",
"fuc"."TABLE_NAME" AS "foreign_table_name",
"fuccol"."COLUMN_NAME" AS "foreign_column_name",
"uc"."DELETE_RULE" AS "on_delete",
"uc"."SEARCH_CONDITION" AS "check_expr"
FROM "USER_CONSTRAINTS" "uc"
INNER JOIN "USER_CONS_COLUMNS" "uccol"
ON "uccol"."OWNER" = "uc"."OWNER" AND "uccol"."CONSTRAINT_NAME" = "uc"."CONSTRAINT_NAME"
LEFT JOIN "USER_CONSTRAINTS" "fuc"
ON "fuc"."OWNER" = "uc"."R_OWNER" AND "fuc"."CONSTRAINT_NAME" = "uc"."R_CONSTRAINT_NAME"
LEFT JOIN "USER_CONS_COLUMNS" "fuccol"
ON "fuccol"."OWNER" = "fuc"."OWNER" AND "fuccol"."CONSTRAINT_NAME" = "fuc"."CONSTRAINT_NAME" AND "fuccol"."POSITION" = "uccol"."POSITION"
WHERE "uc"."OWNER" = :schemaName AND "uc"."TABLE_NAME" = :tableName
ORDER BY "uccol"."POSITION" ASC
SQL;
$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 '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];
}
}

315
framework/db/pgsql/Schema.php

@ -7,10 +7,16 @@
namespace yii\db\pgsql;
use yii\db\ColumnSchema;
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\ViewFinderTrait;
use yii\helpers\ArrayHelper;
/**
* Schema is the class for retrieving metadata from a PostgreSQL database
@ -22,6 +28,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.
@ -113,6 +120,155 @@ 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 = <<<SQL
SELECT ns.nspname AS schema_name
FROM pg_namespace ns
WHERE ns.nspname != 'information_schema' AND ns.nspname NOT LIKE 'pg_%'
ORDER BY ns.nspname
SQL;
return $this->db->createCommand($sql)->queryColumn();
}
/**
* @inheritDoc
*/
protected function findTableNames($schema = '')
{
if ($schema === '') {
$schema = $this->defaultSchema;
}
$sql = <<<SQL
SELECT c.relname AS table_name
FROM pg_class c
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f')
ORDER BY c.relname
SQL;
return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
}
/**
* @inheritDoc
*/
protected function loadTableSchema($name)
{
$table = new TableSchema();
$this->resolveTableNames($table, $name);
if ($this->findColumns($table)) {
$this->findConstraints($table);
return $table;
}
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 = <<<SQL
SELECT
"ic"."relname" AS "name",
"ia"."attname" AS "column_name",
"i"."indisunique" AS "index_is_unique",
"i"."indisprimary" AS "index_is_primary"
FROM "pg_class" AS "tc"
INNER JOIN "pg_namespace" AS "tcns"
ON "tcns"."oid" = "tc"."relnamespace"
INNER JOIN "pg_index" AS "i"
ON "i"."indrelid" = "tc"."oid"
INNER JOIN "pg_class" AS "ic"
ON "ic"."oid" = "i"."indexrelid"
INNER JOIN "pg_attribute" AS "ia"
ON "ia"."attrelid" = "i"."indrelid" AND "ia"."attnum" = ANY ("i"."indkey")
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
ORDER BY "ia"."attnum" ASC
SQL;
$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;
}
/**
* @inheritDoc
*/
protected function loadTableUniques($tableName)
{
return $this->loadTableConstraints($tableName, 'uniques');
}
/**
* @inheritDoc
*/
protected function loadTableChecks($tableName)
{
return $this->loadTableConstraints($tableName, 'checks');
}
/**
* @inheritDoc
* @throws NotSupportedException if this method is called.
*/
protected function loadTableDefaultValues($tableName)
{
throw new NotSupportedException('PostgreSQL does not support default value constraints.');
}
/**
* Creates a query builder for the PostgreSQL database.
* @return QueryBuilder query builder instance
*/
@ -153,61 +309,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);
if ($this->findColumns($table)) {
$this->findConstraints($table);
return $table;
} else {
return null;
}
}
/**
* 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
*/
protected function findSchemaNames()
{
$sql = <<<SQL
SELECT ns.nspname AS schema_name
FROM pg_namespace ns
WHERE ns.nspname != 'information_schema' AND ns.nspname NOT LIKE 'pg_%'
ORDER BY ns.nspname
SQL;
return $this->db->createCommand($sql)->queryColumn();
}
/**
* 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 = <<<SQL
SELECT c.relname AS table_name
FROM pg_class c
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f')
ORDER BY c.relname
SQL;
return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
}
/**
* @inheritdoc
*/
protected function findViewNames($schema = '')
@ -506,4 +607,106 @@ SQL;
return !$command->pdoStatement->rowCount() ? false : $result;
}
/**
* Loads multiple types of constraints and returns the specified ones.
* @param string $tableName table name.
* @param string $returnType return type:
* - primaryKey
* - foreignKeys
* - uniques
* - checks
* @return mixed constraints.
*/
private function loadTableConstraints($tableName, $returnType)
{
static $sql = <<<SQL
SELECT
"c"."conname" AS "name",
"a"."attname" AS "column_name",
"c"."contype" AS "type",
"ftcns"."nspname" AS "foreign_table_schema",
"ftc"."relname" AS "foreign_table_name",
"fa"."attname" AS "foreign_column_name",
"c"."confupdtype" AS "on_update",
"c"."confdeltype" AS "on_delete",
"c"."consrc" AS "check_expr"
FROM "pg_class" AS "tc"
INNER JOIN "pg_namespace" AS "tcns"
ON "tcns"."oid" = "tc"."relnamespace"
INNER JOIN "pg_constraint" AS "c"
ON "c"."conrelid" = "tc"."oid"
INNER JOIN "pg_attribute" AS "a"
ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
LEFT JOIN "pg_class" AS "ftc"
ON "ftc"."oid" = "c"."confrelid"
LEFT JOIN "pg_namespace" AS "ftcns"
ON "ftcns"."oid" = "ftc"."relnamespace"
LEFT JOIN "pg_attribute" "fa"
ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
SQL;
static $actionTypes = [
'a' => '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];
}
}

54
framework/db/sqlite/QueryBuilder.php

@ -300,6 +300,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, $expression)
{
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, $value)
{
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

206
framework/db/sqlite/Schema.php

@ -8,10 +8,17 @@
namespace yii\db\sqlite;
use yii\base\NotSupportedException;
use yii\db\CheckConstraint;
use yii\db\ColumnSchema;
use yii\db\Constraint;
use yii\db\ConstraintFinderTrait;
use yii\db\Expression;
use yii\db\ForeignKeyConstraint;
use yii\db\IndexConstraint;
use yii\db\SqlToken;
use yii\db\TableSchema;
use yii\db\Transaction;
use yii\helpers\ArrayHelper;
/**
* Schema is the class for retrieving metadata from a SQLite (2/3) database.
@ -24,6 +31,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 +67,124 @@ 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;
}
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();
/** @var $code SqlToken[]|SqlToken[][]|SqlToken[][][] */
$code = (new SqlTokenizer($sql))->tokenize();
$pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
return [];
}
$createTableToken = $code[0][$lastMatchIndex - 1];
$result = [];
$offset = 0;
while (true) {
$pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
break;
}
$checkSql = $createTableToken[$offset - 1]->getSql();
$name = null;
$pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches($pattern, $firstMatchIndex - 2)) {
$name = $createTableToken[$firstMatchIndex - 1]->content;
}
$result[] = new CheckConstraint([
'name' => $name,
'expression' => $checkSql,
]);
}
return $result;
}
/**
* @inheritDoc
* @throws NotSupportedException if this method is called.
*/
protected function loadTableDefaultValues($tableName)
{
throw new NotSupportedException('SQLite does not support default value constraints.');
}
/**
* Quotes a table name for use in a query.
@ -101,38 +228,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
@ -273,7 +368,7 @@ class Schema extends \yii\db\Schema
* Sets the isolation level of the current transaction.
* @param string $level The transaction isolation level to use for this transaction.
* This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]].
* @throws \yii\base\NotSupportedException when unsupported isolation levels are used.
* @throws NotSupportedException when unsupported isolation levels are used.
* SQLite only supports SERIALIZABLE and READ UNCOMMITTED.
* @see http://www.sqlite.org/pragma.html#pragma_read_uncommitted
*/
@ -290,4 +385,49 @@ class Schema extends \yii\db\Schema
throw new NotSupportedException(get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.');
}
}
/**
* Loads multiple types of constraints and returns the specified ones.
* @param string $tableName table name.
* @param string $returnType return type:
* - primaryKey
* - indexes
* - uniques
* @return mixed constraints.
*/
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];
}
}

288
framework/db/sqlite/SqlTokenizer.php

@ -0,0 +1,288 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db\sqlite;
/**
* SqlTokenizer splits SQLite query into individual SQL tokens.
* It's used to obtain a `CHECK` constraint information from a `CREATE TABLE` SQL code.
*
* @see http://www.sqlite.org/draft/tokenreq.html
* @see https://sqlite.org/lang.html
* @author Sergey Makinen <sergey@makinen.ru>
* @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;
}
}

43
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")
);

43
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])
);

47
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';

43
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
*/

44
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")
);

43
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")
);

28
tests/framework/db/AnyCaseValue.php

@ -0,0 +1,28 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\db;
class AnyCaseValue extends CompareValue
{
public $value;
/**
* Constructor.
* @param string|string[] $value
* @param array $config
*/
public function __construct($value, $config = [])
{
if (is_array($value)) {
$this->value = array_map('strtolower', $value);
} else {
$this->value = strtolower($value);
}
parent::__construct($config);
}
}

24
tests/framework/db/AnyValue.php

@ -0,0 +1,24 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\db;
class AnyValue extends CompareValue
{
/**
* @var self
*/
private static $_instance;
public static function getInstance()
{
if (self::$_instance === null) {
self::$_instance = new self();
}
return self::$_instance;
}
}

157
tests/framework/db/CommandTest.php

@ -680,23 +680,166 @@ SQL;
public function testRenameColumn()
{
}
*/
public function testAddForeignKey()
public function testAddDropPrimaryKey()
{
$db = $this->getConnection(false);
$tableName = 'test_pk';
$name = 'test_pk_constraint';
/** @var \yii\db\pgsql\Schema $schema */
$schema = $db->getSchema();
if ($schema->getTableSchema($tableName) !== null) {
$db->createCommand()->dropTable($tableName)->execute();
}
$db->createCommand()->createTable($tableName, [
'int1' => 'integer not null',
'int2' => 'integer not null',
])->execute();
$this->assertNull($schema->getTablePrimaryKey($tableName, true));
$db->createCommand()->addPrimaryKey($name, $tableName, ['int1'])->execute();
$this->assertEquals(['int1'], $schema->getTablePrimaryKey($tableName, true)->columnNames);
$db->createCommand()->dropPrimaryKey($name, $tableName)->execute();
$this->assertNull($schema->getTablePrimaryKey($tableName, true));
$db->createCommand()->addPrimaryKey($name, $tableName, ['int1', 'int2'])->execute();
$this->assertEquals(['int1', 'int2'], $schema->getTablePrimaryKey($tableName, true)->columnNames);
}
public function testDropForeignKey()
public function testAddDropForeignKey()
{
$db = $this->getConnection(false);
$tableName = 'test_fk';
$name = 'test_fk_constraint';
/** @var \yii\db\pgsql\Schema $schema */
$schema = $db->getSchema();
if ($schema->getTableSchema($tableName) !== null) {
$db->createCommand()->dropTable($tableName)->execute();
}
$db->createCommand()->createTable($tableName, [
'int1' => 'integer not null unique',
'int2' => 'integer not null unique',
'int3' => 'integer not null unique',
'int4' => 'integer not null unique',
'unique ([[int1]], [[int2]])',
'unique ([[int3]], [[int4]])',
])->execute();
$this->assertEmpty($schema->getTableForeignKeys($tableName, true));
$db->createCommand()->addForeignKey($name, $tableName, ['int1'], $tableName, ['int3'])->execute();
$this->assertEquals(['int1'], $schema->getTableForeignKeys($tableName, true)[0]->columnNames);
$this->assertEquals(['int3'], $schema->getTableForeignKeys($tableName, true)[0]->foreignColumnNames);
$db->createCommand()->dropForeignKey($name, $tableName)->execute();
$this->assertEmpty($schema->getTableForeignKeys($tableName, true));
$db->createCommand()->addForeignKey($name, $tableName, ['int1', 'int2'], $tableName, ['int3', 'int4'])->execute();
$this->assertEquals(['int1', 'int2'], $schema->getTableForeignKeys($tableName, true)[0]->columnNames);
$this->assertEquals(['int3', 'int4'], $schema->getTableForeignKeys($tableName, true)[0]->foreignColumnNames);
}
public function testCreateIndex()
public function testCreateDropIndex()
{
$db = $this->getConnection(false);
$tableName = 'test_idx';
$name = 'test_idx_constraint';
/** @var \yii\db\pgsql\Schema $schema */
$schema = $db->getSchema();
if ($schema->getTableSchema($tableName) !== null) {
$db->createCommand()->dropTable($tableName)->execute();
}
$db->createCommand()->createTable($tableName, [
'int1' => 'integer not null',
'int2' => 'integer not null',
])->execute();
$this->assertEmpty($schema->getTableIndexes($tableName, true));
$db->createCommand()->createIndex($name, $tableName, ['int1'])->execute();
$this->assertEquals(['int1'], $schema->getTableIndexes($tableName, true)[0]->columnNames);
$this->assertFalse($schema->getTableIndexes($tableName, true)[0]->isUnique);
$db->createCommand()->dropIndex($name, $tableName)->execute();
$this->assertEmpty($schema->getTableIndexes($tableName, true));
$db->createCommand()->createIndex($name, $tableName, ['int1', 'int2'])->execute();
$this->assertEquals(['int1', 'int2'], $schema->getTableIndexes($tableName, true)[0]->columnNames);
$this->assertFalse($schema->getTableIndexes($tableName, true)[0]->isUnique);
$db->createCommand()->dropIndex($name, $tableName)->execute();
$this->assertEmpty($schema->getTableIndexes($tableName, true));
$this->assertEmpty($schema->getTableIndexes($tableName, true));
$db->createCommand()->createIndex($name, $tableName, ['int1'], true)->execute();
$this->assertEquals(['int1'], $schema->getTableIndexes($tableName, true)[0]->columnNames);
$this->assertTrue($schema->getTableIndexes($tableName, true)[0]->isUnique);
$db->createCommand()->dropIndex($name, $tableName)->execute();
$this->assertEmpty($schema->getTableIndexes($tableName, true));
$db->createCommand()->createIndex($name, $tableName, ['int1', 'int2'], true)->execute();
$this->assertEquals(['int1', 'int2'], $schema->getTableIndexes($tableName, true)[0]->columnNames);
$this->assertTrue($schema->getTableIndexes($tableName, true)[0]->isUnique);
}
public function testDropIndex()
public function testAddDropUnique()
{
$db = $this->getConnection(false);
$tableName = 'test_uq';
$name = 'test_uq_constraint';
/** @var \yii\db\pgsql\Schema $schema */
$schema = $db->getSchema();
if ($schema->getTableSchema($tableName) !== null) {
$db->createCommand()->dropTable($tableName)->execute();
}
$db->createCommand()->createTable($tableName, [
'int1' => 'integer not null',
'int2' => 'integer not null',
])->execute();
$this->assertEmpty($schema->getTableUniques($tableName, true));
$db->createCommand()->addUnique($name, $tableName, ['int1'])->execute();
$this->assertEquals(['int1'], $schema->getTableUniques($tableName, true)[0]->columnNames);
$db->createCommand()->dropUnique($name, $tableName)->execute();
$this->assertEmpty($schema->getTableUniques($tableName, true));
$db->createCommand()->addUnique($name, $tableName, ['int1', 'int2'])->execute();
$this->assertEquals(['int1', 'int2'], $schema->getTableUniques($tableName, true)[0]->columnNames);
}
public function testAddDropCheck()
{
$db = $this->getConnection(false);
$tableName = 'test_ck';
$name = 'test_ck_constraint';
/** @var \yii\db\pgsql\Schema $schema */
$schema = $db->getSchema();
if ($schema->getTableSchema($tableName) !== null) {
$db->createCommand()->dropTable($tableName)->execute();
}
$db->createCommand()->createTable($tableName, [
'int1' => 'integer',
])->execute();
$this->assertEmpty($schema->getTableChecks($tableName, true));
$db->createCommand()->addCheck($name, $tableName, '[[int1]] > 1')->execute();
$this->assertRegExp('/^.*int1.*>.*1.*$/', $schema->getTableChecks($tableName, true)[0]->expression);
$db->createCommand()->dropCheck($name, $tableName)->execute();
$this->assertEmpty($schema->getTableChecks($tableName, true));
}
public function testAddDropDefaultValue()
{
$this->markTestSkipped($this->driverName . ' does not support adding/dropping default value constraints.');
}
*/
public function testIntegrityViolation()
{
@ -850,6 +993,10 @@ SQL;
$tableName = 'test';
$fkName = 'test_fk';
if ($db->getSchema()->getTableSchema($tableName) !== null) {
$db->createCommand()->dropTable($tableName)->execute();
}
$this->assertNull($db->getSchema()->getTableSchema($tableName));
$db->createCommand()->createTable($tableName, [

14
tests/framework/db/CompareValue.php

@ -0,0 +1,14 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\db;
use yii\base\Object;
abstract class CompareValue extends Object
{
}

226
tests/framework/db/QueryBuilderTest.php

@ -1227,34 +1227,212 @@ abstract class QueryBuilderTest extends DatabaseTestCase
$this->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)));
}
// 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);
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');
},
],
];
}
// 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);
/**
* @dataProvider foreignKeysProvider
*/
public function testAddDropForeignKey($sql, \Closure $builder)
{
$this->assertSame($this->getConnection(false)->quoteSql($sql), $builder($this->getQueryBuilder(false)));
}
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()

248
tests/framework/db/SchemaTest.php

@ -9,8 +9,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
@ -431,4 +435,248 @@ 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', false],
'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', false],
'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', false],
'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', false],
];
}
public function lowercaseConstraintsProvider()
{
return $this->constraintsProvider();
}
public function uppercaseConstraintsProvider()
{
return $this->constraintsProvider();
}
/**
* @dataProvider constraintsProvider
*/
public function testTableSchemaConstraints($tableName, $type, $expected)
{
if ($expected === false) {
$this->expectException('yii\base\NotSupportedException');
}
$constraints = $this->getConnection(false)->getSchema()->{'getTable' . ucfirst($type)}($tableName);
$this->assertMetadataEquals($expected, $constraints);
}
/**
* @dataProvider uppercaseConstraintsProvider
*/
public function testTableSchemaConstraintsWithPdoUppercase($tableName, $type, $expected)
{
if ($expected === false) {
$this->expectException('yii\base\NotSupportedException');
}
$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 lowercaseConstraintsProvider
*/
public function testTableSchemaConstraintsWithPdoLowercase($tableName, $type, $expected)
{
if ($expected === false) {
$this->expectException('yii\base\NotSupportedException');
}
$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'], $key['foreignSchemaName']);
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);
}
}
}
}

5
tests/framework/db/cubrid/CommandTest.php

@ -83,4 +83,9 @@ class CommandTest extends \yiiunit\framework\db\CommandTest
$command = $db->createCommand($sql);
$this->assertEquals('SELECT "id", "t"."name" FROM "customer" t', $command->sql);
}
public function testAddDropCheck()
{
$this->markTestSkipped('CUBRID does not support adding/dropping check constraints.');
}
}

21
tests/framework/db/cubrid/QueryBuilderTest.php

@ -32,6 +32,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();
@ -44,4 +54,15 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
$sql = $qb->resetSequence('item', 4);
$this->assertEquals($expected, $sql);
}
public function testCommentColumn()
{
$version = $this->getQueryBuilder(false)->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
if (version_compare($version, '10.0', '<')) {
$this->markTestSkipped('Comments on columns are supported starting with CUBRID 10.0.');
return;
}
parent::testCommentColumn();
}
}

58
tests/framework/db/cubrid/SchemaTest.php

@ -8,6 +8,7 @@
namespace yiiunit\framework\db\cubrid;
use yii\db\Expression;
use yiiunit\framework\db\AnyCaseValue;
/**
* @group db
@ -31,7 +32,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) {
@ -75,4 +75,60 @@ 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] = false;
unset($result['1: index'][2][0]);
$result['2: check'][2] = false;
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] = [];
$result['3: check'][2] = false;
$result['4: check'][2] = false;
return $result;
}
public function lowercaseConstraintsProvider()
{
$this->markTestSkipped('This test hangs on CUBRID.');
}
public function uppercaseConstraintsProvider()
{
$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;
}
}

23
tests/framework/db/mssql/CommandTest.php

@ -93,4 +93,27 @@ class CommandTest extends \yiiunit\framework\db\CommandTest
['SELECT SUBSTRING(name, :len, 6) FROM {{customer}} WHERE [[email]] = :email'],
];
}
public function testAddDropDefaultValue()
{
$db = $this->getConnection(false);
$tableName = 'test_def';
$name = 'test_def_constraint';
/** @var \yii\db\pgsql\Schema $schema */
$schema = $db->getSchema();
if ($schema->getTableSchema($tableName) !== null) {
$db->createCommand()->dropTable($tableName)->execute();
}
$db->createCommand()->createTable($tableName, [
'int1' => 'integer',
])->execute();
$this->assertEmpty($schema->getTableDefaultValues($tableName, true));
$db->createCommand()->addDefaultValue($name, $tableName, 'int1', 41)->execute();
$this->assertRegExp('/^.*41.*$/', $schema->getTableDefaultValues($tableName, true)[0]->value);
$db->createCommand()->dropDefaultValue($name, $tableName)->execute();
$this->assertEmpty($schema->getTableDefaultValues($tableName, true));
}
}

24
tests/framework/db/mssql/SchemaTest.php

@ -7,6 +7,9 @@
namespace yiiunit\framework\db\mssql;
use yii\db\DefaultValueConstraint;
use yiiunit\framework\db\AnyValue;
/**
* @group db
* @group mssql
@ -14,4 +17,25 @@ 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] = [];
$result['1: default'][2][] = new DefaultValueConstraint([
'name' => AnyValue::getInstance(),
'columnNames' => ['C_default'],
'value' => '((0))',
]);
$result['2: default'][2] = [];
$result['3: foreign key'][2][0]->foreignSchemaName = 'dbo';
$result['3: index'][2] = [];
$result['3: default'][2] = [];
$result['4: default'][2] = [];
return $result;
}
}

5
tests/framework/db/mysql/CommandTest.php

@ -14,4 +14,9 @@ namespace yiiunit\framework\db\mysql;
class CommandTest extends \yiiunit\framework\db\CommandTest
{
public $driverName = 'mysql';
public function testAddDropCheck()
{
$this->markTestSkipped('MySQL does not support adding/dropping check constraints.');
}
}

43
tests/framework/db/mysql/QueryBuilderTest.php

@ -62,6 +62,49 @@ 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 uniquesProvider()
{
$result = parent::uniquesProvider();
$result['drop'][0] = 'DROP INDEX [[CN_unique]] ON {{T_constraints_1}}';
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();

18
tests/framework/db/mysql/SchemaTest.php

@ -7,6 +7,8 @@
namespace yiiunit\framework\db\mysql;
use yiiunit\framework\db\AnyCaseValue;
/**
* @group db
* @group mysql
@ -14,4 +16,20 @@ 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] = false;
$result['2: primary key'][2]->name = null;
$result['2: check'][2] = false;
// 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');
$result['3: check'][2] = false;
$result['4: check'][2] = false;
return $result;
}
}

39
tests/framework/db/oci/QueryBuilderTest.php

@ -7,6 +7,7 @@
namespace yiiunit\framework\db\oci;
use yii\db\oci\QueryBuilder;
use yii\db\oci\Schema;
use yiiunit\data\base\TraversableObject;
@ -40,6 +41,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()
{

71
tests/framework/db/oci/SchemaTest.php

@ -7,6 +7,9 @@
namespace yiiunit\framework\db\oci;
use yii\db\CheckConstraint;
use yiiunit\framework\db\AnyValue;
/**
* @group db
* @group oci
@ -95,4 +98,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 = AnyValue::getInstance();
$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;
}
}

12
tests/framework/db/pgsql/QueryBuilderTest.php

@ -100,6 +100,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();

14
tests/framework/db/pgsql/SchemaTest.php

@ -8,7 +8,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;
@ -88,7 +87,6 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest
public function testCompositeFk()
{
/* @var $schema Schema */
$schema = $this->getConnection()->schema;
$table = $schema->getTableSchema('composite_fk');
@ -114,7 +112,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) {
@ -125,7 +122,6 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest
public function testBooleanDefaultValues()
{
/* @var $schema Schema */
$schema = $this->getConnection()->schema;
$table = $schema->getTableSchema('bool_values');
@ -219,4 +215,14 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest
$tableSchema = $db->schema->getTableSchema('test_timestamp_default_null');
$this->assertNull($tableSchema->getColumn('timestamp')->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;
}
}

20
tests/framework/db/sqlite/CommandTest.php

@ -23,4 +23,24 @@ class CommandTest extends \yiiunit\framework\db\CommandTest
$command = $db->createCommand($sql);
$this->assertEquals('SELECT `id`, `t`.`name` FROM `customer` t', $command->sql);
}
public function testAddDropPrimaryKey()
{
$this->markTestSkipped('SQLite does not support adding/dropping primary keys.');
}
public function testAddDropForeignKey()
{
$this->markTestSkipped('SQLite does not support adding/dropping foreign keys.');
}
public function testAddDropUnique()
{
$this->markTestSkipped('SQLite does not support adding/dropping unique constraints.');
}
public function testAddDropCheck()
{
$this->markTestSkipped('SQLite does not support adding/dropping check constraints.');
}
}

31
tests/framework/db/sqlite/QueryBuilderTest.php

@ -51,9 +51,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()

24
tests/framework/db/sqlite/SchemaTest.php

@ -7,6 +7,8 @@
namespace yiiunit\framework\db\sqlite;
use yiiunit\framework\db\AnyValue;
/**
* @group db
* @group sqlite
@ -36,7 +38,6 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest
public function testCompositeFk()
{
/* @var $schema Schema */
$schema = $this->getConnection()->schema;
$table = $schema->getTableSchema('composite_fk');
@ -47,4 +48,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;
}
}

1143
tests/framework/db/sqlite/SqlTokenizerTest.php

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save