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
47 changes: 47 additions & 0 deletions src/Illuminate/Database/Schema/Blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ class Blueprint
*/
protected $commands = [];

/**
* All defined check constraints for the table.
*
* @var \Illuminate\Database\Schema\CheckConstraintBuilder[]
*/
protected array $checkConstraints = [];

/**
* The storage engine that should be used for the table.
*
Expand Down Expand Up @@ -1903,4 +1910,44 @@ protected function defaultTimePrecision(): ?int
{
return $this->connection->getSchemaBuilder()::$defaultTimePrecision;
}

/**
* Add a new check constraint to the blueprint.
*
* @param string $name
* @param Closure $callback
* @return void
*/
public function checkConstraint(string $name, Closure $callback)
{
$builder = new CheckConstraintBuilder($name);
$callback($builder);

$this->checkConstraints[] = $builder;

if (! $this->creating()) {
$this->addCommand('addCheckConstraint', compact('builder'));
}
}

/**
* Get the check constraints.
*
* @return CheckConstraintBuilder[]
*/
public function getCheckConstraints(): array
{
return $this->checkConstraints;
}

/**
* Drop a constraint.
*
* @param string $name
* @return void
*/
public function dropConstraint(string $name): void
{
$this->addCommand('dropConstraint')->set('constraintName', $name);
}
}
191 changes: 191 additions & 0 deletions src/Illuminate/Database/Schema/CheckConstraintBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php

namespace Illuminate\Database\Schema;

use Closure;
class CheckConstraintBuilder
{
/**
* The constraint groups.
*
* @var array<int, array<string, string>>
*/
protected array $groups = [];

/**
* The constraint expressions.
*
* @var array<int, string>
*/
protected array $expressions = [];

/**
* @param string $name
*/
public function __construct(
protected string $name,
) {

}

/**
* Get the constraint name.
*
* @return string
*/
public function name(): string
{
return $this->name;
}

/**
* Add a new constraint expression and group it with an AND operator.
*
* @param string $column
* @param string $operator
* @param mixed $value
* @param Closure $callback
* @return $this
*/
public function where(string $column, string $operator, mixed $value, Closure $callback): static
{
return $this->addGroup('AND', $column, $operator, $value, $callback);
}

/**
* Add a new constraint expression and group it with an OR operator.
*
* @param string $column
* @param string $operator
* @param mixed $value
* @param Closure $callback
* @return $this
*/
public function orWhere(string $column, string $operator, mixed $value, Closure $callback): static
{
return $this->addGroup('OR', $column, $operator, $value, $callback);
}

/**
* Add a new constraint expression and group it with the given glue operator.
*
* @param string $glue
* @param string $column
* @param string $operator
* @param mixed $value
* @param Closure $callback
* @return $this
*/
protected function addGroup(string $glue, string $column, string $operator, mixed $value, Closure $callback): static
{
$groupBuilder = new static('_inline_'); // temp inner builder
$callback($groupBuilder);

$this->groups[] = [
'glue' => $glue,
'condition' => "$column $operator ".$this->quote($value),
'expressions' => $groupBuilder->expressions,
];

return $this;
}

/**
* Add a new constraint expression to the builder.
*
* @param string $column
* @param array $values
* @return $this
*/
public function whereNotIn(string $column, array $values): static
{
$quoted = implode(', ', array_map([$this, 'quote'], $values));
$this->expressions[] = "$column NOT IN ($quoted)";

return $this;
}

/**
* Add a new constraint expression to the builder.
*
* @param string $column
* @param array $values
* @return $this
*/
public function whereIn(string $column, array $values): static
{
$quoted = implode(', ', array_map([$this, 'quote'], $values));
$this->expressions[] = "$column IN ($quoted)";

return $this;
}

/**
* Compile the check constraint to SQL.
*
* @return string
*/
public function toSql(): string
{
$parts = [];

// Handle groups (from where/orWhere)
foreach ($this->groups as $index => $group) {
$expr = implode(' AND ', $group['expressions']);
$logic = $index > 0 ? $group['glue'].' ' : '';
$parts[] = $logic."({$group['condition']} AND {$expr})";
}

// Handle standalone expressions (from whereIn, whereNotIn, rule)
if (! empty($this->expressions) && empty($this->groups)) {
$parts[] = implode(' AND ', $this->expressions);
}

return 'CHECK ('.implode(' ', $parts).')';
}

/**
* Quote the given value.
*
* @param mixed $value
* @return string
*/
protected function quote(mixed $value): string
{
if (is_string($value)) {
return "'".str_replace("'", "''", $value)."'";
}

if (is_bool($value)) {
return $value ? 'TRUE' : 'FALSE';
}

if (is_null($value)) {
return 'NULL';
}

return (string) $value;
}

/**
* Add a new rule to the constraint.
*
* @param string $column
* @param string $operator
* @param mixed|null $value
* @return $this
*/
public function rule(string $column, string $operator, mixed $value = null): static
{
$operator = strtoupper(trim($operator));
$expression = "$column $operator";

if (! str_ends_with($operator, 'NULL')) {
$expression .= ' '.$this->quote($value);
}

$this->expressions[] = $expression;

return $this;
}
}
52 changes: 52 additions & 0 deletions src/Illuminate/Database/Schema/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Database\Concerns\CompilesJsonPaths;
use Illuminate\Database\Grammar as BaseGrammar;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\CheckConstraintBuilder;
use Illuminate\Support\Fluent;
use RuntimeException;

Expand Down Expand Up @@ -505,4 +506,55 @@ public function supportsSchemaTransactions()
{
return $this->transactions;
}

/**
* Build the constraints for the table.
*
* @param Blueprint $blueprint
* @return string[]
*/
protected function getCheckConstraints(Blueprint $blueprint): array
{
return collect($blueprint->getCheckConstraints())
->map(fn (CheckConstraintBuilder $builder) => "CONSTRAINT {$builder->name()} {$builder->toSql()}")
->all();
}

/**
* Compile a drop constraint command.
*
* @param Blueprint $blueprint
* @param Fluent $command
* @return string[]
*/
public function compileAddCheckConstraint(Blueprint $blueprint, Fluent $command): array
{
$sql = [];
foreach ($blueprint->getCheckConstraints() as $constraint) {
$sql[] = sprintf(
'ALTER TABLE %s ADD CONSTRAINT %s %s',
$this->wrapTable($blueprint),
$this->wrap($constraint->name()),
$constraint->toSql()
);
}

return $sql;
}

/**
* Compile a drop constraint command.
*
* @param Blueprint $blueprint
* @param Fluent $command
* @return string
*/
public function compileDropConstraint(Blueprint $blueprint, Fluent $command): string
{
return sprintf(
'ALTER TABLE %s DROP CONSTRAINT %s',
$this->wrapTable($blueprint),
$this->wrap($command->get('constraintName'))
);
}
}
49 changes: 48 additions & 1 deletion src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,10 @@ public function compileCreate(Blueprint $blueprint, Fluent $command)
*/
protected function compileCreateTable($blueprint, $command)
{
$tableStructure = $this->getColumns($blueprint);
$tableStructure = array_merge(
$this->getColumns($blueprint),
$this->getCheckConstraints($blueprint)
);

if ($primaryKey = $this->getCommandByName($blueprint, 'primary')) {
$tableStructure[] = sprintf(
Expand Down Expand Up @@ -1390,4 +1393,48 @@ protected function wrapJsonSelector($value)

return 'json_unquote(json_extract('.$field.$path.'))';
}

/**
* Check version compatibility before calling main handler.
*
* @throws RuntimeException
*/
private function checkCheckConstraintCompatibility(): void
{
$isMaria = $this->connection->isMaria();
$version = $this->connection->getServerVersion();

$isCompatible = $isMaria
? version_compare($version, '10.2.1', '>=')
: version_compare($version, '8.0.16', '>=');

if (! $isCompatible) {
$driverWithVersion = $isMaria ? 'MariaDB 10.2.1' : 'MySQL 8.0.16';
throw new RuntimeException("$driverWithVersion or higher is required to use check constraints.");
}
}

/**
* {@inheritDoc}
*
* @throws RuntimeException
*/
public function compileAddCheckConstraint(Blueprint $blueprint, Fluent $command): array
{
$this->checkCheckConstraintCompatibility();

return parent::compileAddCheckConstraint($blueprint, $command);
}

/**
* {@inheritDoc}
*
* @throws RuntimeException
*/
public function compileDropConstraint(Blueprint $blueprint, Fluent $command): string
{
$this->checkCheckConstraintCompatibility();

return parent::compileDropConstraint($blueprint, $command);
}
}
7 changes: 6 additions & 1 deletion src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,15 @@ public function compileForeignKeys($schema, $table)
*/
public function compileCreate(Blueprint $blueprint, Fluent $command)
{
$columns = array_merge(
$this->getColumns($blueprint),
$this->getCheckConstraints($blueprint)
);

return sprintf('%s table %s (%s)',
$blueprint->temporary ? 'create temporary' : 'create',
$this->wrapTable($blueprint),
implode(', ', $this->getColumns($blueprint))
implode(', ', $columns)
);
}

Expand Down
Loading