Skip to content

Conversation

hafezdivandari
Copy link
Contributor

@hafezdivandari hafezdivandari commented Sep 4, 2025

Closes #56523
Fixes #56151
Related to #51373, #45487
Previous attempts #46512, #46883, #56000, #41078

This PR adds support for inspecting, adding, and dropping check constraints on all database drivers (Yes, even SQLite!).

Why?

Check constraints are a valuable feature at the database layer, and adding support for them is now straightforward (after #51373). Managing check constraints manually can be cumbersome, especially in SQLite.

We currently rely on check constraints for enum column types (except on MySQL and MariaDB, which have a native enum type). Without proper support, modifying enum columns has been difficult. This PR addresses that by allowing developers to:

  • Inspect existing constraints
  • Drop specific constraints
  • Define new constraints, including adding values to enum lists by redefining/modifying the column

Usage

Create a table with check constraints

Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->enum('status', ['pending', 'processing', 'shipped']);
    $table->decimal('price')->check('price > 0');
    $table->integer('discount_percent')->check('discount_percent >= 0 and discount_percent <= 100');
    $table->timestamps();

    $table->check('price * (1 - discount_percent / 100.0) > 0', 'orders_price_discount_check');
});

Modify a table with check constraints

Schema::table('orders', function (Blueprint $table) {
    $table->dropCheck(['status']);
    $table->enum('status', ['pending', 'processing', 'shipped', 'delivered'])->change();
    $table->timestamp('delivered_at')->nullable()->check('delivered_at > created_at');
});

Drop check constraints of a table

Schema::table('orders', function (Blueprint $table) {
    $table->dropCheck(['status']);            // drop by columns, drops all check constraints of the "status" column.
    $table->dropCheck('orders_status_check'); // drop by name
});

Inspect check constraints of a table

use Illuminate\Support\Facades\Schema;

$constraints = Schema::getCheckConstraints('orders'); 

/*[
    ['name' => 'orders_status_check', 'columns' => ['status'], 'definition' => "(status in ('pending', 'processing', 'shipped', 'delivered'))"],
    ['name' => 'orders_price_check', 'columns' => ['price'], 'definition' => '(price > 0)'],
    ['name' => 'orders_price_discount_check', 'columns' => ['price', 'discount_percent'], '(price * (1 - discount_percent / 100.0) > 0)'],
    ['name' => 'orders_delivered_at_check', 'columns' => ['delivered_at', 'created_at'], 'definition' => '(delivered_at > created_at)'],
]*/
  • name (?string): Name of the constraint
  • columns (string[]): Array of constrained columns
  • definition (string): Constraint definition expression

Notes

  1. Similar to other implemented constraints (e.g., foreign keys, unique, etc.), check constraints are always added through separate commands rather than at the column level.

  2. A check constraint defined on a column (e.g. $table->integer('price')->check('price > 0')) will be assigned a default name following the same convention as other constraints: table_column_check.

    However, a check constraint defined at the table level (e.g. $table->check('price > 0')) will not receive a default name (hard to generate a random one using the expression!)

    It is highly recommended to assign explicit names to your constraints. You can do this:

    • by passing a string as the second argument :$table->check('price > 0', 'orders_price_check')
    • or column names as the second argument: $table->check('price > 0', ['price'])
  3. On PostgreSQL, SQL Server, and SQLite, the enum column type is defined as a string with a check constraint (i.e. $table->enum('foo', ['a', 'b', 'c']) was equivalent to foo varchar check (foo in ('a', 'b', 'c'))) . As a result, modifying or redefining an enum column was not previously supported on these DB drivers.

    This PR resolves that. You can now drop the previous check constraint of an enum column and redefine it as needed:

    Schema::table('orders', function (Blueprint $table) {
        $table->dropCheck(['status']);                           // drop the previous check constraint
        $table->enum('status', ['foo', 'bar', 'new'])->change(); // redefine the enum and add a new value to the allowed list
    });

    Previously, this check constraint of an enum column did not have a default name. As a result, PostgreSQL and SQL Server automatically assigned their own names to the constraint. This PR now explicitly assigns names (as explained in Note 2). The assigned name matches PostgreSQL’s behavior but differs from SQL Server’s default.

    Q: Why do I have to manually drop the constraint? Why doesn't the change modifier handle it automatically?
    A: As a rule of thumb, the change modifier only modifies column attributes (e.g., type, default, nullable, comment, etc.) and does not affect indexes, foreign keys, or check constraints. Therefore, constraints must be dropped manually when needed.

  4. You can drop check constraints either by name or by columns. Dropping by columns is possible thanks to the table inspection functionality:

    • When dropping check constraints by columns (e.g., $table->dropCheck(['c1'])), the command will drop every constraint that involves only c1. That means a constraints that include both c1 and c2 columns will not be dropped.
    • The order of columns does not matter when dropping check constraints by columns. For example, $table->dropCheck(['c1', 'c2']) and $table->dropCheck(['c2', 'c1']) are equivalent and will drop any constraints that have both of these columns only.
    • Although most databases do not allow duplicate constraint names, SQLite does. This means you can create multiple constraints with the same name in SQLite. Therefore, when dropping a constraint by name (e.g., $table->dropCheck('orders_price_check')), SQLite will drop all constraints with that name.

Copy link

github-actions bot commented Sep 4, 2025

Thanks for submitting a PR!

Note that draft PR's are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sqlite enum column check constraints are not preserved after adding foreign key to table
1 participant