From f337880346cec7656e538e11382e30181853d14a Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 4 Sep 2025 18:37:38 +0330 Subject: [PATCH 1/7] inspect check constraints --- .../Database/Query/Processors/Processor.php | 19 +++++++++++++ .../Query/Processors/SQLiteProcessor.php | 24 +++++++++++++++++ src/Illuminate/Database/Schema/Builder.php | 19 +++++++++++++ .../Database/Schema/Grammars/Grammar.php | 14 ++++++++++ .../Database/Schema/Grammars/MySqlGrammar.php | 26 ++++++++++++++++++ .../Schema/Grammars/PostgresGrammar.php | 27 +++++++++++++++++++ .../Schema/Grammars/SqlServerGrammar.php | 26 ++++++++++++++++++ .../Database/Schema/SQLiteBuilder.php | 16 +++++++++++ src/Illuminate/Support/Facades/Schema.php | 1 + 9 files changed, 172 insertions(+) diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 46f692e49a58..fa34f70eecfc 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -141,4 +141,23 @@ public function processForeignKeys($results) { return $results; } + + /** + * Process the results of a check constraints query. + * + * @param list> $results + * @return list, definition: string}> + */ + public function processCheckConstraints($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'columns' => explode(',', $result->columns ?? ''), + 'definition' => $result->definition ?? null, + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php index ed4916a7a54d..7aaee23700c2 100644 --- a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php @@ -96,4 +96,28 @@ public function processForeignKeys($results) ]; }, $results); } + + /** + * Process the results of a check constraints query. + * + * @param list> $results + * @param string $sql + * @param list $columns + * @return list, definition: string}> + */ + public function processCheckConstraints($results, ?string $sql = '', array $columns = []) + { + preg_match_all( + '/(?:constraint\s+"?([^"\s]+)"?)?\s+check\s*(\((?:[^()]+|\((?:[^()]+|\([^()]*\))*\))*\))/i', + $sql ?? '', + $matches, + PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL + ); + + return array_map(fn (array $match) => [ + 'name' => $match[1], + 'columns' => array_values(array_filter($columns, fn (string $column) => str_contains($match[2], $column))), + 'definition' => $match[2], + ], $matches); + } } diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index cf3018f89699..acc3aeaf8b9e 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -448,6 +448,25 @@ public function getForeignKeys($table) ); } + /** + * Get the check constraints for a given table. + * + * @param string $table + * @return array + */ + public function getCheckConstraints($table) + { + [$schema, $table] = $this->parseSchemaAndTable($table); + + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processCheckConstraints( + $this->connection->selectFromWriteConnection( + $this->grammar->compileCheckConstraints($schema, $table) + ) + ); + } + /** * Modify a table on the schema. * diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index bd67b9fded29..15afc525426c 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -164,6 +164,20 @@ public function compileForeignKeys($schema, $table) throw new RuntimeException('This database driver does not support retrieving foreign keys.'); } + /** + * Compile the query to determine the check constraints. + * + * @param string|null $schema + * @param string $table + * @return string + * + * @throws \RuntimeException + */ + public function compileCheckConstraints($schema, $table) + { + throw new RuntimeException('This database driver does not support retrieving check constraints.'); + } + /** * Compile a rename column command. * diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 16e8634d3e6b..97dc7efd4c86 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -199,6 +199,32 @@ public function compileForeignKeys($schema, $table) ); } + /** + * Compile the query to determine the check constraints. + * + * @param string|null $schema + * @param string $table + * @return string + * + * @throws \RuntimeException + */ + public function compileCheckConstraints($schema, $table) + { + return sprintf( + 'select cc.constraint_name as `name`, ' + .'group_concat(c.column_name order by c.ordinal_position) as `columns`, ' + .'cc.check_clause as `definition` ' + .'from information_schema.check_constraints cc join information_schema.table_constraints tc ' + .'on tc.constraint_schema = cc.constraint_schema and tc.constraint_name = cc.constraint_name ' + .'left join information_schema.columns c on tc.table_schema = c.table_schema and tc.table_name = c.table_name ' + ."where tc.table_schema = %s and tc.table_name = %s and tc.constraint_type = 'CHECK' " + ."and cc.check_clause like concat('%%`', c.column_name, '`%%') " + .'group by cc.constraint_name, cc.check_clause', + $schema ? $this->quoteString($schema) : 'schema()', + $this->quoteString($table) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index fa95e2a50fd9..f34e0bc54174 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -227,6 +227,33 @@ public function compileForeignKeys($schema, $table) ); } + /** + * Compile the query to determine the check constraints. + * + * @param string|null $schema + * @param string $table + * @return string + * + * @throws \RuntimeException + */ + public function compileCheckConstraints($schema, $table) + { + return sprintf( + 'select c.conname as name, ' + ."string_agg(la.attname, ',' order by conseq.ord) as columns, " + .'right(pg_get_constraintdef(c.oid, true), -6) as definition ' + .'from pg_constraint c ' + .'join pg_class tc on c.conrelid = tc.oid ' + .'join pg_namespace tn on tn.oid = tc.relnamespace ' + .'join lateral unnest(c.conkey) with ordinality as conseq(num, ord) on true ' + .'join pg_attribute la on la.attrelid = c.conrelid and la.attnum = conseq.num ' + ."where c.contype = 'c' and tc.relname = %s and tn.nspname = %s " + .'group by c.conname, c.oid', + $this->quoteString($table), + $schema ? $this->quoteString($schema) : 'current_schema()' + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index 28b5e5a7a161..5db76939714a 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -196,6 +196,32 @@ public function compileForeignKeys($schema, $table) ); } + /** + * Compile the query to determine the check constraints. + * + * @param string|null $schema + * @param string $table + * @return string + * + * @throws \RuntimeException + */ + public function compileCheckConstraints($schema, $table) + { + return sprintf( + 'select cc.name as name, ' + ."string_agg(col.name, ',') within group (order by col.column_id) as columns, " + .'cc.definition as definition ' + .'from sys.check_constraints as cc ' + .'join sys.tables as tbl on cc.parent_object_id = tbl.object_id ' + .'join sys.schemas as scm on tbl.schema_id = scm.schema_id ' + .'left join sys.columns as col on tbl.object_id = col.object_id ' + ."where tbl.name = %s and scm.name = %s and cc.definition like concat('%%[[]', col.name, ']%%') " + .'group by cc.name, cc.definition', + $this->quoteString($table), + $schema ? $this->quoteString($schema) : 'schema_name()', + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index 040f1623f8a1..08aa2a16900d 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -89,6 +89,22 @@ public function getColumns($table) ); } + /** @inheritDoc */ + public function getCheckConstraints($table) + { + $columns = $this->getColumnListing($table); + + [$schema, $table] = $this->parseSchemaAndTable($table); + + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processCheckConstraints( + [], + $this->connection->scalar($this->grammar->compileSqlCreateStatement($schema, $table)), + $columns + ); + } + /** * Drop all tables from the database. * diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 09d0844c8610..4774b6ce6314 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -28,6 +28,7 @@ * @method static array getIndexListing(string $table) * @method static bool hasIndex(string $table, string|array $index, string|null $type = null) * @method static array getForeignKeys(string $table) + * @method static array getCheckConstraints(string $table) * @method static void table(string $table, \Closure $callback) * @method static void create(string $table, \Closure $callback) * @method static void drop(string $table) From ad25e8645d8ce9250979687e7b392740bfde837f Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 4 Sep 2025 18:38:57 +0330 Subject: [PATCH 2/7] add/drop check constraints --- src/Illuminate/Database/Schema/Blueprint.php | 56 ++++++++++++++++++ .../Database/Schema/BlueprintState.php | 33 +++++++++++ .../Database/Schema/ColumnDefinition.php | 1 + .../Database/Schema/Grammars/Grammar.php | 31 ++++++++++ .../Schema/Grammars/PostgresGrammar.php | 6 +- .../Schema/Grammars/SQLiteGrammar.php | 57 +++++++++++++++---- .../Schema/Grammars/SqlServerGrammar.php | 6 +- 7 files changed, 170 insertions(+), 20 deletions(-) diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index de2233249055..96aff17e19c7 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -8,7 +8,9 @@ use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Grammars\Grammar; use Illuminate\Database\Schema\Grammars\MySqlGrammar; +use Illuminate\Database\Schema\Grammars\PostgresGrammar; use Illuminate\Database\Schema\Grammars\SQLiteGrammar; +use Illuminate\Database\Schema\Grammars\SqlServerGrammar; use Illuminate\Support\Collection; use Illuminate\Support\Fluent; use Illuminate\Support\Traits\Macroable; @@ -193,6 +195,7 @@ protected function commandsNamed(array $names) protected function addImpliedCommands() { $this->addFluentIndexes(); + $this->addFluentCheckConstraints(); $this->addFluentCommands(); if (! $this->creating()) { @@ -256,6 +259,36 @@ protected function addFluentIndexes() } } + /** + * Add the check constraint commands fluently specified on columns. + * + * @return void + */ + protected function addFluentCheckConstraints() + { + foreach ($this->columns as $column) { + if ($column->type === 'enum' && ( + $this->grammar instanceof PostgresGrammar || + $this->grammar instanceof SQLiteGrammar || + $this->grammar instanceof SqlServerGrammar + )) { + $column->check = sprintf('%s in (%s)', + $this->grammar->wrap($column->name), + $this->grammar->quoteString($column->allowed) + ); + } + + if ($column->check) { + $this->check( + $column->check, + $this->createIndexName('check', [$column->name]) + ); + + $column->check = null; + } + } + } + /** * Add the fluent commands specified on any columns. * @@ -540,6 +573,17 @@ public function dropConstrainedForeignIdFor($model, $column = null) return $this->dropConstrainedForeignId($column ?: $model->getForeignKey()); } + /** + * Indicate that the given check constraint should be dropped. + * + * @param string $name + * @return \Illuminate\Support\Fluent + */ + public function dropCheck(string $name): Fluent + { + return $this->addCommand('dropCheck', ['constraint' => $name]); + } + /** * Indicate that the given indexes should be renamed. * @@ -724,6 +768,18 @@ public function foreign($columns, $name = null) return $command; } + /** + * Specify a check constraint for the table. + * + * @param \Illuminate\Contracts\Database\Query\Expression|string $expression + * @param string|null $name + * @return \Illuminate\Support\Fluent + */ + public function check(Expression|string $expression, ?string $name = null): Fluent + { + return $this->addCommand('check', ['expression' => $expression, 'constraint' => $name]); + } + /** * Create a new auto-incrementing big integer (8-byte) column on the table. * diff --git a/src/Illuminate/Database/Schema/BlueprintState.php b/src/Illuminate/Database/Schema/BlueprintState.php index a4ad1149d479..51fa62b5e9f2 100644 --- a/src/Illuminate/Database/Schema/BlueprintState.php +++ b/src/Illuminate/Database/Schema/BlueprintState.php @@ -52,6 +52,13 @@ class BlueprintState */ private $foreignKeys; + /** + * The check constraints. + * + * @var \Illuminate\Support\Fluent[] + */ + private $checkConstraints; + /** * Create a new blueprint state instance. * @@ -103,6 +110,11 @@ public function __construct(Blueprint $blueprint, Connection $connection) 'onUpdate' => $foreignKey['on_update'], 'onDelete' => $foreignKey['on_delete'], ]))->all(); + + $this->checkConstraints = (new Collection($schema->getCheckConstraints($table)))->map(fn ($constraint) => new Fluent([ + 'constraint' => $constraint['name'], + 'expression' => new Expression(Str::unwrap($constraint['definition'], '(', ')')), + ]))->all(); } /** @@ -145,6 +157,16 @@ public function getForeignKeys() return $this->foreignKeys; } + /** + * Get the check constraints. + * + * @return \Illuminate\Support\Fluent[] + */ + public function getCheckConstraints() + { + return $this->checkConstraints; + } + /* * Update the blueprint's state. * @@ -224,6 +246,10 @@ public function update(Fluent $command) $this->foreignKeys[] = $command; break; + case 'check': + $this->checkConstraints[] = $command; + break; + case 'dropPrimary': $this->primaryKey = null; break; @@ -241,6 +267,13 @@ public function update(Fluent $command) array_filter($this->foreignKeys, fn ($fk) => $fk->columns !== $command->columns) ); + break; + + case 'dropCheck': + $this->checkConstraints = array_values( + array_filter($this->checkConstraints, fn ($constraint) => $constraint->constraint !== $command->constraint) + ); + break; } } diff --git a/src/Illuminate/Database/Schema/ColumnDefinition.php b/src/Illuminate/Database/Schema/ColumnDefinition.php index 255ddabd5873..907eed875054 100644 --- a/src/Illuminate/Database/Schema/ColumnDefinition.php +++ b/src/Illuminate/Database/Schema/ColumnDefinition.php @@ -10,6 +10,7 @@ * @method $this autoIncrement() Set INTEGER columns as auto-increment (primary key) * @method $this change() Change the column * @method $this charset(string $charset) Specify a character set for the column (MySQL) + * @method $this check(\Illuminate\Contracts\Database\Query\Expression|string $expression) Specify a check constraint for the column * @method $this collation(string $collation) Specify a collation for the column * @method $this comment(string $comment) Add a comment to the column (MySQL/PostgreSQL) * @method $this default(mixed $value) Specify a "default" value for the column diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index 15afc525426c..f4100334ccf3 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -288,6 +288,37 @@ public function compileDropForeign(Blueprint $blueprint, Fluent $command) throw new RuntimeException('This database driver does not support dropping foreign keys.'); } + /** + * Compile a check constraint command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string|null + */ + public function compileCheck(Blueprint $blueprint, Fluent $command) + { + return sprintf('alter table %s add%s check (%s)', + $this->wrapTable($blueprint), + $command->constraint ? ' constraint '.$this->wrap($command->constraint) : '', + $this->getValue($command->expression) + ); + } + + /** + * Compile a drop check constraint command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string|null + */ + public function compileDropCheck(Blueprint $blueprint, Fluent $command) + { + return sprintf('alter table %s drop constraint %s', + $this->wrapTable($blueprint), + $this->wrap($command->constraint) + ); + } + /** * Compile the blueprint's added column definitions. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index f34e0bc54174..e759a46b7a68 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -969,11 +969,7 @@ protected function typeBoolean(Fluent $column) */ protected function typeEnum(Fluent $column) { - return sprintf( - 'varchar(255) check ("%s" in (%s))', - $column->name, - $this->quoteString($column->allowed) - ); + return 'varchar(255)'; } /** diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 8908836dd9c7..b0749340f7aa 100644 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -33,7 +33,7 @@ class SQLiteGrammar extends Grammar */ public function getAlterCommands() { - $alterCommands = ['change', 'primary', 'dropPrimary', 'foreign', 'dropForeign']; + $alterCommands = ['change', 'primary', 'dropPrimary', 'foreign', 'dropForeign', 'check', 'dropCheck']; if (version_compare($this->connection->getServerVersion(), '3.35', '<')) { $alterCommands[] = 'dropColumn'; @@ -230,12 +230,13 @@ public function compileForeignKeys($schema, $table) */ public function compileCreate(Blueprint $blueprint, Fluent $command) { - return sprintf('%s table %s (%s%s%s)', + return sprintf('%s table %s (%s%s%s%s)', $blueprint->temporary ? 'create temporary' : 'create', $this->wrapTable($blueprint), implode(', ', $this->getColumns($blueprint)), $this->addForeignKeys($this->getCommandsByName($blueprint, 'foreign')), - $this->addPrimaryKeys($this->getCommandByName($blueprint, 'primary')) + $this->addPrimaryKeys($this->getCommandByName($blueprint, 'primary')), + $this->addCheckConstraints($this->getCommandsByName($blueprint, 'check')), ); } @@ -299,6 +300,21 @@ protected function addPrimaryKeys($primary) } } + /** + * Get the check constraint syntax for a table creation statement. + * + * @param \Illuminate\Support\Fluent[] $constraints + * @return string|null + */ + protected function addCheckConstraints($constraints) + { + return (new Collection($constraints))->reduce(fn ($sql, $constraint) => $sql.sprintf( + ',%s check (%s)', + $constraint->constraint ? ' constraint '.$this->wrap($constraint->constraint) : '', + $this->getValue($constraint->expression) + ), ''); + } + /** * Compile alter table commands for adding columns. * @@ -358,11 +374,12 @@ public function compileAlter(Blueprint $blueprint, Fluent $command) return array_filter(array_merge([ $foreignKeyConstraintsEnabled ? $this->compileDisableForeignKeyConstraints() : null, - sprintf('create table %s (%s%s%s)', + sprintf('create table %s (%s%s%s%s)', $tempTable, implode(', ', $columns), $this->addForeignKeys($blueprint->getState()->getForeignKeys()), - $autoIncrementColumn ? '' : $this->addPrimaryKeys($blueprint->getState()->getPrimaryKey()) + $autoIncrementColumn ? '' : $this->addPrimaryKeys($blueprint->getState()->getPrimaryKey()), + $this->addCheckConstraints($blueprint->getState()->getCheckConstraints()) ), sprintf('insert into %s (%s) select %s from %s', $tempTable, $columnNames, $columnNames, $table), sprintf('drop table %s', $table), @@ -452,6 +469,18 @@ public function compileForeign(Blueprint $blueprint, Fluent $command) // Handled on table creation or alteration... } + /** + * Compile a check constraint command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string|null + */ + public function compileCheck(Blueprint $blueprint, Fluent $command) + { + // Handled on table creation or alteration... + } + /** * Compile a drop table command. * @@ -608,6 +637,18 @@ public function compileDropForeign(Blueprint $blueprint, Fluent $command) // Handled on table alteration... } + /** + * Compile a drop check constraint command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string|null + */ + public function compileDropCheck(Blueprint $blueprint, Fluent $command) + { + // Handled on table alteration... + } + /** * Compile a rename table command. * @@ -870,11 +911,7 @@ protected function typeBoolean(Fluent $column) */ protected function typeEnum(Fluent $column) { - return sprintf( - 'varchar check ("%s" in (%s))', - $column->name, - $this->quoteString($column->allowed) - ); + return 'varchar'; } /** diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index 5db76939714a..252ad483efc0 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -760,11 +760,7 @@ protected function typeBoolean(Fluent $column) */ protected function typeEnum(Fluent $column) { - return sprintf( - 'nvarchar(255) check ("%s" in (%s))', - $column->name, - $this->quoteString($column->allowed) - ); + return 'nvarchar(255)'; } /** From edc2d90a889ee08a8323f8b10d132fb18ce1c2ea Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 4 Sep 2025 18:45:34 +0330 Subject: [PATCH 3/7] add tests --- .../DatabasePostgresSchemaGrammarTest.php | 8 +- .../DatabaseSQLiteSchemaGrammarTest.php | 26 ++++- .../Database/DatabaseSchemaBlueprintTest.php | 94 +++++++++++++++++++ .../DatabaseSqlServerSchemaGrammarTest.php | 8 +- .../Database/SchemaBuilderTest.php | 78 +++++++++++++++ .../Sqlite/DatabaseSchemaBlueprintTest.php | 52 ++++++++++ .../Sqlite/DatabaseSchemaBuilderTest.php | 23 +++++ 7 files changed, 279 insertions(+), 10 deletions(-) diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index cf2ea7d63f4a..0f672380f7d7 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -783,9 +783,11 @@ public function testAddingEnum() $blueprint->enum('status', Foo::cases()); $statements = $blueprint->toSql(); - $this->assertCount(2, $statements); - $this->assertSame('alter table "users" add column "role" varchar(255) check ("role" in (\'member\', \'admin\')) not null', $statements[0]); - $this->assertSame('alter table "users" add column "status" varchar(255) check ("status" in (\'bar\')) not null', $statements[1]); + $this->assertCount(4, $statements); + $this->assertSame('alter table "users" add column "role" varchar(255) not null', $statements[0]); + $this->assertSame('alter table "users" add column "status" varchar(255) not null', $statements[1]); + $this->assertSame('alter table "users" add constraint "users_role_check" check ("role" in (\'member\', \'admin\'))', $statements[2]); + $this->assertSame('alter table "users" add constraint "users_status_check" check ("status" in (\'bar\'))', $statements[3]); } public function testAddingDate() diff --git a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php index 90fc5b053781..f9f1d05f378c 100755 --- a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php +++ b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php @@ -565,14 +565,29 @@ public function testAddingBoolean() public function testAddingEnum() { - $blueprint = new Blueprint($this->getConnection(), 'users'); + $builder = mock(SQLiteBuilder::class) + ->makePartial() + ->shouldReceive('getColumns')->andReturn([]) + ->shouldReceive('getIndexes')->andReturn([]) + ->shouldReceive('getForeignKeys')->andReturn([]) + ->shouldReceive('getCheckConstraints')->andReturn([]) + ->getMock(); + + $connection = $this->getConnection(builder: $builder); + $connection->shouldReceive('scalar')->with('pragma foreign_keys')->andReturn(false); + + $blueprint = new Blueprint($connection, 'users'); $blueprint->enum('role', ['member', 'admin']); $blueprint->enum('status', Foo::cases()); $statements = $blueprint->toSql(); - $this->assertCount(2, $statements); - $this->assertSame('alter table "users" add column "role" varchar check ("role" in (\'member\', \'admin\')) not null', $statements[0]); - $this->assertSame('alter table "users" add column "status" varchar check ("status" in (\'bar\')) not null', $statements[1]); + $this->assertCount(6, $statements); + $this->assertSame('alter table "users" add column "role" varchar not null', $statements[0]); + $this->assertSame('alter table "users" add column "status" varchar not null', $statements[1]); + $this->assertSame('create table "__temp__users" ("role" varchar not null, "status" varchar not null, constraint "users_role_check" check ("role" in (\'member\', \'admin\')), constraint "users_status_check" check ("status" in (\'bar\')))', $statements[2]); + $this->assertSame('insert into "__temp__users" ("role", "status") select "role", "status" from "users"', $statements[3]); + $this->assertSame('drop table "users"', $statements[4]); + $this->assertSame('alter table "__temp__users" rename to "users"', $statements[5]); } public function testAddingJson() @@ -1088,6 +1103,7 @@ public function testRenamingAndChangingColumnsWork() ]) ->shouldReceive('getIndexes')->andReturn([]) ->shouldReceive('getForeignKeys')->andReturn([]) + ->shouldReceive('getCheckConstraints')->andReturn([]) ->getMock(); $connection = $this->getConnection(builder: $builder); @@ -1116,6 +1132,7 @@ public function testRenamingAndChangingColumnsWorkWithSchema() ]) ->shouldReceive('getIndexes')->andReturn([]) ->shouldReceive('getForeignKeys')->andReturn([]) + ->shouldReceive('getCheckConstraints')->andReturn([]) ->getMock(); $connection = $this->getConnection(builder: $builder); @@ -1164,6 +1181,7 @@ public function getBuilder() ->shouldReceive('getColumns')->andReturn([]) ->shouldReceive('getIndexes')->andReturn([]) ->shouldReceive('getForeignKeys')->andReturn([]) + ->shouldReceive('getCheckConstraints')->andReturn([]) ->getMock(); } } diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index 76c734baa369..273e85db50ad 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Database\Connection; +use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Builder; use Illuminate\Database\Schema\Grammars\MySqlGrammar; @@ -655,6 +656,99 @@ public function testColumnDefault() $this->assertEquals(['alter table `posts` add `note` tinytext not null default \'this\'\'ll work too\''], $getSql('MySql')); } + public function testAddCheckConstraintWorks() + { + $getSql = function ($grammar) { + return $this->getBlueprint($grammar, 'users', function ($table) { + $table->check('c1 > 10'); + $table->check('c1 > 10', 'foo'); + $table->check(new Expression('c1 > 10')); + $table->check(new Expression('c1 > 10'), 'foo'); + })->toSql(); + }; + + $this->assertEquals([ + 'alter table `users` add check (c1 > 10)', + 'alter table `users` add constraint `foo` check (c1 > 10)', + 'alter table `users` add check (c1 > 10)', + 'alter table `users` add constraint `foo` check (c1 > 10)', + ], $getSql('MySql')); + + $this->assertEquals([ + 'alter table "users" add check (c1 > 10)', + 'alter table "users" add constraint "foo" check (c1 > 10)', + 'alter table "users" add check (c1 > 10)', + 'alter table "users" add constraint "foo" check (c1 > 10)', + ], $getSql('Postgres')); + + $this->assertEquals([ + 'alter table "users" add check (c1 > 10)', + 'alter table "users" add constraint "foo" check (c1 > 10)', + 'alter table "users" add check (c1 > 10)', + 'alter table "users" add constraint "foo" check (c1 > 10)', + ], $getSql('SqlServer')); + } + + public function testDropCheckConstraintWorks() + { + $getSql = function ($grammar) { + return $this->getBlueprint($grammar, 'users', function ($table) { + $table->dropCheck('foo'); + })->toSql(); + }; + + $this->assertEquals(['alter table `users` drop constraint `foo`'], $getSql('MySql')); + + $this->assertEquals(['alter table "users" drop constraint "foo"'], $getSql('Postgres')); + + $this->assertEquals(['alter table "users" drop constraint "foo"'], $getSql('SqlServer')); + } + + public function testCheckConstraintOnColumnsWorks() + { + $getSql = function ($grammar) { + return $this->getBlueprint($grammar, 'users', function (Blueprint $table) { + $table->enum('e1', ['v1', 'v2'])->change(); + $table->enum('e2', ['v3', 'v4']); + $table->integer('c1')->check('c1 > 10'); + $table->integer('c2')->check(new Expression('c2 > 10')); + })->toSql(); + }; + + $this->assertEquals([ + "alter table `users` modify `e1` enum('v1', 'v2') not null", + "alter table `users` add `e2` enum('v3', 'v4') not null", + 'alter table `users` add `c1` int not null', + 'alter table `users` add `c2` int not null', + 'alter table `users` add constraint `users_c1_check` check (c1 > 10)', + 'alter table `users` add constraint `users_c2_check` check (c2 > 10)', + ], $getSql('MySql')); + + $this->assertEquals([ + 'alter table "users" alter column "e1" type varchar(255), alter column "e1" set not null, alter column "e1" drop default, alter column "e1" drop identity if exists', + 'alter table "users" add column "e2" varchar(255) not null', + 'alter table "users" add column "c1" integer not null', + 'alter table "users" add column "c2" integer not null', + 'alter table "users" add constraint "users_e1_check" check ("e1" in (\'v1\', \'v2\'))', + 'alter table "users" add constraint "users_e2_check" check ("e2" in (\'v3\', \'v4\'))', + 'alter table "users" add constraint "users_c1_check" check (c1 > 10)', + 'alter table "users" add constraint "users_c2_check" check (c2 > 10)', + 'comment on column "users"."e1" is NULL', + ], $getSql('Postgres')); + + $this->assertEquals([ + "DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"users\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'\"users\"') AND [name] in ('e1') AND [default_object_id] <> 0;EXEC(@sql)", + 'alter table "users" alter column "e1" nvarchar(255) not null', + 'alter table "users" add "e2" nvarchar(255) not null', + 'alter table "users" add "c1" int not null', + 'alter table "users" add "c2" int not null', + 'alter table "users" add constraint "users_e1_check" check ("e1" in (N\'v1\', N\'v2\'))', + 'alter table "users" add constraint "users_e2_check" check ("e2" in (N\'v3\', N\'v4\'))', + 'alter table "users" add constraint "users_c1_check" check (c1 > 10)', + 'alter table "users" add constraint "users_c2_check" check (c2 > 10)', + ], $getSql('SqlServer')); + } + protected function getConnection(?string $grammar = null, string $prefix = '') { $connection = m::mock(Connection::class) diff --git a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php index 0e2dbafd5e2d..0f0884f6a603 100755 --- a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php +++ b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php @@ -602,9 +602,11 @@ public function testAddingEnum() $blueprint->enum('status', Foo::cases()); $statements = $blueprint->toSql(); - $this->assertCount(2, $statements); - $this->assertSame('alter table "users" add "role" nvarchar(255) check ("role" in (N\'member\', N\'admin\')) not null', $statements[0]); - $this->assertSame('alter table "users" add "status" nvarchar(255) check ("status" in (N\'bar\')) not null', $statements[1]); + $this->assertCount(4, $statements); + $this->assertSame('alter table "users" add "role" nvarchar(255) not null', $statements[0]); + $this->assertSame('alter table "users" add "status" nvarchar(255) not null', $statements[1]); + $this->assertSame('alter table "users" add constraint "users_role_check" check ("role" in (N\'member\', N\'admin\'))', $statements[2]); + $this->assertSame('alter table "users" add constraint "users_status_check" check ("status" in (N\'bar\'))', $statements[3]); } public function testAddingJson() diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index 6fca5a006116..63241f1dfac0 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -564,6 +564,84 @@ public function testAlteringTableWithForeignKeyConstraintsEnabled() )); } + public function testTableWithCheckConstraints() + { + if ($this->driver === 'mysql' && version_compare(DB::getServerVersion(), '8.0.19', '<')) { + $this->markTestSkipped('This test requires MySQL >= 8.0.19'); + } + + Schema::create('test', function (Blueprint $table) { + $table->integer('c1')->check('c1 > 100'); + $table->integer('c2'); + $table->enum('c3', ['foo', 'bar', 'baz']); + $table->check('c1 > 0 and c2 < 0', 'my_constraint'); + }); + + $constraints = Schema::getCheckConstraints('test'); + + $this->assertCount($this->driver === 'mysql' ? 2 : 3, $constraints); + $this->assertContains(['name' => 'test_c1_check', 'columns' => ['c1'], 'definition' => match ($this->driver) { + 'mysql' => '(`c1` > 100)', + 'sqlsrv' => '([c1]>(100))', + default => '(c1 > 100)', + }], $constraints); + $this->assertContains(['name' => 'my_constraint', 'columns' => ['c1', 'c2'], 'definition' => match ($this->driver) { + 'mysql' => '((`c1` > 0) and (`c2` < 0))', + 'pgsql' => '(c1 > 0 AND c2 < 0)', + 'sqlsrv' => '([c1]>(0) AND [c2]<(0))', + default => '(c1 > 0 and c2 < 0)', + }], $constraints); + + if ($this->driver !== 'mysql') { + $this->assertContains(['name' => 'test_c3_check', 'columns' => ['c3'], 'definition' => match ($this->driver) { + 'pgsql' => "(c3::text = ANY (ARRAY['foo'::character varying, 'bar'::character varying, 'baz'::character varying]::text[]))", + 'sqlsrv' => "([c3]=N'baz' OR [c3]=N'bar' OR [c3]=N'foo')", + default => '("c3" in (\'foo\', \'bar\', \'baz\'))', + }], $constraints); + } + + Schema::table('test', function (Blueprint $table) { + $table->dropCheck('my_constraint'); + $table->integer('c4')->check('c4 < 100'); + $table->enum('c2', ['v1', 'v2'])->change(); + $table->check('c1 > 0 and c4 > 0', 'unsigned_columns'); + }); + + $constraints = Schema::getCheckConstraints('test'); + + $this->assertCount($this->driver === 'mysql' ? 3 : 5, $constraints); + $this->assertContains(['name' => 'test_c1_check', 'columns' => ['c1'], 'definition' => match ($this->driver) { + 'mysql' => '(`c1` > 100)', + 'sqlsrv' => '([c1]>(100))', + default => '(c1 > 100)', + }], $constraints); + $this->assertContains(['name' => 'unsigned_columns', 'columns' => ['c1', 'c4'], 'definition' => match ($this->driver) { + 'mysql' => '((`c1` > 0) and (`c4` > 0))', + 'pgsql' => '(c1 > 0 AND c4 > 0)', + 'sqlsrv' => '([c1]>(0) AND [c4]>(0))', + default => '(c1 > 0 and c4 > 0)', + }], $constraints); + $this->assertContains(['name' => 'test_c4_check', 'columns' => ['c4'], 'definition' => match ($this->driver) { + 'mysql' => '(`c4` < 100)', + 'sqlsrv' => '([c4]<(100))', + default => '(c4 < 100)', + }], $constraints); + + if ($this->driver !== 'mysql') { + $this->assertContains(['name' => 'test_c3_check', 'columns' => ['c3'], 'definition' => match ($this->driver) { + 'pgsql' => "(c3::text = ANY (ARRAY['foo'::character varying, 'bar'::character varying, 'baz'::character varying]::text[]))", + 'sqlsrv' => "([c3]=N'baz' OR [c3]=N'bar' OR [c3]=N'foo')", + default => '("c3" in (\'foo\', \'bar\', \'baz\'))', + }], $constraints); + + $this->assertContains(['name' => 'test_c2_check', 'columns' => ['c2'], 'definition' => match ($this->driver) { + 'pgsql' => "(c2::text = ANY (ARRAY['v1'::character varying, 'v2'::character varying]::text[]))", + 'sqlsrv' => "([c2]=N'v2' OR [c2]=N'v1')", + default => '("c2" in (\'v1\', \'v2\'))', + }], $constraints); + } + } + #[RequiresDatabase('mariadb')] public function testSystemVersionedTables() { diff --git a/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php b/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php index cf1e71712318..9bd7066f087b 100644 --- a/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php +++ b/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Integration\Database\Sqlite; use Closure; +use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; @@ -509,6 +510,57 @@ public function testItEnsuresDroppingForeignKeyIsAvailable() }); } + public function testAddCheckConstraintWorks() + { + $getSql = function ($grammar) { + return $this->getBlueprint($grammar, 'users', function ($table) { + $table->create(); + $table->string('c1')->check('c1 > 10'); + $table->check('c1 > 20'); + $table->check('c1 > 30', 'foo'); + $table->check(new Expression('c1 > 40')); + $table->check(new Expression('c1 > 50'), 'foo'); + })->toSql(); + }; + + $this->assertEquals([ + 'create table "users" ("c1" varchar not null, ' + .'check (c1 > 20), ' + .'constraint "foo" check (c1 > 30), ' + .'check (c1 > 40), ' + .'constraint "foo" check (c1 > 50), ' + .'constraint "users_c1_check" check (c1 > 10))', + ], $getSql('SQLite')); + + DB::connection()->getSchemaBuilder()->create('users', function ($table) { + $table->string('e1'); + $table->check('e1 > 10', 'foo'); + }); + + $getSql = function ($grammar) { + return $this->getBlueprint($grammar, 'users', function ($table) { + $table->enum('e2', ['v3', 'v4']); + $table->integer('c1')->check('c1 > 10'); + $table->dropCheck('foo'); + $table->enum('e1', ['v1', 'v2'])->change(); + $table->check(new Expression('c1 > 20'), 'foo'); + })->toSql(); + }; + + $this->assertEquals([ + 'alter table "users" add column "e2" varchar not null', + 'alter table "users" add column "c1" integer not null', + 'create table "__temp__users" ("e1" varchar not null, "e2" varchar not null, "c1" integer not null, ' + .'constraint "foo" check (c1 > 20), ' + .'constraint "users_e2_check" check ("e2" in (\'v3\', \'v4\')), ' + .'constraint "users_c1_check" check (c1 > 10), ' + .'constraint "users_e1_check" check ("e1" in (\'v1\', \'v2\')))', + 'insert into "__temp__users" ("e1", "e2", "c1") select "e1", "e2", "c1" from "users"', + 'drop table "users"', + 'alter table "__temp__users" rename to "users"', + ], $getSql('SQLite')); + } + protected function getBlueprint( string $grammar, string $table, diff --git a/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php b/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php index 46c00c6dccaa..be2d6e745aa7 100644 --- a/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php +++ b/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php @@ -137,4 +137,27 @@ public function testAlterTableAddForeignKeyWithExpressionDefault() $this->assertTrue(collect($columns)->contains(fn ($column) => $column['name'] === 'item_id' && $column['nullable'])); } + + public function testGetCheckConstraints() + { + DB::statement(<< 0), + price DECIMAL(8,2) CONSTRAINT positive_price CHECK (price > 0), + discount_percent INT CHECK (discount_percent >= 0 AND discount_percent <= 100), + CONSTRAINT price_discount_check CHECK (price * (1 - discount_percent/100.0) > 0) + ) + SQL); + + $constraints = Schema::getCheckConstraints('products'); + + $this->assertCount(5, $constraints); + $this->assertContains(['name' => null, 'columns' => ['status'], 'definition' => "(\"status\" in ('pending', 'processing', 'shipped', 'delivered'))"], $constraints); + $this->assertContains(['name' => null, 'columns' => ['product_name'], 'definition' => '(LENGTH(product_name) > 0)'], $constraints); + $this->assertContains(['name' => 'positive_price', 'columns' => ['price'], 'definition' => '(price > 0)'], $constraints); + $this->assertContains(['name' => null, 'columns' => ['discount_percent'], 'definition' => '(discount_percent >= 0 AND discount_percent <= 100)'], $constraints); + $this->assertContains(['name' => 'price_discount_check', 'columns' => ['price', 'discount_percent'], 'definition' => '(price * (1 - discount_percent/100.0) > 0)'], $constraints); + } } From 8c847e118773a0202125a207cf1aa93fc9d7c6ba Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 4 Sep 2025 19:03:34 +0330 Subject: [PATCH 4/7] fix mariadb tests --- .../Integration/Database/SchemaBuilderTest.php | 18 +++++++++--------- .../Sqlite/DatabaseSchemaBuilderTest.php | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index 63241f1dfac0..0d649c4230a8 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -579,20 +579,20 @@ public function testTableWithCheckConstraints() $constraints = Schema::getCheckConstraints('test'); - $this->assertCount($this->driver === 'mysql' ? 2 : 3, $constraints); + $this->assertCount(in_array($this->driver, ['mysql', 'mariadb']) ? 2 : 3, $constraints); $this->assertContains(['name' => 'test_c1_check', 'columns' => ['c1'], 'definition' => match ($this->driver) { - 'mysql' => '(`c1` > 100)', + 'mysql', 'mariadb' => '(`c1` > 100)', 'sqlsrv' => '([c1]>(100))', default => '(c1 > 100)', }], $constraints); $this->assertContains(['name' => 'my_constraint', 'columns' => ['c1', 'c2'], 'definition' => match ($this->driver) { - 'mysql' => '((`c1` > 0) and (`c2` < 0))', + 'mysql', 'mariadb' => '((`c1` > 0) and (`c2` < 0))', 'pgsql' => '(c1 > 0 AND c2 < 0)', 'sqlsrv' => '([c1]>(0) AND [c2]<(0))', default => '(c1 > 0 and c2 < 0)', }], $constraints); - if ($this->driver !== 'mysql') { + if (! in_array($this->driver, ['mysql', 'mariadb'])) { $this->assertContains(['name' => 'test_c3_check', 'columns' => ['c3'], 'definition' => match ($this->driver) { 'pgsql' => "(c3::text = ANY (ARRAY['foo'::character varying, 'bar'::character varying, 'baz'::character varying]::text[]))", 'sqlsrv' => "([c3]=N'baz' OR [c3]=N'bar' OR [c3]=N'foo')", @@ -609,25 +609,25 @@ public function testTableWithCheckConstraints() $constraints = Schema::getCheckConstraints('test'); - $this->assertCount($this->driver === 'mysql' ? 3 : 5, $constraints); + $this->assertCount(in_array($this->driver, ['mysql', 'mariadb']) ? 3 : 5, $constraints); $this->assertContains(['name' => 'test_c1_check', 'columns' => ['c1'], 'definition' => match ($this->driver) { - 'mysql' => '(`c1` > 100)', + 'mysql', 'mariadb' => '(`c1` > 100)', 'sqlsrv' => '([c1]>(100))', default => '(c1 > 100)', }], $constraints); $this->assertContains(['name' => 'unsigned_columns', 'columns' => ['c1', 'c4'], 'definition' => match ($this->driver) { - 'mysql' => '((`c1` > 0) and (`c4` > 0))', + 'mysql', 'mariadb' => '((`c1` > 0) and (`c4` > 0))', 'pgsql' => '(c1 > 0 AND c4 > 0)', 'sqlsrv' => '([c1]>(0) AND [c4]>(0))', default => '(c1 > 0 and c4 > 0)', }], $constraints); $this->assertContains(['name' => 'test_c4_check', 'columns' => ['c4'], 'definition' => match ($this->driver) { - 'mysql' => '(`c4` < 100)', + 'mysql', 'mariadb' => '(`c4` < 100)', 'sqlsrv' => '([c4]<(100))', default => '(c4 < 100)', }], $constraints); - if ($this->driver !== 'mysql') { + if (! in_array($this->driver, ['mysql', 'mariadb'])) { $this->assertContains(['name' => 'test_c3_check', 'columns' => ['c3'], 'definition' => match ($this->driver) { 'pgsql' => "(c3::text = ANY (ARRAY['foo'::character varying, 'bar'::character varying, 'baz'::character varying]::text[]))", 'sqlsrv' => "([c3]=N'baz' OR [c3]=N'bar' OR [c3]=N'foo')", diff --git a/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php b/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php index be2d6e745aa7..c6fb8c5b9621 100644 --- a/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php +++ b/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php @@ -140,7 +140,7 @@ public function testAlterTableAddForeignKeyWithExpressionDefault() public function testGetCheckConstraints() { - DB::statement(<< Date: Thu, 4 Sep 2025 19:41:06 +0330 Subject: [PATCH 5/7] fix mariadb test --- .../Query/Processors/MariaDbProcessor.php | 19 ++++++++++++++++++- .../Database/Query/Processors/Processor.php | 4 ++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Query/Processors/MariaDbProcessor.php b/src/Illuminate/Database/Query/Processors/MariaDbProcessor.php index 7a549d57fc1d..857e75a3f922 100644 --- a/src/Illuminate/Database/Query/Processors/MariaDbProcessor.php +++ b/src/Illuminate/Database/Query/Processors/MariaDbProcessor.php @@ -4,5 +4,22 @@ class MariaDbProcessor extends MySqlProcessor { - // + /** + * Process the results of a check constraints query. + * + * @param list> $results + * @return list, definition: string}> + */ + public function processCheckConstraints($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name ?? null, + 'columns' => explode(',', $result->columns ?? ''), + 'definition' => '('.$result->definition.')', + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index fa34f70eecfc..d018c48a53d4 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -154,9 +154,9 @@ public function processCheckConstraints($results) $result = (object) $result; return [ - 'name' => $result->name, + 'name' => $result->name ?? null, 'columns' => explode(',', $result->columns ?? ''), - 'definition' => $result->definition ?? null, + 'definition' => $result->definition, ]; }, $results); } From 76349d6655ca5a0f3becc5a49d74f9dae610518c Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 4 Sep 2025 19:55:43 +0330 Subject: [PATCH 6/7] fix mariadb tests --- .../Query/Processors/MariaDbProcessor.php | 19 +------------------ .../Database/SchemaBuilderTest.php | 15 ++++++++++----- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/Illuminate/Database/Query/Processors/MariaDbProcessor.php b/src/Illuminate/Database/Query/Processors/MariaDbProcessor.php index 857e75a3f922..7a549d57fc1d 100644 --- a/src/Illuminate/Database/Query/Processors/MariaDbProcessor.php +++ b/src/Illuminate/Database/Query/Processors/MariaDbProcessor.php @@ -4,22 +4,5 @@ class MariaDbProcessor extends MySqlProcessor { - /** - * Process the results of a check constraints query. - * - * @param list> $results - * @return list, definition: string}> - */ - public function processCheckConstraints($results) - { - return array_map(function ($result) { - $result = (object) $result; - - return [ - 'name' => $result->name ?? null, - 'columns' => explode(',', $result->columns ?? ''), - 'definition' => '('.$result->definition.')', - ]; - }, $results); - } + // } diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index 0d649c4230a8..dfdbdb8143c5 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -581,12 +581,14 @@ public function testTableWithCheckConstraints() $this->assertCount(in_array($this->driver, ['mysql', 'mariadb']) ? 2 : 3, $constraints); $this->assertContains(['name' => 'test_c1_check', 'columns' => ['c1'], 'definition' => match ($this->driver) { - 'mysql', 'mariadb' => '(`c1` > 100)', + 'mysql' => '(`c1` > 100)', + 'mariadb' => '`c1` > 100', 'sqlsrv' => '([c1]>(100))', default => '(c1 > 100)', }], $constraints); $this->assertContains(['name' => 'my_constraint', 'columns' => ['c1', 'c2'], 'definition' => match ($this->driver) { - 'mysql', 'mariadb' => '((`c1` > 0) and (`c2` < 0))', + 'mysql' => '((`c1` > 0) and (`c2` < 0))', + 'mariadb' => '`c1` > 0 and `c2` < 0', 'pgsql' => '(c1 > 0 AND c2 < 0)', 'sqlsrv' => '([c1]>(0) AND [c2]<(0))', default => '(c1 > 0 and c2 < 0)', @@ -611,18 +613,21 @@ public function testTableWithCheckConstraints() $this->assertCount(in_array($this->driver, ['mysql', 'mariadb']) ? 3 : 5, $constraints); $this->assertContains(['name' => 'test_c1_check', 'columns' => ['c1'], 'definition' => match ($this->driver) { - 'mysql', 'mariadb' => '(`c1` > 100)', + 'mysql' => '(`c1` > 100)', + 'mariadb' => '`c1` > 100', 'sqlsrv' => '([c1]>(100))', default => '(c1 > 100)', }], $constraints); $this->assertContains(['name' => 'unsigned_columns', 'columns' => ['c1', 'c4'], 'definition' => match ($this->driver) { - 'mysql', 'mariadb' => '((`c1` > 0) and (`c4` > 0))', + 'mysql' => '((`c1` > 0) and (`c4` > 0))', + 'mariadb' => '`c1` > 0 and `c4` > 0', 'pgsql' => '(c1 > 0 AND c4 > 0)', 'sqlsrv' => '([c1]>(0) AND [c4]>(0))', default => '(c1 > 0 and c4 > 0)', }], $constraints); $this->assertContains(['name' => 'test_c4_check', 'columns' => ['c4'], 'definition' => match ($this->driver) { - 'mysql', 'mariadb' => '(`c4` < 100)', + 'mysql' => '(`c4` < 100)', + 'mariadb' => '`c4` < 100', 'sqlsrv' => '([c4]<(100))', default => '(c4 < 100)', }], $constraints); From d9e95e06304130e47430e3bdef55950ecc27a5e4 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 5 Sep 2025 02:28:18 +0330 Subject: [PATCH 7/7] drop check by columns --- src/Illuminate/Database/Schema/Blueprint.php | 17 +++++---- .../Database/Schema/BlueprintState.php | 23 +++++++++++- .../Database/Schema/Grammars/Grammar.php | 35 +++++++++++++++++-- .../Schema/Grammars/SqlServerGrammar.php | 15 ++++++++ .../Database/SchemaBuilderTest.php | 31 ++++++++++++++++ 5 files changed, 112 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index 96aff17e19c7..a80ec62409fc 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Schema; use Closure; +use Illuminate\Contracts\Database\Query\Expression as ExpressionContract; use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Query\Expression; @@ -576,12 +577,12 @@ public function dropConstrainedForeignIdFor($model, $column = null) /** * Indicate that the given check constraint should be dropped. * - * @param string $name + * @param string|string[] $nameOrColumns * @return \Illuminate\Support\Fluent */ - public function dropCheck(string $name): Fluent + public function dropCheck(string|array $nameOrColumns): Fluent { - return $this->addCommand('dropCheck', ['constraint' => $name]); + return $this->addCommand('dropCheck', [(is_array($nameOrColumns) ? 'columns' : 'constraint') => $nameOrColumns]); } /** @@ -772,12 +773,16 @@ public function foreign($columns, $name = null) * Specify a check constraint for the table. * * @param \Illuminate\Contracts\Database\Query\Expression|string $expression - * @param string|null $name + * @param string|string[]|null $nameOrColumns * @return \Illuminate\Support\Fluent */ - public function check(Expression|string $expression, ?string $name = null): Fluent + public function check(ExpressionContract|string $expression, string|array|null $nameOrColumns = null): Fluent { - return $this->addCommand('check', ['expression' => $expression, 'constraint' => $name]); + if (is_array($nameOrColumns)) { + $nameOrColumns = $this->createIndexName('check', $nameOrColumns); + } + + return $this->addCommand('check', ['expression' => $expression, 'constraint' => $nameOrColumns]); } /** diff --git a/src/Illuminate/Database/Schema/BlueprintState.php b/src/Illuminate/Database/Schema/BlueprintState.php index 51fa62b5e9f2..7c98e47915f2 100644 --- a/src/Illuminate/Database/Schema/BlueprintState.php +++ b/src/Illuminate/Database/Schema/BlueprintState.php @@ -113,6 +113,7 @@ public function __construct(Blueprint $blueprint, Connection $connection) $this->checkConstraints = (new Collection($schema->getCheckConstraints($table)))->map(fn ($constraint) => new Fluent([ 'constraint' => $constraint['name'], + 'columns' => $constraint['columns'], 'expression' => new Expression(Str::unwrap($constraint['definition'], '(', ')')), ]))->all(); } @@ -214,6 +215,10 @@ public function update(Fluent $command) $foreignKey->columns = str_replace($command->from, $command->to, $foreignKey->columns); } + foreach ($this->checkConstraints as $checkConstraint) { + $checkConstraint->columns = str_replace($command->from, $command->to, $checkConstraint->columns); + } + break; case 'dropColumn': @@ -270,8 +275,24 @@ public function update(Fluent $command) break; case 'dropCheck': + if ($command->constraint) { + $this->checkConstraints = array_values( + array_filter($this->checkConstraints, fn ($constraint) => $constraint->constraint !== $command->constraint) + ); + + break; + } + + $commandColumns = $command->columns; + sort($commandColumns); + $this->checkConstraints = array_values( - array_filter($this->checkConstraints, fn ($constraint) => $constraint->constraint !== $command->constraint) + array_filter($this->checkConstraints, function ($constraint) use ($commandColumns) { + $constraintColumns = $constraint->columns; + sort($constraintColumns); + + return $constraintColumns !== $commandColumns; + }) ); break; diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index f4100334ccf3..a0748cbe9729 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -7,6 +7,7 @@ use Illuminate\Database\Concerns\CompilesJsonPaths; use Illuminate\Database\Grammar as BaseGrammar; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Collection; use Illuminate\Support\Fluent; use RuntimeException; @@ -313,9 +314,39 @@ public function compileCheck(Blueprint $blueprint, Fluent $command) */ public function compileDropCheck(Blueprint $blueprint, Fluent $command) { - return sprintf('alter table %s drop constraint %s', + if ($command->constraint) { + $command->constraints = [$command->constraint]; + $command->constraint = null; + + return $this->compileDropConstraints($blueprint, $command); + } + + $columns = $command->columns; + $command->columns = null; + sort($columns); + + $command->constraints = (new Collection($this->connection->getSchemaBuilder()->getCheckConstraints($blueprint->getTable()))) + ->filter(function (array $checkConstraint) use ($columns) { + sort($checkConstraint['columns']); + + return $checkConstraint['columns'] === $columns; + })->pluck('name')->all(); + + return $this->compileDropConstraints($blueprint, $command); + } + + /** + * Compile a drop constraints command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string|null + */ + public function compileDropConstraints(Blueprint $blueprint, Fluent $command) + { + return sprintf('alter table %s %s', $this->wrapTable($blueprint), - $this->wrap($command->constraint) + implode(', ', $this->prefixArray('drop constraint', $this->wrapArray($command->constraints))) ); } diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index 252ad483efc0..2a6d5397dd37 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -503,6 +503,21 @@ public function compileDropForeign(Blueprint $blueprint, Fluent $command) return "alter table {$this->wrapTable($blueprint)} drop constraint {$index}"; } + /** + * Compile a drop constraints command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string|null + */ + public function compileDropConstraints(Blueprint $blueprint, Fluent $command) + { + return sprintf('alter table %s drop constraint %s', + $this->wrapTable($blueprint), + implode(', ', $this->wrapArray($command->constraints)) + ); + } + /** * Compile a rename table command. * diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index dfdbdb8143c5..e46609143eb7 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -647,6 +647,37 @@ public function testTableWithCheckConstraints() } } + public function testDropCheckByColumns() + { + if ($this->driver === 'mysql' && version_compare(DB::getServerVersion(), '8.0.19', '<')) { + $this->markTestSkipped('This test requires MySQL >= 8.0.19'); + } + + Schema::create('test', function (Blueprint $table) { + $table->integer('c1')->check('c1 > 0'); + $table->integer('c2')->check('c2 > 0'); + $table->integer('c3')->check('c3 > 0'); + $table->integer('c4')->check('c4 > 0'); + + $table->check('c1 < 100', 'c1_less_than_100'); + $table->check('c3 < 100 and c2 < 100', ['c3', 'c2']); + $table->check('c4 < 100', 'c4_less_than_100'); + }); + + $constraints = Schema::getCheckConstraints('test'); + $this->assertCount(7, $constraints); + + Schema::table('test', function (Blueprint $table) { + $table->dropCheck(['c1']); + $table->dropCheck(['c2']); + $table->dropCheck(['c3']); + $table->dropCheck(['c2', 'c3']); + }); + + $constraints = Schema::getCheckConstraints('test'); + $this->assertCount(2, $constraints); + } + #[RequiresDatabase('mariadb')] public function testSystemVersionedTables() {