diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index 7d1f0594cd55..a8e494a18102 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -9,6 +9,7 @@ use Illuminate\Database\Schema\Grammars\Grammar; use Illuminate\Database\SQLiteConnection; use Illuminate\Support\Fluent; +use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; class Blueprint @@ -445,6 +446,19 @@ public function dropConstrainedForeignIdFor($model, $column = null) return $this->dropConstrainedForeignId($column ?: $model->getForeignKey()); } + /** + * Indicate that the given check constraints should be dropped. + * + * @param string|array $constraints + * @return \Illuminate\Support\Fluent + */ + public function dropCheck($constraints) + { + $constraints = is_array($constraints) ? $constraints : func_get_args(); + + return $this->addCommand('dropCheck', compact('constraints')); + } + /** * Indicate that the given indexes should be renamed. * @@ -628,6 +642,35 @@ public function foreign($columns, $name = null) return $command; } + /** + * Specify a check constraint for the table. + * + * @param string $expression + * @param string|null $constraint + * @return \Illuminate\Support\Fluent + */ + public function check($expression, $constraint = null) + { + $constraint = $constraint ?: $this->createCheckName($expression); + + return $this->addCommand('check', compact('expression', 'constraint')); + } + + /** + * Create a default check constraint name for the table. + * + * @param string $expression + * @return string + */ + protected function createCheckName($expression) + { + return Str::of("{$this->prefix}{$this->table}_{$expression}_check") + ->replaceMatches('#[\W_]+#', '_') + ->trim('_') + ->lower() + ->value(); + } + /** * Create a new auto-incrementing big integer (8-byte) column on the table. * diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index ff2c455a1a5a..c3d670cdc799 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -151,6 +151,36 @@ public function compileForeign(Blueprint $blueprint, Fluent $command) return $sql; } + /** + * Compile a check constraint command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileCheck(Blueprint $blueprint, Fluent $command) + { + return sprintf('alter table %s add constraint %s check (%s)', + $this->wrapTable($blueprint), + $this->wrap($command->constraint), + $command->expression, + ); + } + + /** + * Compile a drop check constraint command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileDropCheck(Blueprint $blueprint, Fluent $command) + { + $constraints = $this->prefixArray('drop constraint', $this->wrapArray($command->constraints)); + + return 'alter table '.$this->wrapTable($blueprint).' '.implode(', ', $constraints); + } + /** * Compile the blueprint's column definitions. * diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 4abdf65d8563..b6ef39802682 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -55,12 +55,13 @@ public function compileColumnListing($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)), (string) $this->addForeignKeys($blueprint), - (string) $this->addPrimaryKeys($blueprint) + (string) $this->addPrimaryKeys($blueprint), + (string) $this->addChecks($blueprint), ); } @@ -126,6 +127,24 @@ protected function addPrimaryKeys(Blueprint $blueprint) } } + /** + * Get the check constraint syntax for a table creation statement. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @return string + */ + protected function addChecks(Blueprint $blueprint) + { + $commands = $this->getCommandsByName($blueprint, 'check'); + + return collect($commands) + ->map(fn ($commands) => sprintf(', constraint %s check (%s)', + $this->wrap($commands->constraint), + $commands->expression, + )) + ->join(''); + } + /** * Compile alter table commands for adding columns. * @@ -202,6 +221,38 @@ public function compileForeign(Blueprint $blueprint, Fluent $command) // Handled on table creation... } + /** + * Compile a check constraint command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + * + * @throws \RuntimeException + */ + public function compileCheck(Blueprint $blueprint, Fluent $command) + { + if (! $blueprint->creating()) { + throw new RuntimeException('This database driver does not support adding check constraints to existing tables.'); + } + + // Handled on table creation... + } + + /** + * Compile a drop check constraint command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + * + * @throws \RuntimeException + */ + public function compileDropCheck(Blueprint $blueprint, Fluent $command) + { + throw new RuntimeException('This database driver does not support dropping check constraints.'); + } + /** * Compile a drop table command. * diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index 1f14e8eee818..e2cee466ac81 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -11,6 +11,7 @@ use Illuminate\Database\Schema\Grammars\SqlServerGrammar; use Mockery as m; use PHPUnit\Framework\TestCase; +use RuntimeException; class DatabaseSchemaBlueprintTest extends TestCase { @@ -415,4 +416,157 @@ public function testTinyTextNullableColumn() 'alter table "posts" add "note" nvarchar(255) null', ], $blueprint->toSql($connection, new SqlServerGrammar)); } + + public function testCheckDefaultNames() + { + $blueprint = new Blueprint('events'); + $blueprint->check('date_end>=date_start'); + $commands = $blueprint->getCommands(); + $this->assertSame('events_date_end_date_start_check', $commands[0]->constraint); + + $blueprint = new Blueprint('events'); + $blueprint->check('date_end>date_start OR is_single_day=true'); + $commands = $blueprint->getCommands(); + $this->assertSame('events_date_end_date_start_or_is_single_day_true_check', $commands[0]->constraint); + + $blueprint = new Blueprint('users'); + $blueprint->check('(age < 21) OR (email IS NOT NULL)'); + $commands = $blueprint->getCommands(); + $this->assertSame('users_age_21_or_email_is_not_null_check', $commands[0]->constraint); + } + + public function testCreateTableWithChecks() + { + $connection = m::mock(Connection::class); + $connection->shouldReceive('getConfig')->once()->with('charset')->andReturn('utf8'); + $connection->shouldReceive('getConfig')->once()->with('collation')->andReturn('utf8_unicode_ci'); + $connection->shouldReceive('getConfig')->once()->with('engine')->andReturn(null); + + $base = new Blueprint('users'); + $base->create(); + $base->unsignedInteger('age'); + $base->check('age>21', 'min_age_check'); + + $blueprint = clone $base; + $this->assertEquals([ + 'create table `users` (`age` int unsigned not null) default character set utf8 collate \'utf8_unicode_ci\'', + 'alter table `users` add constraint `min_age_check` check (age>21)', + ], $blueprint->toSql($connection, new MySqlGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'create table "users" ("age" integer not null, constraint "min_age_check" check (age>21))', + ], $blueprint->toSql($connection, new SQLiteGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'create table "users" ("age" integer not null)', + 'alter table "users" add constraint "min_age_check" check (age>21)', + ], $blueprint->toSql($connection, new PostgresGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'create table "users" ("age" int not null)', + 'alter table "users" add constraint "min_age_check" check (age>21)', + ], $blueprint->toSql($connection, new SqlServerGrammar)); + } + + public function testAlterTableWithChecks() + { + $connection = m::mock(Connection::class); + + $base = new Blueprint('users'); + $base->check('age>21', 'min_age_check'); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table `users` add constraint `min_age_check` check (age>21)', + ], $blueprint->toSql($connection, new MySqlGrammar)); + + // SQLite does not support adding check constraints to existing tables. + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "users" add constraint "min_age_check" check (age>21)', + ], $blueprint->toSql($connection, new PostgresGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "users" add constraint "min_age_check" check (age>21)', + ], $blueprint->toSql($connection, new SqlServerGrammar)); + } + + public function testAlterTableWithChecksThrowsForSQLite() + { + $connection = m::mock(Connection::class); + + $base = new Blueprint('users'); + $base->check('age>21', 'min_age_check'); + + $this->expectException(RuntimeException::class); + + $base->toSql($connection, new SQLiteGrammar); + } + + public function testDropChecks() + { + $connection = m::mock(Connection::class); + + $base = new Blueprint('users'); + $base->dropCheck('min_age_check'); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table `users` drop constraint `min_age_check`', + ], $blueprint->toSql($connection, new MySqlGrammar)); + + // SQLite does not support dropping check constraints. + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "users" drop constraint "min_age_check"', + ], $blueprint->toSql($connection, new PostgresGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "users" drop constraint "min_age_check"', + ], $blueprint->toSql($connection, new SqlServerGrammar)); + } + + public function testDropMultipleChecks() + { + $connection = m::mock(Connection::class); + + $base = new Blueprint('users'); + $base->dropCheck('min_age_check', 'max_age_check'); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table `users` drop constraint `min_age_check`, drop constraint `max_age_check`', + ], $blueprint->toSql($connection, new MySqlGrammar)); + + // SQLite does not support dropping check constraints. + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "users" drop constraint "min_age_check", drop constraint "max_age_check"', + ], $blueprint->toSql($connection, new PostgresGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "users" drop constraint "min_age_check", drop constraint "max_age_check"', + ], $blueprint->toSql($connection, new SqlServerGrammar)); + } + + public function testDropChecksThrowsForSQLite() + { + $connection = m::mock(Connection::class); + + $base = new Blueprint('users'); + $base->dropCheck('min_age_check'); + + $this->expectException(RuntimeException::class); + + $base->toSql($connection, new SQLiteGrammar); + } }