diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index 00fe7c55e086..dc898a6b6d0f 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -158,12 +158,6 @@ protected function ensureCommandsAreValid(Connection $connection) "SQLite doesn't support multiple calls to dropColumn / renameColumn in a single modification." ); } - - if ($this->commandsNamed(['dropForeign'])->count() > 0) { - throw new BadMethodCallException( - "SQLite doesn't support dropping foreign keys (you would need to re-create the table)." - ); - } } } @@ -452,6 +446,28 @@ public function dropConstrainedForeignIdFor($model, $column = null) return $this->dropConstrainedForeignId($column ?: $model->getForeignKey()); } + /** + * Indicate that the given constraint should be dropped. + * + * @param string $name + * @return \Illuminate\Support\Fluent + */ + public function dropConstraint($name) + { + return $this->addCommand('dropConstraint', ['constraint' => $name]); + } + + /** + * Indicate that the given check constraint should be dropped. + * + * @param string $name + * @return \Illuminate\Support\Fluent + */ + public function dropCheck($name) + { + return $this->dropConstraint($name); + } + /** * Indicate that the given indexes should be renamed. * @@ -635,6 +651,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, $name = null) + { + 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/ColumnDefinition.php b/src/Illuminate/Database/Schema/ColumnDefinition.php index 51265ac4213e..52ee3530257e 100644 --- a/src/Illuminate/Database/Schema/ColumnDefinition.php +++ b/src/Illuminate/Database/Schema/ColumnDefinition.php @@ -10,12 +10,13 @@ * @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 (MySQL/PostgreSQL/SQL Server) * @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 * @method $this first() Place the column "first" in the table (MySQL) * @method $this from(int $startingValue) Set the starting value of an auto-incrementing field (MySQL / PostgreSQL) - * @method $this generatedAs(string|Expression $expression = null) Create a SQL compliant identity column (PostgreSQL) + * @method $this generatedAs(\Illuminate\Contracts\Database\Query\Expression|string $expression = null) Create a SQL compliant identity column (PostgreSQL) * @method $this index(string $indexName = null) Add an index * @method $this invisible() Specify that the column should be invisible to "SELECT *" (MySQL) * @method $this nullable(bool $value = true) Allow NULL values to be inserted into the column diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index 0ff439c1c6a1..e223646f29ff 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -161,6 +161,37 @@ 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%s check (%s)', + $this->wrapTable($blueprint), + $command->constraint ? ' constraint '.$this->wrap($command->constraint) : '', + $this->getValue($command->expression) + ); + } + + /** + * Compile a drop constraint command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileDropConstraint(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/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 124aeb7a2fed..55a96d00a023 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -17,7 +17,7 @@ class MySqlGrammar extends Grammar */ protected $modifiers = [ 'Unsigned', 'Charset', 'Collate', 'VirtualAs', 'StoredAs', 'Nullable', - 'Srid', 'Default', 'OnUpdate', 'Invisible', 'Increment', 'Comment', 'After', 'First', + 'Srid', 'Default', 'OnUpdate', 'Invisible', 'Increment', 'Comment', 'Check', 'After', 'First', ]; /** @@ -401,9 +401,7 @@ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) */ public function compileDropUnique(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap($command->index); - - return "alter table {$this->wrapTable($blueprint)} drop index {$index}"; + return $this->compileDropIndex($blueprint, $command); } /** @@ -1250,6 +1248,20 @@ protected function modifySrid(Blueprint $blueprint, Fluent $column) } } + /** + * Get the SQL for a check constraint column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyCheck(Blueprint $blueprint, Fluent $column) + { + if (! is_null($column->check)) { + return ' check ('.$this->getValue($column->check).')'; + } + } + /** * Wrap a single string in keyword identifiers. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index e01aa947fe35..8b876ef33300 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -22,7 +22,7 @@ class PostgresGrammar extends Grammar * * @var string[] */ - protected $modifiers = ['Collate', 'Nullable', 'Default', 'VirtualAs', 'StoredAs', 'GeneratedAs', 'Increment']; + protected $modifiers = ['Collate', 'Nullable', 'Check', 'Default', 'VirtualAs', 'StoredAs', 'GeneratedAs', 'Increment']; /** * The columns available as serials. @@ -427,9 +427,9 @@ public function compileDropColumn(Blueprint $blueprint, Fluent $command) */ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap("{$blueprint->getTable()}_pkey"); + $command->constraint = "{$blueprint->getTable()}_pkey"; - return 'alter table '.$this->wrapTable($blueprint)." drop constraint {$index}"; + return $this->compileDropConstraint($blueprint, $command); } /** @@ -441,9 +441,11 @@ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) */ public function compileDropUnique(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap($command->index); + $command->constraint = $command->index; - return "alter table {$this->wrapTable($blueprint)} drop constraint {$index}"; + $command->index = null; + + return $this->compileDropConstraint($blueprint, $command); } /** @@ -491,9 +493,11 @@ public function compileDropSpatialIndex(Blueprint $blueprint, Fluent $command) */ public function compileDropForeign(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap($command->index); + $command->constraint = $command->index; + + $command->index = null; - return "alter table {$this->wrapTable($blueprint)} drop constraint {$index}"; + return $this->compileDropConstraint($blueprint, $command); } /** @@ -785,11 +789,12 @@ protected function typeBoolean(Fluent $column) */ protected function typeEnum(Fluent $column) { - return sprintf( - 'varchar(255) check ("%s" in (%s))', - $column->name, + $column->check(sprintf('%s in (%s)', + $this->wrap($column), $this->quoteString($column->allowed) - ); + )); + + return 'varchar(255)'; } /** @@ -1216,4 +1221,18 @@ protected function modifyGeneratedAs(Blueprint $blueprint, Fluent $column) return $sql; } + + /** + * Get the SQL for a check constraint column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyCheck(Blueprint $blueprint, Fluent $column) + { + if (! $column->change && ! is_null($column->check)) { + return ' check ('.$this->getValue($column->check).')'; + } + } } diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 9c3b1c9fb61e..0f7576daa60a 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Schema\Grammars; +use BadMethodCallException; use Doctrine\DBAL\Schema\Index; use Illuminate\Database\Connection; use Illuminate\Database\Query\Expression; @@ -17,7 +18,7 @@ class SQLiteGrammar extends Grammar * * @var string[] */ - protected $modifiers = ['Increment', 'Nullable', 'Default', 'VirtualAs', 'StoredAs']; + protected $modifiers = ['Increment', 'Nullable', 'Check', 'Default', 'VirtualAs', 'StoredAs']; /** * The columns available as serials. @@ -56,12 +57,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->addCheckConstraints($blueprint), ); } @@ -127,6 +129,24 @@ protected function addPrimaryKeys(Blueprint $blueprint) } } + /** + * Get the check constraint syntax for a table creation statement. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @return string|null + */ + protected function addCheckConstraints(Blueprint $blueprint) + { + $constraints = $this->getCommandsByName($blueprint, 'check'); + + return collect($constraints)->reduce(function ($sql, $constraint) { + return $sql.sprintf(',%s check (%s)', + $constraint->constraint ? ' constraint '.$this->wrap($constraint->constraint) : '', + $this->getValue($constraint->expression) + ); + }, ''); + } + /** * Compile alter table commands for adding columns. * @@ -222,6 +242,18 @@ 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|null + */ + public function compileCheck(Blueprint $blueprint, Fluent $command) + { + // Handled on table creation... + } + /** * Compile a drop table command. * @@ -337,9 +369,7 @@ public function compileDropColumn(Blueprint $blueprint, Fluent $command, Connect */ public function compileDropUnique(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap($command->index); - - return "drop index {$index}"; + return $this->compileDropIndex($blueprint, $command); } /** @@ -370,6 +400,32 @@ public function compileDropSpatialIndex(Blueprint $blueprint, Fluent $command) throw new RuntimeException('The database driver in use does not support spatial indexes.'); } + /** + * Compile a drop foreign key command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileDropForeign(Blueprint $blueprint, Fluent $command) + { + throw new BadMethodCallException( + "SQLite doesn't support dropping foreign keys (you would need to re-create the table)." + ); + } + + /** + * Compile a drop constraint command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileDropConstraint(Blueprint $blueprint, Fluent $command) + { + throw new RuntimeException('This database driver does not support dropping constraints.'); + } + /** * Compile a rename table command. * @@ -632,11 +688,12 @@ protected function typeBoolean(Fluent $column) */ protected function typeEnum(Fluent $column) { - return sprintf( - 'varchar check ("%s" in (%s))', - $column->name, + $column->check(sprintf('%s in (%s)', + $this->wrap($column), $this->quoteString($column->allowed) - ); + )); + + return 'varchar'; } /** @@ -995,6 +1052,20 @@ protected function modifyIncrement(Blueprint $blueprint, Fluent $column) } } + /** + * Get the SQL for a check constraint column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyCheck(Blueprint $blueprint, Fluent $column) + { + if (! is_null($column->check)) { + return ' check ('.$this->getValue($column->check).')'; + } + } + /** * Wrap the given JSON selector. * diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index a2b56afacf61..de3cc9cb49f8 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -21,7 +21,7 @@ class SqlServerGrammar extends Grammar * * @var string[] */ - protected $modifiers = ['Collate', 'Nullable', 'Default', 'Persisted', 'Increment']; + protected $modifiers = ['Collate', 'Nullable', 'Default', 'Persisted', 'Increment', 'Check']; /** * The columns available as serials. @@ -339,9 +339,11 @@ public function compileDropDefaultConstraint(Blueprint $blueprint, Fluent $comma */ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap($command->index); + $command->constraint = $command->index; - return "alter table {$this->wrapTable($blueprint)} drop constraint {$index}"; + $command->index = null; + + return $this->compileDropConstraint($blueprint, $command); } /** @@ -353,9 +355,7 @@ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) */ public function compileDropUnique(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap($command->index); - - return "drop index {$index} on {$this->wrapTable($blueprint)}"; + return $this->compileDropIndex($blueprint, $command); } /** @@ -670,11 +670,12 @@ protected function typeBoolean(Fluent $column) */ protected function typeEnum(Fluent $column) { - return sprintf( - 'nvarchar(255) check ("%s" in (%s))', - $column->name, + $column->check(sprintf('%s in (%s)', + $this->wrap($column), $this->quoteString($column->allowed) - ); + )); + + return 'nvarchar(255)'; } /** @@ -1018,6 +1019,20 @@ protected function modifyPersisted(Blueprint $blueprint, Fluent $column) } } + /** + * Get the SQL for a check constraint column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyCheck(Blueprint $blueprint, Fluent $column) + { + if (! $column->change && ! is_null($column->check)) { + return ' check ('.$this->getValue($column->check).')'; + } + } + /** * Wrap a table in keyword identifiers. * diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index 8d0a942ef9eb..5270b16c2cf5 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -638,7 +638,7 @@ public function testAddingEnum() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $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 "role" varchar(255) not null check ("role" in (\'member\', \'admin\'))', $statements[0]); } public function testAddingDate() diff --git a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php index 1dfd63c54fa7..9ddd6cea4f91 100755 --- a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php +++ b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php @@ -497,7 +497,7 @@ public function testAddingEnum() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $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 "role" varchar not null check ("role" in (\'member\', \'admin\'))', $statements[0]); } public function testAddingJson() diff --git a/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php b/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php index 51d0737c89e3..11a2cd0096f8 100644 --- a/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php +++ b/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php @@ -5,6 +5,7 @@ use BadMethodCallException; use Illuminate\Container\Container; use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Grammars\MySqlGrammar; use Illuminate\Database\Schema\Grammars\PostgresGrammar; @@ -591,6 +592,117 @@ public function testDropIndexOnColumnChangeWorks() ); } + public function testAddCheckConstraintWorks() + { + $connection = $this->db->connection(); + + $blueprint = new Blueprint('users', function (Blueprint $table) { + $table->create(); + $table->integer('c1'); + $table->check('c1 > 10'); + $table->check('c1 > 10', 'foo'); + $table->check(new Expression('c1 > 10')); + $table->check(new Expression('c1 > 10'), 'foo'); + }); + + $this->assertEquals([ + 'create table `users` (`c1` int not null)', + '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)', + ], $blueprint->toSql($connection, new MySqlGrammar)); + + $this->assertEquals([ + 'create table "users" ("c1" integer not null)', + '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)', + ], $blueprint->toSql($connection, new PostgresGrammar)); + + $this->assertEquals([ + 'create table "users" ("c1" integer not null, '. + 'check (c1 > 10), '. + 'constraint "foo" check (c1 > 10), '. + 'check (c1 > 10), '. + 'constraint "foo" check (c1 > 10))', + ], $blueprint->toSql($connection, new SQLiteGrammar)); + + $this->assertEquals([ + 'create table "users" ("c1" int not null)', + '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)', + ], $blueprint->toSql($connection, new SqlServerGrammar)); + } + + public function testDropCheckConstraintWorks() + { + $connection = $this->db->connection(); + + $blueprint = new Blueprint('users', function (Blueprint $table) { + $table->dropCheck('foo'); + }); + + $this->assertEquals( + ['alter table `users` drop constraint `foo`'], + $blueprint->toSql($connection, new MySqlGrammar) + ); + + $this->assertEquals( + ['alter table "users" drop constraint "foo"'], + $blueprint->toSql($connection, new PostgresGrammar) + ); + + $this->assertEquals( + ['alter table "users" drop constraint "foo"'], + $blueprint->toSql($connection, new SqlServerGrammar) + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('This database driver does not support dropping constraints.'); + $blueprint->toSql($connection, new SQLiteGrammar); + } + + public function testCheckConstraintOnColumnsWorks() + { + $connection = $this->db->connection(); + + $base = new Blueprint('users', function (Blueprint $table) { + $table->integer('c1')->check('c1 > 10'); + $table->integer('c2')->check(new Expression('c2 > 10')); + }); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table `users` '. + 'add `c1` int not null check (c1 > 10), '. + 'add `c2` int not null check (c2 > 10)', + ], $blueprint->toSql($connection, new MySqlGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "users" '. + 'add column "c1" integer not null check (c1 > 10), '. + 'add column "c2" integer not null check (c2 > 10)', + ], $blueprint->toSql($connection, new PostgresGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "users" add column "c1" integer not null check (c1 > 10)', + 'alter table "users" add column "c2" integer not null check (c2 > 10)', + ], $blueprint->toSql($connection, new SQLiteGrammar)); + + $blueprint = clone $base; + $this->assertEquals([ + 'alter table "users" add '. + '"c1" int not null check (c1 > 10), '. + '"c2" int not null check (c2 > 10)', + ], $blueprint->toSql($connection, new SqlServerGrammar)); + } + public function testItEnsuresDroppingMultipleColumnsIsAvailable() { $this->expectException(BadMethodCallException::class); diff --git a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php index 40c0ff6ba61a..5a9c62eaca05 100755 --- a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php +++ b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php @@ -542,7 +542,7 @@ public function testAddingEnum() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $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 "role" nvarchar(255) not null check ("role" in (N\'member\', N\'admin\'))', $statements[0]); } public function testAddingJson()