Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/Illuminate/Database/Schema/Blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
30 changes: 30 additions & 0 deletions src/Illuminate/Database/Schema/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
55 changes: 53 additions & 2 deletions src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
}

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
154 changes: 154 additions & 0 deletions tests/Database/DatabaseSchemaBlueprintTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Illuminate\Database\Schema\Grammars\SqlServerGrammar;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use RuntimeException;

class DatabaseSchemaBlueprintTest extends TestCase
{
Expand Down Expand Up @@ -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);
}
}