diff --git a/docs/en/advanced/integration-and-deployment.md b/docs/en/advanced/integration-and-deployment.md
index 35be96e6..7b1f57b9 100644
--- a/docs/en/advanced/integration-and-deployment.md
+++ b/docs/en/advanced/integration-and-deployment.md
@@ -192,7 +192,7 @@ columns not existing when performing operations on those new columns. The
CakePHP core includes a [Schema Cache Shell](https://book.cakephp.org/5/en/console-and-shells/schema-cache.html) that you can use:
```bash
-bin/cake migration migrate
+bin/cake migrations migrate
bin/cake schema_cache clear
```
diff --git a/docs/en/getting-started/creating-migrations.md b/docs/en/getting-started/creating-migrations.md
index f99f2608..7f8b8e05 100644
--- a/docs/en/getting-started/creating-migrations.md
+++ b/docs/en/getting-started/creating-migrations.md
@@ -45,6 +45,54 @@ You can also use the `underscore_form` as the name for your migrations, such as
> migrations if the class names are not unique. In that case, you may need to
> rename the migration manually.
+## Anonymous Migration Classes
+
+Migrations also supports generating anonymous migration classes, which use PHP's
+anonymous class feature instead of named classes. This style is useful for:
+
+- Avoiding namespace declarations
+- Better PHPCS compatibility (no class name to filename matching required)
+- Simpler file structure without named class constraints
+- More readable filenames like `2024_12_08_120000_CreateProducts.php`
+
+To generate an anonymous migration class, use the `--style anonymous` option:
+
+```bash
+bin/cake bake migration CreateProducts --style anonymous
+```
+
+This generates a migration file using an anonymous class:
+
+```php
+ [
+ 'style' => 'anonymous', // or 'traditional'
+],
+```
+
+This configuration also applies to seeds, allowing you to use consistent
+styling across your entire project.
+
## Creating a Table
You can use `bake migration` to create a table:
diff --git a/docs/en/guides/seeding.md b/docs/en/guides/seeding.md
index f1914aad..6ceb8282 100644
--- a/docs/en/guides/seeding.md
+++ b/docs/en/guides/seeding.md
@@ -509,7 +509,7 @@ The method takes three arguments:
## Truncating Tables
In addition to inserting data Migrations makes it trivial to empty your tables using the
-SQL TRUNCATE command:
+SQL `TRUNCATE` command:
```php
-v parameter for more output verbosity:
+You can also use the `-v` parameter for more output verbosity:
```bash
bin/cake seeds run -v
diff --git a/docs/en/guides/using-the-query-builder.md b/docs/en/guides/using-the-query-builder.md
index 931c2d7d..1f65cbc1 100644
--- a/docs/en/guides/using-the-query-builder.md
+++ b/docs/en/guides/using-the-query-builder.md
@@ -7,7 +7,7 @@ Migrations provides access to a Query builder object, that you may use to execut
The Query builder is provided by the [cakephp/database](https://github.com/cakephp/database) project, and should
be easy to work with as it resembles very closely plain SQL. Accesing the query builder is done by calling the
-`getQueryBuilder(string $type)` function. The `string $type` options are 'select', 'insert', 'update' and \`'delete'\`:
+`getQueryBuilder(string $type)` function. The `string $type` options are `'select'`, `'insert'`, `'update'` and `'delete'`:
```php
[!NOTE]
+> Partition columns must be included in the primary key for MySQL. SQLite does
+> not support partitioning. MySQL's `RANGE` and `LIST` types only work with
+> integer columns - use `RANGE COLUMNS` and `LIST COLUMNS` for DATE/STRING
+> columns.
+
+### RANGE Partitioning
+
+RANGE partitioning is useful when you want to partition by numeric ranges. For
+MySQL, use `TYPE_RANGE` with integer columns or expressions, and
+`TYPE_RANGE_COLUMNS` for DATE/DATETIME/STRING columns:
+
+```php
+table('orders', [
+ 'id' => false,
+ 'primary_key' => ['id', 'order_date'],
+ ]);
+ $table->addColumn('id', 'integer', ['identity' => true])
+ ->addColumn('order_date', 'date')
+ ->addColumn('amount', 'decimal', ['precision' => 10, 'scale' => 2])
+ ->partitionBy(Partition::TYPE_RANGE_COLUMNS, 'order_date')
+ ->addPartition('p2022', '2023-01-01')
+ ->addPartition('p2023', '2024-01-01')
+ ->addPartition('p2024', '2025-01-01')
+ ->addPartition('pmax', 'MAXVALUE')
+ ->create();
+ }
+}
+```
+
+### LIST Partitioning
+
+LIST partitioning is useful when you want to partition by discrete values. For
+MySQL, use `TYPE_LIST` with integer columns and `TYPE_LIST_COLUMNS` for STRING
+columns:
+
+```php
+table('customers', [
+ 'id' => false,
+ 'primary_key' => ['id', 'region'],
+ ]);
+ $table->addColumn('id', 'integer', ['identity' => true])
+ ->addColumn('region', 'string', ['limit' => 20])
+ ->addColumn('name', 'string')
+ ->partitionBy(Partition::TYPE_LIST_COLUMNS, 'region')
+ ->addPartition('p_americas', ['US', 'CA', 'MX', 'BR'])
+ ->addPartition('p_europe', ['UK', 'DE', 'FR', 'IT'])
+ ->addPartition('p_asia', ['JP', 'CN', 'IN', 'KR'])
+ ->create();
+ }
+}
+```
+
+### HASH Partitioning
+
+HASH partitioning distributes data evenly across a specified number of
+partitions:
+
+```php
+table('sessions');
+ $table->addColumn('user_id', 'integer')
+ ->addColumn('data', 'text')
+ ->partitionBy(Partition::TYPE_HASH, 'user_id', ['count' => 8])
+ ->create();
+ }
+}
+```
-- `RANGE`
-- `RANGE COLUMNS`
-- `LIST`
-- `LIST COLUMNS`
-- `HASH`
-- `KEY` *(MySQL only)*
+### KEY Partitioning (MySQL only)
-You can also partition by expressions using `Literal::from(...)`, and add or
-drop partitions on existing tables.
+KEY partitioning is similar to HASH but uses MySQL's internal hashing function:
+
+```php
+table('cache', [
+ 'id' => false,
+ 'primary_key' => ['cache_key'],
+ ]);
+ $table->addColumn('cache_key', 'string', ['limit' => 255])
+ ->addColumn('value', 'binary')
+ ->partitionBy(Partition::TYPE_KEY, 'cache_key', ['count' => 16])
+ ->create();
+ }
+}
+```
+
+### Partitioning with Expressions
+
+You can partition by expressions using the `Literal` class:
+
+```php
+table('events', [
+ 'id' => false,
+ 'primary_key' => ['id', 'created_at'],
+ ]);
+ $table->addColumn('id', 'integer', ['identity' => true])
+ ->addColumn('created_at', 'datetime')
+ ->partitionBy(Partition::TYPE_RANGE, Literal::from('YEAR(created_at)'))
+ ->addPartition('p2022', 2023)
+ ->addPartition('p2023', 2024)
+ ->addPartition('pmax', 'MAXVALUE')
+ ->create();
+ }
+}
+```
+
+### Modifying Partitions on Existing Tables
+
+You can add or drop partitions on existing partitioned tables:
+
+```php
+table('orders')
+ ->addPartitionToExisting('p2025', '2026-01-01')
+ ->update();
+ }
+
+ public function down(): void
+ {
+ $this->table('orders')
+ ->dropPartition('p2025')
+ ->update();
+ }
+}
+```
## Saving Changes
diff --git a/docs/en/guides/writing-migrations/indexes-and-constraints.md b/docs/en/guides/writing-migrations/indexes-and-constraints.md
index 5c7a680d..3ab9115c 100644
--- a/docs/en/guides/writing-migrations/indexes-and-constraints.md
+++ b/docs/en/guides/writing-migrations/indexes-and-constraints.md
@@ -2,11 +2,10 @@
## Working With Indexes
-To add an index to a table, call `addIndex()`:
+To add an index to a table, call `addIndex()` on the Table object:
```php
table('users')
+ ->addColumn('email', 'string')
+ ->addColumn('username', 'string')
+ ->addIndex(['email', 'username'], [
+ 'unique' => true,
+ 'name' => 'idx_users_email',
+ 'order' => ['email' => 'DESC', 'username' => 'ASC'],
+ ])
+ ->save();
+```
-The fluent builder is also available:
+As of 4.6.0, you can use `BaseMigration::index()` to get a fluent builder:
```php
$this->table('users')
@@ -37,102 +49,334 @@ $this->table('users')
->save();
```
-Adapter-specific capabilities include:
+### MySQL Fulltext Indexes
+
+The MySQL adapter supports `fulltext` indexes. If you are using a version
+before 5.6 the table must use the `MyISAM` engine:
+
+```php
+$table = $this->table('users', ['engine' => 'MyISAM']);
+$table->addColumn('email', 'string')
+ ->addIndex('email', ['type' => 'fulltext'])
+ ->create();
+```
+
+### MySQL Index Length
+
+The MySQL adapter supports setting the index length via the `limit` option.
+For multi-column indexes you can define the length per column:
+
+```php
+$this->table('users')
+ ->addColumn('email', 'string')
+ ->addColumn('username', 'string')
+ ->addColumn('user_guid', 'string', ['limit' => 36])
+ ->addIndex(['email', 'username'], ['limit' => ['email' => 5, 'username' => 2]])
+ ->addIndex('user_guid', ['limit' => 6])
+ ->create();
+```
+
+### Include Columns (SQL Server and PostgreSQL)
+
+The SQL Server and PostgreSQL adapters support `include` (non-key) columns on
+indexes:
+
+```php
+$this->table('users')
+ ->addColumn('email', 'string')
+ ->addColumn('firstname', 'string')
+ ->addColumn('lastname', 'string')
+ ->addIndex(['email'], ['include' => ['firstname', 'lastname']])
+ ->create();
+```
+
+### Partial Indexes
+
+PostgreSQL, SQL Server, and SQLite support partial indexes via a `WHERE`
+clause:
-- MySQL `fulltext` indexes
-- MySQL index-length options
-- SQL Server and PostgreSQL `include` columns
-- PostgreSQL, SQL Server, and SQLite partial indexes
-- PostgreSQL concurrent index creation
-- PostgreSQL `gin` indexes
+```php
+$this->table('users')
+ ->addColumn('email', 'string')
+ ->addColumn('is_verified', 'boolean')
+ ->addIndex(
+ $this->index('email')
+ ->setName('user_email_verified_idx')
+ ->setType('unique')
+ ->setWhere('is_verified = true')
+ )
+ ->create();
+```
-To remove indexes, use `removeIndex()` or `removeIndexByName()`.
+### Concurrent Index Creation (PostgreSQL)
+
+PostgreSQL can create indexes concurrently, which avoids taking disruptive
+locks during index creation:
+
+```php
+$this->table('users')
+ ->addColumn('email', 'string')
+ ->addIndex(
+ $this->index('email')
+ ->setName('user_email_unique_idx')
+ ->setType('unique')
+ ->setConcurrently(true)
+ )
+ ->create();
+```
+
+### GIN Indexes (PostgreSQL)
+
+The PostgreSQL adapter also supports Generalized Inverted Index (`gin`)
+indexes:
+
+```php
+$this->table('users')
+ ->addColumn('address', 'string')
+ ->addIndex('address', ['type' => 'gin'])
+ ->create();
+```
+
+### Removing Indexes
+
+Use `removeIndex()` with the list of columns, or `removeIndexByName()` if you
+named the index:
+
+```php
+$table = $this->table('users');
+$table->removeIndex(['email'])
+ ->save();
+
+$table->removeIndexByName('idx_users_email')
+ ->save();
+```
+
+::: info Added in version 4.6.0
+`Index::setWhere()` and `Index::setConcurrently()` were added.
+:::
## Working With Foreign Keys
-Migrations supports foreign key constraints on database tables:
+Migrations supports creating foreign key constraints on database tables:
```php
table('tags')
+ ->addColumn('tag_name', 'string')
+ ->save();
+
$this->table('tag_relationships')
->addColumn('tag_id', 'integer', ['null' => true])
->addForeignKey(
'tag_id',
'tags',
'id',
- ['delete' => 'SET_NULL', 'update' => 'NO_ACTION']
+ ['delete' => 'SET_NULL', 'update' => 'NO_ACTION'],
)
->save();
}
}
```
-The `delete` and `update` options control `ON DELETE` and `ON UPDATE`
+The `delete` and `update` options define the `ON DELETE` and `ON UPDATE`
behavior. Valid values are `SET_NULL`, `NO_ACTION`, `CASCADE`, and
-`RESTRICT`.
+`RESTRICT`. If `SET_NULL` is used, the column must be created as nullable
+with `['null' => true]`.
-Foreign keys can also be defined with arrays of columns for composite keys.
+### Composite Foreign Keys
-The `foreignKey()` fluent builder is available for more complex cases:
+Foreign keys can be defined with arrays of columns to build constraints
+between tables with composite keys:
```php
-$this->table('articles')
+$this->table('follower_events')
+ ->addColumn('user_id', 'integer')
+ ->addColumn('follower_id', 'integer')
+ ->addColumn('event_id', 'integer')
->addForeignKey(
- $this->foreignKey()
- ->setColumns('user_id')
- ->setReferencedTable('users')
- ->setReferencedColumns('user_id')
- ->setName('article_user_fk')
+ ['user_id', 'follower_id'],
+ 'followers',
+ ['user_id', 'follower_id'],
+ [
+ 'delete' => 'NO_ACTION',
+ 'update' => 'NO_ACTION',
+ 'constraint' => 'user_follower_id',
+ ],
)
->save();
```
-Use `hasForeignKey()` to check whether a foreign key exists, and
-`dropForeignKey()` to remove one.
+The options parameter of `addForeignKey()` supports the following:
+
+| Option | Description |
+|------------|--------------------------------------------------------|
+| update | set an action to be triggered when the row is updated |
+| delete | set an action to be triggered when the row is deleted |
+| constraint | set a name to be used by foreign key constraint |
+| deferrable | define deferred constraint application (postgres only) |
+
+### Fluent Foreign Key Builder
+
+`foreignKey()` returns a fluent builder for more complex cases:
+
+```php
+table('articles')
+ ->addForeignKey(
+ $this->foreignKey()
+ ->setColumns('user_id')
+ ->setReferencedTable('users')
+ ->setReferencedColumns('user_id')
+ ->setDelete(ForeignKey::CASCADE)
+ ->setName('article_user_fk')
+ )
+ ->save();
+ }
+}
+```
+
+::: info Added in version 4.6.0
+The `foreignKey` method was added.
+:::
+
+### Checking and Dropping Foreign Keys
+
+Use `hasForeignKey()` to check whether a foreign key exists:
+
+```php
+$table = $this->table('tag_relationships');
+if ($table->hasForeignKey('tag_id')) {
+ // do something
+}
+```
+
+To delete a foreign key, use `dropForeignKey()`. Like other Table methods, it
+needs `save()` to be called at the end:
+
+```php
+$this->table('tag_relationships')
+ ->dropForeignKey('tag_id')
+ ->save();
+```
## Working With Check Constraints
+::: info Added in version 5.0.0
+Check constraints were added in 5.0.0.
+:::
+
Check constraints allow you to enforce data validation rules at the database
level.
> [!NOTE]
> Check constraints are supported by MySQL 8.0.16+, PostgreSQL, and SQLite.
+> SQL Server support is planned for a future release.
### Adding a Check Constraint
+The first argument is the constraint name, and the second is the SQL
+expression that defines the constraint:
+
```php
-$this->table('products')
- ->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2])
- ->addCheckConstraint('price_positive', 'price > 0')
- ->save();
+table('products')
+ ->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2])
+ ->addCheckConstraint('price_positive', 'price > 0')
+ ->save();
+ }
+
+ public function down(): void
+ {
+ $this->table('products')
+ ->dropCheckConstraint('price_positive')
+ ->save();
+ }
+}
```
-### Using the Fluent Builder
+### Using the CheckConstraint Fluent Builder
+
+For more complex scenarios, `checkConstraint()` returns a fluent builder:
```php
$this->table('users')
+ ->addColumn('age', 'integer')
+ ->addColumn('status', 'string', ['limit' => 20])
->addCheckConstraint(
$this->checkConstraint()
->setName('age_valid')
->setExpression('age >= 18 AND age <= 120')
)
+ ->addCheckConstraint(
+ $this->checkConstraint()
+ ->setName('status_valid')
+ ->setExpression("status IN ('active', 'inactive', 'pending')")
+ )
->save();
```
-If you do not specify a name, one will be auto-generated.
+### Auto-Generated Constraint Names
-Check constraints can reference multiple columns and use more complex SQL
-expressions.
+If you do not specify a constraint name, one will be automatically generated
+based on the table name and expression hash:
-Use `hasCheckConstraint()` to verify existence and `dropCheckConstraint()` to
-remove a constraint.
+```php
+$this->table('inventory')
+ ->addColumn('quantity', 'integer')
+ // Name will be auto-generated like 'inventory_chk_a1b2c3d4'
+ ->addCheckConstraint(
+ $this->checkConstraint()
+ ->setExpression('quantity >= 0')
+ )
+ ->save();
+```
+
+### Complex Check Constraints
+
+Check constraints can reference multiple columns and use complex SQL
+expressions:
+
+```php
+$this->table('date_ranges')
+ ->addColumn('start_date', 'date')
+ ->addColumn('end_date', 'date')
+ ->addColumn('discount', 'decimal', ['precision' => 5, 'scale' => 2])
+ ->addCheckConstraint('valid_date_range', 'end_date >= start_date')
+ ->addCheckConstraint('valid_discount', 'discount BETWEEN 0 AND 100')
+ ->save();
+```
+
+### Checking and Dropping Check Constraints
+
+Use `hasCheckConstraint()` to check whether a constraint exists, and
+`dropCheckConstraint()` to remove one:
+
+```php
+$table = $this->table('products');
+if ($table->hasCheckConstraint('price_positive')) {
+ $table->dropCheckConstraint('price_positive')
+ ->save();
+}
+```
### Database-Specific Behavior
diff --git a/docs/en/upgrades/upgrading-to-builtin-backend.md b/docs/en/upgrades/upgrading-to-builtin-backend.md
index f6a6f2a1..57544665 100644
--- a/docs/en/upgrades/upgrading-to-builtin-backend.md
+++ b/docs/en/upgrades/upgrading-to-builtin-backend.md
@@ -160,7 +160,7 @@ To migrate from `phinxlog` tables to the new `cake_migrations` table:
```
4. **Optionally drop phinx tables**: Your migration history is preserved
- by default. Use `--drop-tables` to drop the `phinxlog`tables after
+ by default. Use `--drop-tables` to drop the `phinxlog` tables after
verifying your migrations run correctly.
```bash