Skip to content

Commit d752038

Browse files
authored
Add fluent Table API for check constraints (#1052)
* Add fluent Table API for check constraints Add addCheckConstraint() and dropCheckConstraint() methods to the Table class, allowing users to manage check constraints using the same fluent API pattern used for indexes and foreign keys. The adapter already had the underlying implementation, this adds: - AddCheckConstraint action class - DropCheckConstraint action class - Table::addCheckConstraint() method - Table::dropCheckConstraint() method - Table::hasCheckConstraint() method - Plan.php updated to handle check constraint actions - Unit tests for the new methods * docs: Fix check constraint API examples Update examples to use the correct API signature: - addCheckConstraint($expression, $options) not addCheckConstraint($name, $expression) - Replace non-existent checkConstraint() fluent builder with CheckConstraint object - Fix all examples throughout the check constraints section
1 parent 5abc573 commit d752038

7 files changed

Lines changed: 242 additions & 26 deletions

File tree

docs/en/writing-migrations.rst

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,7 +1547,7 @@ You can add a check constraint to a table using the ``addCheckConstraint()`` met
15471547
{
15481548
$table = $this->table('products');
15491549
$table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2])
1550-
->addCheckConstraint('price_positive', 'price > 0')
1550+
->addCheckConstraint('price > 0', ['name' => 'price_positive'])
15511551
->save();
15521552
}
15531553

@@ -1562,18 +1562,19 @@ You can add a check constraint to a table using the ``addCheckConstraint()`` met
15621562
}
15631563
}
15641564

1565-
The first argument is the constraint name, and the second is the SQL expression
1566-
that defines the constraint. The expression should evaluate to a boolean value.
1565+
The first argument is the SQL expression that defines the constraint. The expression
1566+
should evaluate to a boolean value. The second argument is an options array where
1567+
you can specify the constraint ``name``.
15671568

1568-
Using the CheckConstraint Fluent Builder
1569-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1569+
Using the CheckConstraint Object
1570+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15701571

1571-
For more complex scenarios, you can use the ``checkConstraint()`` method to get
1572-
a fluent builder::
1572+
For more complex scenarios, you can create a ``CheckConstraint`` object directly::
15731573

15741574
<?php
15751575

15761576
use Migrations\BaseMigration;
1577+
use Migrations\Db\Table\CheckConstraint;
15771578

15781579
class MyNewMigration extends BaseMigration
15791580
{
@@ -1586,14 +1587,10 @@ a fluent builder::
15861587
$table->addColumn('age', 'integer')
15871588
->addColumn('status', 'string', ['limit' => 20])
15881589
->addCheckConstraint(
1589-
$this->checkConstraint()
1590-
->setName('age_valid')
1591-
->setExpression('age >= 18 AND age <= 120')
1590+
new CheckConstraint('age_valid', 'age >= 18 AND age <= 120')
15921591
)
15931592
->addCheckConstraint(
1594-
$this->checkConstraint()
1595-
->setName('status_valid')
1596-
->setExpression("status IN ('active', 'inactive', 'pending')")
1593+
new CheckConstraint('status_valid', "status IN ('active', 'inactive', 'pending')")
15971594
)
15981595
->save();
15991596
}
@@ -1602,8 +1599,7 @@ a fluent builder::
16021599
Auto-Generated Constraint Names
16031600
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16041601

1605-
If you don't specify a constraint name, one will be automatically generated based
1606-
on the table name and expression hash::
1602+
If you don't specify a constraint name, one will be automatically generated::
16071603

16081604
<?php
16091605

@@ -1618,11 +1614,8 @@ on the table name and expression hash::
16181614
{
16191615
$table = $this->table('inventory');
16201616
$table->addColumn('quantity', 'integer')
1621-
// Name will be auto-generated like 'inventory_chk_a1b2c3d4'
1622-
->addCheckConstraint(
1623-
$this->checkConstraint()
1624-
->setExpression('quantity >= 0')
1625-
)
1617+
// Name will be auto-generated
1618+
->addCheckConstraint('quantity >= 0')
16261619
->save();
16271620
}
16281621
}
@@ -1647,8 +1640,8 @@ Check constraints can reference multiple columns and use complex SQL expressions
16471640
$table->addColumn('start_date', 'date')
16481641
->addColumn('end_date', 'date')
16491642
->addColumn('discount', 'decimal', ['precision' => 5, 'scale' => 2])
1650-
->addCheckConstraint('valid_date_range', 'end_date >= start_date')
1651-
->addCheckConstraint('valid_discount', 'discount BETWEEN 0 AND 100')
1643+
->addCheckConstraint('end_date >= start_date', ['name' => 'valid_date_range'])
1644+
->addCheckConstraint('discount BETWEEN 0 AND 100', ['name' => 'valid_discount'])
16521645
->save();
16531646
}
16541647
}
@@ -1674,7 +1667,7 @@ You can verify if a check constraint exists using the ``hasCheckConstraint()`` m
16741667
if ($exists) {
16751668
// do something
16761669
} else {
1677-
$table->addCheckConstraint('price_positive', 'price > 0')
1670+
$table->addCheckConstraint('price > 0', ['name' => 'price_positive'])
16781671
->save();
16791672
}
16801673
}
@@ -1708,7 +1701,7 @@ constraint name::
17081701
public function down(): void
17091702
{
17101703
$table = $this->table('products');
1711-
$table->addCheckConstraint('price_positive', 'price > 0')
1704+
$table->addCheckConstraint('price > 0', ['name' => 'price_positive'])
17121705
->save();
17131706
}
17141707
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* MIT License
6+
* For full license information, please view the LICENSE file that was distributed with this source code.
7+
*/
8+
9+
namespace Migrations\Db\Action;
10+
11+
use Migrations\Db\Table\CheckConstraint;
12+
use Migrations\Db\Table\TableMetadata;
13+
14+
class AddCheckConstraint extends Action
15+
{
16+
/**
17+
* The check constraint to add
18+
*
19+
* @var \Migrations\Db\Table\CheckConstraint
20+
*/
21+
protected CheckConstraint $checkConstraint;
22+
23+
/**
24+
* Constructor
25+
*
26+
* @param \Migrations\Db\Table\TableMetadata $table The table to add the check constraint to
27+
* @param \Migrations\Db\Table\CheckConstraint $checkConstraint The check constraint to add
28+
*/
29+
public function __construct(TableMetadata $table, CheckConstraint $checkConstraint)
30+
{
31+
parent::__construct($table);
32+
$this->checkConstraint = $checkConstraint;
33+
}
34+
35+
/**
36+
* Creates a new AddCheckConstraint object after building the check constraint with
37+
* the passed attributes
38+
*
39+
* @param \Migrations\Db\Table\TableMetadata $table The table object to add the check constraint to
40+
* @param string $expression The check constraint expression (e.g., "age >= 18")
41+
* @param array<string, mixed> $options Options for the check constraint (e.g., 'name')
42+
* @return self
43+
*/
44+
public static function build(
45+
TableMetadata $table,
46+
string $expression,
47+
array $options = [],
48+
): self {
49+
$name = $options['name'] ?? '';
50+
51+
$checkConstraint = new CheckConstraint($name, $expression);
52+
53+
return new AddCheckConstraint($table, $checkConstraint);
54+
}
55+
56+
/**
57+
* Returns the check constraint to be added
58+
*
59+
* @return \Migrations\Db\Table\CheckConstraint
60+
*/
61+
public function getCheckConstraint(): CheckConstraint
62+
{
63+
return $this->checkConstraint;
64+
}
65+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* MIT License
6+
* For full license information, please view the LICENSE file that was distributed with this source code.
7+
*/
8+
9+
namespace Migrations\Db\Action;
10+
11+
use Migrations\Db\Table\TableMetadata;
12+
13+
class DropCheckConstraint extends Action
14+
{
15+
/**
16+
* The check constraint name to drop
17+
*
18+
* @var string
19+
*/
20+
protected string $constraintName;
21+
22+
/**
23+
* Constructor
24+
*
25+
* @param \Migrations\Db\Table\TableMetadata $table The table to remove the constraint from
26+
* @param string $constraintName The name of the check constraint to drop
27+
*/
28+
public function __construct(TableMetadata $table, string $constraintName)
29+
{
30+
parent::__construct($table);
31+
$this->constraintName = $constraintName;
32+
}
33+
34+
/**
35+
* Returns the name of the check constraint to drop
36+
*
37+
* @return string
38+
*/
39+
public function getConstraintName(): string
40+
{
41+
return $this->constraintName;
42+
}
43+
}

src/Db/Adapter/AbstractAdapter.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Exception;
2424
use InvalidArgumentException;
2525
use Migrations\Config\Config;
26+
use Migrations\Db\Action\AddCheckConstraint;
2627
use Migrations\Db\Action\AddColumn;
2728
use Migrations\Db\Action\AddForeignKey;
2829
use Migrations\Db\Action\AddIndex;
@@ -32,6 +33,7 @@
3233
use Migrations\Db\Action\ChangePrimaryKey;
3334
use Migrations\Db\Action\CreateTrigger;
3435
use Migrations\Db\Action\CreateView;
36+
use Migrations\Db\Action\DropCheckConstraint;
3537
use Migrations\Db\Action\DropForeignKey;
3638
use Migrations\Db\Action\DropIndex;
3739
use Migrations\Db\Action\DropPartition;
@@ -1850,6 +1852,22 @@ public function executeActions(TableMetadata $table, array $actions): void
18501852
));
18511853
break;
18521854

1855+
case $action instanceof AddCheckConstraint:
1856+
/** @var \Migrations\Db\Action\AddCheckConstraint $action */
1857+
$instructions->merge($this->getAddCheckConstraintInstructions(
1858+
$table,
1859+
$action->getCheckConstraint(),
1860+
));
1861+
break;
1862+
1863+
case $action instanceof DropCheckConstraint:
1864+
/** @var \Migrations\Db\Action\DropCheckConstraint $action */
1865+
$instructions->merge($this->getDropCheckConstraintInstructions(
1866+
$table->getName(),
1867+
$action->getConstraintName(),
1868+
));
1869+
break;
1870+
18531871
default:
18541872
throw new InvalidArgumentException(
18551873
sprintf("Don't know how to execute action `%s`", get_class($action)),

src/Db/Plan/Plan.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace Migrations\Db\Plan;
1010

1111
use ArrayObject;
12+
use Migrations\Db\Action\AddCheckConstraint;
1213
use Migrations\Db\Action\AddColumn;
1314
use Migrations\Db\Action\AddForeignKey;
1415
use Migrations\Db\Action\AddIndex;
@@ -19,6 +20,7 @@
1920
use Migrations\Db\Action\CreateTable;
2021
use Migrations\Db\Action\CreateTrigger;
2122
use Migrations\Db\Action\CreateView;
23+
use Migrations\Db\Action\DropCheckConstraint;
2224
use Migrations\Db\Action\DropForeignKey;
2325
use Migrations\Db\Action\DropIndex;
2426
use Migrations\Db\Action\DropPartition;
@@ -497,15 +499,22 @@ protected function gatherIndexes(array $actions): void
497499
}
498500

499501
/**
500-
* Collects all foreign key creation and drops from the given intent
502+
* Collects all constraint creation and drops from the given intent
503+
*
504+
* This includes foreign keys and check constraints.
501505
*
502506
* @param \Migrations\Db\Action\Action[] $actions The actions to parse
503507
* @return void
504508
*/
505509
protected function gatherConstraints(array $actions): void
506510
{
507511
foreach ($actions as $action) {
508-
if (!($action instanceof AddForeignKey || $action instanceof DropForeignKey)) {
512+
if (
513+
!($action instanceof AddForeignKey)
514+
&& !($action instanceof DropForeignKey)
515+
&& !($action instanceof AddCheckConstraint)
516+
&& !($action instanceof DropCheckConstraint)
517+
) {
509518
continue;
510519
}
511520
$table = $action->getTable();

src/Db/Table.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Cake\Collection\Collection;
1212
use Cake\Core\Configure;
1313
use InvalidArgumentException;
14+
use Migrations\Db\Action\AddCheckConstraint;
1415
use Migrations\Db\Action\AddColumn;
1516
use Migrations\Db\Action\AddForeignKey;
1617
use Migrations\Db\Action\AddIndex;
@@ -21,6 +22,7 @@
2122
use Migrations\Db\Action\CreateTable;
2223
use Migrations\Db\Action\CreateTrigger;
2324
use Migrations\Db\Action\CreateView;
25+
use Migrations\Db\Action\DropCheckConstraint;
2426
use Migrations\Db\Action\DropForeignKey;
2527
use Migrations\Db\Action\DropIndex;
2628
use Migrations\Db\Action\DropPartition;
@@ -35,6 +37,7 @@
3537
use Migrations\Db\Adapter\MysqlAdapter;
3638
use Migrations\Db\Plan\Intent;
3739
use Migrations\Db\Plan\Plan;
40+
use Migrations\Db\Table\CheckConstraint;
3841
use Migrations\Db\Table\Column;
3942
use Migrations\Db\Table\ForeignKey;
4043
use Migrations\Db\Table\Index;
@@ -625,6 +628,50 @@ public function hasForeignKey(string|array $columns, ?string $constraint = null)
625628
return $this->getAdapter()->hasForeignKey($this->getName(), $columns, $constraint);
626629
}
627630

631+
/**
632+
* Add a check constraint to a database table.
633+
*
634+
* @param string|\Migrations\Db\Table\CheckConstraint $expression The check constraint expression or object
635+
* @param array<string, mixed> $options Options for the check constraint (e.g., 'name')
636+
* @return $this
637+
*/
638+
public function addCheckConstraint(string|CheckConstraint $expression, array $options = [])
639+
{
640+
if ($expression instanceof CheckConstraint) {
641+
$action = new AddCheckConstraint($this->table, $expression);
642+
} else {
643+
$action = AddCheckConstraint::build($this->table, $expression, $options);
644+
}
645+
$this->actions->addAction($action);
646+
647+
return $this;
648+
}
649+
650+
/**
651+
* Removes the given check constraint from the table.
652+
*
653+
* @param string $constraintName The name of the check constraint to drop
654+
* @return $this
655+
*/
656+
public function dropCheckConstraint(string $constraintName)
657+
{
658+
$action = new DropCheckConstraint($this->table, $constraintName);
659+
$this->actions->addAction($action);
660+
661+
return $this;
662+
}
663+
664+
/**
665+
* Checks to see if a check constraint exists.
666+
*
667+
* @param string $constraintName The name of the check constraint
668+
* @return bool
669+
*/
670+
public function hasCheckConstraint(string $constraintName): bool
671+
{
672+
return $this->getAdapter()->hasCheckConstraint($this->getName(), $constraintName);
673+
}
674+
628675
/**
629676
* Add partitioning to the table.
630677
*

0 commit comments

Comments
 (0)