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