diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 302698ff536..43c56c52296 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -27,6 +27,7 @@ Yii Framework 2 Change Log - New #20137: Added `yii\caching\CallbackDependency` to allow using a callback to determine if a cache dependency is still valid (laxity7) - Enh #20134: Raise minimum `PHP` version to `7.3` (@terabytesoftw) - Bug #20141: Update `ezyang/htmlpurifier` dependency to version `4.17` (@terabytesoftw) +- Bug #19817: Add MySQL Query `addCheck()` and `dropCheck()` (@bobonov) 2.0.49.2 October 12, 2023 ------------------------- diff --git a/framework/db/Migration.php b/framework/db/Migration.php index 8bbce464038..a36f0d41697 100644 --- a/framework/db/Migration.php +++ b/framework/db/Migration.php @@ -520,6 +520,35 @@ public function dropIndex($name, $table) $this->endCommand($time); } + /** + * 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. + */ + public function addCheck($name, $table, $expression) + { + $time = $this->beginCommand("add check $name in table $table"); + $this->db->createCommand()->addCheck($name, $table, $expression)->execute(); + $this->endCommand($time); + } + + /** + * 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. + */ + public function dropCheck($name, $table) + { + $time = $this->beginCommand("drop check $name in table $table"); + $this->db->createCommand()->dropCheck($name, $table)->execute(); + $this->endCommand($time); + } + /** * Builds and execute a SQL statement for adding comment to column. * diff --git a/framework/db/mysql/QueryBuilder.php b/framework/db/mysql/QueryBuilder.php index 21f9bc3f227..2cda1b6fa5b 100644 --- a/framework/db/mysql/QueryBuilder.php +++ b/framework/db/mysql/QueryBuilder.php @@ -151,24 +151,6 @@ 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 diff --git a/framework/db/mysql/Schema.php b/framework/db/mysql/Schema.php index 6f8a2eb3ef7..7a60c620f21 100644 --- a/framework/db/mysql/Schema.php +++ b/framework/db/mysql/Schema.php @@ -11,6 +11,7 @@ use Yii; use yii\base\InvalidConfigException; use yii\base\NotSupportedException; +use yii\db\CheckConstraint; use yii\db\Constraint; use yii\db\ConstraintFinderInterface; use yii\db\ConstraintFinderTrait; @@ -201,11 +202,52 @@ protected function loadTableUniques($tableName) /** * {@inheritdoc} - * @throws NotSupportedException if this method is called. */ protected function loadTableChecks($tableName) { - throw new NotSupportedException('MySQL does not support check constraints.'); + // check version MySQL >= 8.0.16 + if (version_compare($this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION), '8.0.16', '<')) { + throw new NotSupportedException('MySQL < 8.0.16 does not support check constraints.'); + } + + $checks = []; + + $sql = <<resolveTableName($tableName); + $tableRows = $this->db->createCommand($sql, [':tableName' => $resolvedName->name])->queryAll(); + + if ($tableRows === []) { + return $checks; + } + + $tableRows = $this->normalizePdoRowKeyCase($tableRows, true); + + foreach ($tableRows as $tableRow) { + $matches = []; + $columnName = null; + + if (preg_match('/\(`?([a-zA-Z0-9_]+)`?\s*[><=]/', $tableRow['check_clause'], $matches)) { + $columnName = $matches[1]; + } + + $check = new CheckConstraint( + [ + 'name' => $tableRow['constraint_name'], + 'columnNames' => [$columnName], + 'expression' => $tableRow['check_clause'], + ] + ); + $checks[] = $check; + } + + return $checks; } /** diff --git a/tests/framework/db/CommandTest.php b/tests/framework/db/CommandTest.php index b888a7073d6..cd3ecc6d686 100644 --- a/tests/framework/db/CommandTest.php +++ b/tests/framework/db/CommandTest.php @@ -619,7 +619,7 @@ public function invalidSelectColumns() * Test INSERT INTO ... SELECT SQL statement with wrong query object. * * @dataProvider invalidSelectColumns - * + * * @param mixed $invalidSelectColumns */ public function testInsertSelectFailed($invalidSelectColumns) @@ -1206,9 +1206,13 @@ public function testAddDropUnique() public function testAddDropCheck() { $db = $this->getConnection(false); + + if (version_compare($db->getServerVersion(), '8.0.16', '<')) { + $this->markTestSkipped('MySQL < 8.0.16 does not support CHECK constraints.'); + } + $tableName = 'test_ck'; $name = 'test_ck_constraint'; - /** @var \yii\db\pgsql\Schema $schema */ $schema = $db->getSchema(); if ($schema->getTableSchema($tableName) !== null) { @@ -1226,6 +1230,7 @@ public function testAddDropCheck() ); $db->createCommand()->dropCheck($name, $tableName)->execute(); + $this->assertEmpty($schema->getTableChecks($tableName, true)); } diff --git a/tests/framework/db/SchemaTest.php b/tests/framework/db/SchemaTest.php index 480aabbfdd0..4dfa40b915c 100644 --- a/tests/framework/db/SchemaTest.php +++ b/tests/framework/db/SchemaTest.php @@ -773,6 +773,14 @@ public function testTableSchemaConstraints($tableName, $type, $expected) $this->expectException('yii\base\NotSupportedException'); } + if ( + $this->driverName === 'mysql' && + version_compare($this->getConnection(false)->getServerVersion(), '8.0.16', '<') && + $type === 'checks' + ) { + $this->expectException('yii\base\NotSupportedException'); + } + $constraints = $this->getConnection(false)->getSchema()->{'getTable' . ucfirst($type)}($tableName); $this->assertMetadataEquals($expected, $constraints); } @@ -789,6 +797,14 @@ public function testTableSchemaConstraintsWithPdoUppercase($tableName, $type, $e $this->expectException('yii\base\NotSupportedException'); } + if ( + $this->driverName === 'mysql' && + version_compare($this->getConnection(false)->getServerVersion(), '8.0.16', '<') && + $type === 'checks' + ) { + $this->expectException('yii\base\NotSupportedException'); + } + $connection = $this->getConnection(false); $connection->getSlavePdo(true)->setAttribute(PDO::ATTR_CASE, PDO::CASE_UPPER); $constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true); @@ -807,6 +823,14 @@ public function testTableSchemaConstraintsWithPdoLowercase($tableName, $type, $e $this->expectException('yii\base\NotSupportedException'); } + if ( + $this->driverName === 'mysql' && + version_compare($this->getConnection(false)->getServerVersion(), '8.0.16', '<') && + $type === 'checks' + ) { + $this->expectException('yii\base\NotSupportedException'); + } + $connection = $this->getConnection(false); $connection->getSlavePdo(true)->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); $constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true); diff --git a/tests/framework/db/mysql/CommandTest.php b/tests/framework/db/mysql/CommandTest.php index 0cd6e63aabf..60c562c737b 100644 --- a/tests/framework/db/mysql/CommandTest.php +++ b/tests/framework/db/mysql/CommandTest.php @@ -17,8 +17,52 @@ class CommandTest extends \yiiunit\framework\db\CommandTest protected $upsertTestCharCast = 'CONVERT([[address]], CHAR)'; - public function testAddDropCheck() + public function testAddDropCheckSeveral() { - $this->markTestSkipped('MySQL does not support adding/dropping check constraints.'); + $db = $this->getConnection(false); + + if (version_compare($db->getServerVersion(), '8.0.16', '<')) { + $this->markTestSkipped('MySQL < 8.0.16 does not support CHECK constraints.'); + } + + $tableName = 'test_ck_several'; + $schema = $db->getSchema(); + + if ($schema->getTableSchema($tableName) !== null) { + $db->createCommand()->dropTable($tableName)->execute(); + } + $db->createCommand()->createTable($tableName, [ + 'int1' => 'integer', + 'int2' => 'integer', + 'int3' => 'integer', + ])->execute(); + + $this->assertEmpty($schema->getTableChecks($tableName, true)); + + $constraints = [ + ['name' => 'check_int1_positive', 'expression' => '[[int1]] > 0', 'expected' => '(`int1` > 0)'], + ['name' => 'check_int2_nonzero', 'expression' => '[[int2]] <> 0', 'expected' => '(`int2` <> 0)'], + ['name' => 'check_int3_less_than_100', 'expression' => '[[int3]] < 100', 'expected' => '(`int3` < 100)'], + ]; + + foreach ($constraints as $constraint) { + $db->createCommand()->addCheck($constraint['name'], $tableName, $constraint['expression'])->execute(); + } + + $tableChecks = $schema->getTableChecks($tableName, true); + $this->assertCount(3, $tableChecks); + + foreach ($constraints as $index => $constraint) { + $this->assertSame( + $constraints[$index]['expected'], + $tableChecks[$index]->expression + ); + } + + foreach ($constraints as $constraint) { + $db->createCommand()->dropCheck($constraint['name'], $tableName)->execute(); + } + + $this->assertEmpty($schema->getTableChecks($tableName, true)); } } diff --git a/tests/framework/db/mysql/QueryBuilderTest.php b/tests/framework/db/mysql/QueryBuilderTest.php index 659bf1a9ee2..f0f8cdfb397 100644 --- a/tests/framework/db/mysql/QueryBuilderTest.php +++ b/tests/framework/db/mysql/QueryBuilderTest.php @@ -196,11 +196,6 @@ public function uniquesProvider() 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.'); @@ -403,7 +398,7 @@ public function testDefaultValues() // primary key columns should have NULL as value $sql = $command->insert('null_values', [])->getRawSql(); $this->assertEquals("INSERT INTO `null_values` (`id`) VALUES (NULL)", $sql); - + // non-primary key columns should have DEFAULT as value $sql = $command->insert('negative_default_values', [])->getRawSql(); $this->assertEquals("INSERT INTO `negative_default_values` (`tinyint_col`) VALUES (DEFAULT)", $sql); diff --git a/tests/framework/db/mysql/SchemaTest.php b/tests/framework/db/mysql/SchemaTest.php index 43a66a60b57..ba98f45e385 100644 --- a/tests/framework/db/mysql/SchemaTest.php +++ b/tests/framework/db/mysql/SchemaTest.php @@ -77,16 +77,14 @@ public function testGetSchemaNames() public function constraintsProvider() { $result = parent::constraintsProvider(); - $result['1: check'][2] = false; + + $result['1: check'][2][0]->expression = "(`C_check` <> _utf8mb4\\'\\')"; $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; }