diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d2da9cfc..e551da118 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,13 @@ on: - 4.x - 4.next - 5.x + paths-ignore: + - docs/** pull_request: branches: - '*' + paths-ignore: + - docs/** workflow_dispatch: permissions: diff --git a/.github/workflows/deploy_docs_5x.yml b/.github/workflows/deploy_docs_5x.yml index bc6f5ad9c..2f5c1a27e 100644 --- a/.github/workflows/deploy_docs_5x.yml +++ b/.github/workflows/deploy_docs_5x.yml @@ -21,3 +21,4 @@ jobs: with: git_remote_url: 'ssh://dokku@apps.cakephp.org:22/migrations-docs-5' ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} + branch: '5.x' diff --git a/Dockerfile b/Dockerfile index 70ceb413d..832f6bc1c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,36 @@ -# Basic docker based environment -# Necessary to trick dokku into building the documentation -# using dockerfile instead of herokuish -FROM ubuntu:22.04 - -# Add basic tools -RUN apt-get update && \ - apt-get install -y build-essential \ - software-properties-common \ - curl \ - git \ - libxml2 \ - libffi-dev \ - libssl-dev && \ - LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php && \ - apt-get update && \ - apt-get install -y php8.1-cli php8.1-mbstring php8.1-xml php8.1-zip php8.1-intl php8.1-opcache php8.1-sqlite &&\ - apt-get clean &&\ - rm -rf /var/lib/apt/lists/* - -WORKDIR /code - -VOLUME ["/code"] - -CMD [ '/bin/bash' ] +# ---------------------- +# 1. Build stage +# ---------------------- +FROM node:22-alpine AS builder + +# Git is required because docs/package.json pulls a dependency from GitHub. +RUN apk add --no-cache git openssh-client + +WORKDIR /app/docs + +# Copy dependency manifests first to preserve Docker layer caching. +COPY docs/ ./ +RUN npm ci + +# Increase max-old-space-size to avoid memory issues during build +ENV NODE_OPTIONS="--max-old-space-size=8192" + +# Build the site. +RUN npm run docs:build + +# ---------------------- +# 2. Runtime stage (nginx) +# ---------------------- +FROM nginx:1.27-alpine AS runner + +# Copy built files +COPY --from=builder /app/docs/.vitepress/dist /usr/share/nginx/html + +# Expose port +EXPOSE 80 + +# Health check (optional) +HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/docs.Dockerfile b/docs.Dockerfile deleted file mode 100644 index 8d5a6570e..000000000 --- a/docs.Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# Generate the HTML output. -FROM ghcr.io/cakephp/docs-builder as builder - -COPY docs /data/docs - -RUN cd /data/docs-builder && \ - # In the future repeat website for each version - make website LANGS="en" SOURCE=/data/docs DEST=/data/website/ - -# Build a small nginx container with just the static site in it. -FROM ghcr.io/cakephp/docs-builder:runtime as runtime - -ENV LANGS="en" -ENV SEARCH_SOURCE="/usr/share/nginx/html" -ENV SEARCH_URL_PREFIX="/migrations/5" - -COPY --from=builder /data/docs /data/docs -COPY --from=builder /data/website /data/website -COPY --from=builder /data/docs-builder/nginx.conf /etc/nginx/conf.d/default.conf - - -# Move each version into place -RUN cp -R /data/website/html/* /usr/share/nginx/html \ - && rm -rf /data/website/ - -RUN ln -s /usr/share/nginx/html /usr/share/nginx/html/2.x diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..a1a455b7d --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +node_modules +*/public/ +.vitepress/cache diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js new file mode 100644 index 000000000..b0ede705f --- /dev/null +++ b/docs/.vitepress/config.js @@ -0,0 +1,51 @@ +import baseConfig from '@cakephp/docs-skeleton/config' + +import { createRequire } from "module"; +const require = createRequire(import.meta.url); +const toc_en = require("./toc_en.json"); + +const versions = { + text: "5.x", + items: [ + { text: "5.x (current)", link: "book.cakephp.org/migrations/5.x/", target: '_self' }, + { text: "4.x", link: "https://book.cakephp.org/migrations/4/", target: '_self' }, + { text: "3.x", link: "https://book.cakephp.org/migrations/3/", target: '_self' }, + { text: "2.x", link: "https://book.cakephp.org/migrations/2/", target: '_self' }, + ], +}; + +// This file contains overrides for .vitepress/config.js +export default { + extends: baseConfig, + srcDir: 'en', + title: 'Migrations plugin', + description: 'Migrations - CakePHP migrations Documentation', + base: "/5.x/", + rewrites: { + "en/:slug*": ":slug*", + }, + sitemap: { + hostname: "https://book.cakephp.org/migrations/5.x/", + }, + themeConfig: { + socialLinks: [ + { icon: "github", link: "https://github.com/cakephp/cakephp" }, + ], + editLink: { + pattern: "https://github.com/cakephp/migrations/edit/5.x/docs/:path", + text: "Edit this page on GitHub", + }, + sidebar: toc_en, + nav: [ + { text: "CakePHP Book", link: "https://book.cakephp.org/" }, + { ...versions }, + ], + }, + substitutions: {}, + locales: { + root: { + label: "English", + lang: "en", + }, + }, +}; diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js new file mode 100644 index 000000000..e33e19ec9 --- /dev/null +++ b/docs/.vitepress/theme/index.js @@ -0,0 +1 @@ +export { default } from '@cakephp/docs-skeleton' diff --git a/docs/.vitepress/toc_en.json b/docs/.vitepress/toc_en.json new file mode 100644 index 000000000..b7f94176a --- /dev/null +++ b/docs/.vitepress/toc_en.json @@ -0,0 +1,23 @@ +{ + "/": [ + { + "text": "Migrations", + "collapsed": false, + "items": [ + { "text": "Introduction", "link": "/index" }, + { "text": "Writing Migrations", "link": "/writing-migrations" }, + { "text": "Using the Query Builder", "link": "/using-the-query-builder" }, + { "text": "Executing Queries", "link": "/executing-queries" }, + { "text": "Database Seeding", "link": "/seeding" } + ] + }, + { + "text": "Upgrade Guides", + "collapsed": true, + "items": [ + { "text": "Upgrading from 4.x to 5.x", "link": "/upgrading" }, + { "text": "Upgrading to the Builtin Backend", "link": "/upgrading-to-builtin-backend" } + ] + } + ] +} diff --git a/docs/config/__init__.py b/docs/config/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/config/all.py b/docs/config/all.py deleted file mode 100644 index a8f5f2f86..000000000 --- a/docs/config/all.py +++ /dev/null @@ -1,53 +0,0 @@ -# Global configuration information used across all the -# translations of documentation. -# -# Import the base theme configuration -from cakephpsphinx.config.all import * - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# - -# The full version, including alpha/beta/rc tags. -release = '5.x' - -# The search index version. -search_version = 'migrations-5' - -# The marketing display name for the book. -version_name = '' - -# Project name shown in the black header bar -project = 'CakePHP Migrations' - -# Other versions that display in the version picker menu. -version_list = [ - {'name': '2.x', 'number': 'migrations/2', 'title': '2.x'}, - {'name': '3.x', 'number': 'migrations/3', 'title': '3.x'}, - {'name': '4.x', 'number': 'migrations/4', 'title': '4.x'}, - {'name': '5.x', 'number': 'migrations/5', 'title': '5.x', 'current': True}, -] - -# Languages available. -languages = ['en', 'fr', 'ja', 'pt', 'ru'] - -# The GitHub branch name for this version of the docs -# for edit links to point at. -branch = '5.x' - -# Current version being built -version = '5.x' - -# Language in use for this directory. -language = 'en' - -show_root_link = True - -repository = 'cakephp/migrations' - -source_path = 'docs/' - -hide_page_contents = ('search', '404', 'contents') - -is_prerelease = False diff --git a/docs/en/conf.py b/docs/en/conf.py deleted file mode 100644 index f638bda22..000000000 --- a/docs/en/conf.py +++ /dev/null @@ -1,9 +0,0 @@ -import sys, os - -# Append the top level directory of the docs, so we can import from the config dir. -sys.path.insert(0, os.path.abspath('..')) - -# Pull in all the configuration options defined in the global config file.. -from config.all import * - -language = 'en' diff --git a/docs/en/contents.md b/docs/en/contents.md new file mode 100644 index 000000000..18fe41d4c --- /dev/null +++ b/docs/en/contents.md @@ -0,0 +1,8 @@ +### CakePHP Migrations + +- [Migrations 5.x](index) +- [Writing Migrations](writing-migrations) +- [Using the Query Builder](using-the-query-builder) +- [Executing Queries](executing-queries) +- [Database Seeding](seeding) +- [Upgrading to the builtin backend](upgrading-to-builtin-backend) diff --git a/docs/en/contents.rst b/docs/en/contents.rst deleted file mode 100644 index 88edb891a..000000000 --- a/docs/en/contents.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. toctree:: - :maxdepth: 2 - :caption: CakePHP Migrations - - /index - /writing-migrations - /using-the-query-builder - /executing-queries - /seeding - /upgrading-to-builtin-backend diff --git a/docs/en/executing-queries.md b/docs/en/executing-queries.md new file mode 100644 index 000000000..a87c58dd0 --- /dev/null +++ b/docs/en/executing-queries.md @@ -0,0 +1,135 @@ +# Executing Queries + +Queries can be executed with the `execute()` and `query()` methods. The +`execute()` method returns the number of affected rows whereas the +`query()` method returns the result as a +[CakePHP Statement](https://book.cakephp.org/5/en/orm/database-basics.html#interacting-with-statements). Both methods +accept an optional second parameter `$params` which is an array of elements, +and if used will cause the underlying connection to use a prepared statement: + +``` php +execute('DELETE FROM users'); // returns the number of affected rows + + // query() + $stmt = $this->query('SELECT * FROM users'); // returns PDOStatement + $rows = $stmt->fetchAll(); // returns the result as an array + + // using prepared queries + $count = $this->execute('DELETE FROM users WHERE id = ?', [5]); + $stmt = $this->query('SELECT * FROM users WHERE id > ?', [5]); // returns PDOStatement + $rows = $stmt->fetchAll(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +## Fetching Rows + +There are two methods available to fetch rows. The `fetchRow()` method will +fetch a single row, whilst the `fetchAll()` method will return multiple rows. +Both methods accept raw SQL as their only parameter: + +``` php +fetchRow('SELECT * FROM users'); + + // fetch an array of messages + $rows = $this->fetchAll('SELECT * FROM messages'); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +## Inserting Data + +Migrations makes it easy to insert data into your tables. Whilst this feature is +intended for the [seed feature](seeding), you are also free to use the +insert methods in your migrations: + +``` php +table('status'); + + // inserting only one row + $singleRow = [ + 'id' => 1, + 'name' => 'In Progress' + ]; + + $table->insert($singleRow)->saveData(); + + // inserting multiple rows + $rows = [ + [ + 'id' => 2, + 'name' => 'Stopped' + ], + [ + 'id' => 3, + 'name' => 'Queued' + ] + ]; + + $table->insert($rows)->saveData(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $this->execute('DELETE FROM status'); + } +} +``` + +> [!NOTE] +> You cannot use the insert methods inside a change() method. Please use the +> up() and down() methods. diff --git a/docs/en/executing-queries.rst b/docs/en/executing-queries.rst deleted file mode 100644 index de26a5490..000000000 --- a/docs/en/executing-queries.rst +++ /dev/null @@ -1,134 +0,0 @@ -Executing Queries -################# - -Queries can be executed with the ``execute()`` and ``query()`` methods. The -``execute()`` method returns the number of affected rows whereas the -``query()`` method returns the result as a -`CakePHP Statement `_. Both methods -accept an optional second parameter ``$params`` which is an array of elements, -and if used will cause the underlying connection to use a prepared statement:: - - execute('DELETE FROM users'); // returns the number of affected rows - - // query() - $stmt = $this->query('SELECT * FROM users'); // returns PDOStatement - $rows = $stmt->fetchAll(); // returns the result as an array - - // using prepared queries - $count = $this->execute('DELETE FROM users WHERE id = ?', [5]); - $stmt = $this->query('SELECT * FROM users WHERE id > ?', [5]); // returns PDOStatement - $rows = $stmt->fetchAll(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Fetching Rows -============= - -There are two methods available to fetch rows. The ``fetchRow()`` method will -fetch a single row, whilst the ``fetchAll()`` method will return multiple rows. -Both methods accept raw SQL as their only parameter:: - - fetchRow('SELECT * FROM users'); - - // fetch an array of messages - $rows = $this->fetchAll('SELECT * FROM messages'); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Inserting Data -============== - -Migrations makes it easy to insert data into your tables. Whilst this feature is -intended for the :doc:`seed feature `, you are also free to use the -insert methods in your migrations:: - - table('status'); - - // inserting only one row - $singleRow = [ - 'id' => 1, - 'name' => 'In Progress' - ]; - - $table->insert($singleRow)->saveData(); - - // inserting multiple rows - $rows = [ - [ - 'id' => 2, - 'name' => 'Stopped' - ], - [ - 'id' => 3, - 'name' => 'Queued' - ] - ]; - - $table->insert($rows)->saveData(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $this->execute('DELETE FROM status'); - } - } - -.. note:: - - You cannot use the insert methods inside a `change()` method. Please use the - `up()` and `down()` methods. - diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 000000000..6eb56c665 --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,941 @@ +# Migrations 5.x + +Migrations is a plugin that lets you track changes to your database schema over +time as PHP code that accompanies your application. This lets you ensure each +environment your application runs in has the appropriate schema by applying +migrations. + +Instead of writing schema modifications in SQL, this plugin allows you to +define schema changes with a high-level database portable API. + +## What's New in 5.x + +Migrations 5.x includes several new features: + +- **Seed tracking** - Seeds are now tracked in a `cake_seeds` table, preventing + accidental re-runs +- **Check constraints** - Support for database check constraints via `addCheckConstraint()` +- **Default values in bake** - Specify default values when baking migrations + (e.g., `active:boolean:default[true]`) +- **MySQL ALTER options** - Control `ALGORITHM` and `LOCK` for ALTER TABLE operations +- **insertOrSkip()** - New method for idempotent seed data insertion + +See the [Upgrading from 4.x to 5.x](upgrading) guide for breaking changes and migration steps from 4.x. + +## Installation + +By default Migrations is installed with the application skeleton. If +you've removed it and want to re-install it, you can do so by running the +following from your application's ROOT directory (where **composer.json** file is +located): + +``` bash +php composer.phar require cakephp/migrations "@stable" + +# Or if composer is installed globally +composer require cakephp/migrations "@stable" +``` + +To use the plugin you'll need to load it in your application's +**config/bootstrap.php** file. You can use [CakePHP's Plugin shell](https://book.cakephp.org/5/en/console-and-shells/plugin-shell.html) to +load and unload plugins from your **config/bootstrap.php**: + +``` bash +bin/cake plugin load Migrations +``` + +Or you can load the plugin by editing your **src/Application.php** file and +adding the following statement: + +``` php +$this->addPlugin('Migrations'); +``` + +Additionally, you will need to configure the default database configuration for +your application in your **config/app.php** file as explained in the [Database +Configuration section](https://book.cakephp.org/5/en/orm/database-basics.html#database-configuration). + +## Upgrading from 4.x + +If you are upgrading from Migrations 4.x, please see the [Upgrading from 4.x to 5.x](upgrading) guide +for breaking changes and migration steps. + +## Overview + +A migration is a PHP file that describes the changes to apply to your database. +A migration file can add, change or remove tables, columns, indexes and foreign keys. + +If we wanted to create a table, we could use a migration similar to this: + +``` php +table('products'); + $table->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]); + $table->addColumn('description', 'text', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('created', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('modified', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->create(); + } +} +``` + +When applied, this migration will add a table to your database named +`products` with the following column definitions: + +- `id` column of type `integer` as primary key. This column is added + implicitly, but you can customize the name and type if necessary. +- `name` column of type `string` +- `description` column of type `text` +- `created` column of type `datetime` +- `modified` column of type `datetime` + +> [!NOTE] +> Migrations are not automatically applied, you can apply and rollback +> migrations with CLI commands. + +Once the file has been created in the **config/Migrations** folder, you can +apply it: + +``` bash +bin/cake migrations migrate +``` + +## Creating Migrations + +Migration files are stored in the **config/Migrations** directory of your +application. The name of the migration files are prefixed with the date in +which they were created, in the format **YYYYMMDDHHMMSS_MigrationName.php**. +Here are examples of migration filenames: + +- **20160121163850_CreateProducts.php** +- **20160210133047_AddRatingToProducts.php** + +The easiest way to create a migrations file is by using `bin/cake bake migration` CLI command: + +``` bash +bin/cake bake migration CreateProducts +``` + +This will create an empty migration that you can edit to add any columns, +indexes and foreign keys you need. See the [Creating A Table](writing-migrations#creating-a-table) section to +learn more about using migrations to define tables. + +> [!NOTE] +> Migrations need to be applied using `bin/cake migrations migrate` after +> they have been created. + +### Migration file names + +When generating a migration, you can follow one of the following patterns +to have additional skeleton code generated: + +- `/^(Create)(.*)/` Creates the specified table. +- `/^(Drop)(.*)/` Drops the specified table. + Ignores specified field arguments +- `/^(Add).*(?:To)(.*)/` Adds fields to the specified + table +- `/^(Remove).*(?:From)(.*)/` Removes fields from the + specified table +- `/^(Alter)(.*)/` Alters the specified table. An alias + for CreateTable and AddField. +- `/^(Alter).*(?:On)(.*)/` Alters fields from the specified table. + +You can also use the `underscore_form` as the name for your migrations i.e. +`create_products`. + +> [!WARNING] +> Migration names are used as class names, and thus may collide with +> other migrations if the class names are not unique. In this case, it may be +> necessary to manually override the name at a later date, or simply change +> the name you are specifying. + +### Creating a table + +You can use `bake migration` to create a table: + +``` bash +bin/cake bake migration CreateProducts name:string description:text created modified +``` + +The command line above will generate a migration file that resembles: + +``` php +table('products'); + $table->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]); + $table->addColumn('description', 'text', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('created', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('modified', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->create(); + } +} +``` + +### Column syntax + +The `bake migration` command provides a compact syntax to define columns when +generating a migration: + +``` bash +bin/cake bake migration CreateProducts name:string description:text created modified +``` + +You can use the column syntax when creating tables and adding columns. You can +also edit the migration after generation to add or customize the columns + +Columns on the command line follow the following pattern: + + fieldName:fieldType?[length]:default[value]:indexType:indexName + +For instance, the following are all valid ways of specifying an email field: + +- `email:string?` +- `email:string:unique` +- `email:string?[50]` +- `email:string:unique:EMAIL_INDEX` +- `email:string[120]:unique:EMAIL_INDEX` + +While defining decimal columns, the `length` can be defined to have precision +and scale, separated by a comma. + +- `amount:decimal[5,2]` +- `amount:decimal?[5,2]` + +Columns with a question mark after the fieldType will make the column nullable. + +The `length` part is optional and should always be written between bracket. + +The `default[value]` part is optional and sets the default value for the column. +Supported value types include: + +- Booleans: `true` or `false` - e.g., `active:boolean:default[true]` +- Integers: `0`, `123`, `-456` - e.g., `count:integer:default[0]` +- Floats: `1.5`, `-2.75` - e.g., `rate:decimal:default[1.5]` +- Strings: `'hello'` or `"world"` (quoted) - e.g., `status:string:default['pending']` +- Null: `null` or `NULL` - e.g., `description:text?:default[null]` +- SQL expressions: `CURRENT_TIMESTAMP` - e.g., `created_at:datetime:default[CURRENT_TIMESTAMP]` + +Fields named `created` and `modified`, as well as any field with a `_at` +suffix, will automatically be set to the type `datetime`. + +There are some heuristics to choosing fieldtypes when left unspecified or set to +an invalid value. Default field type is `string`: + +- id: integer +- created, modified, updated: datetime +- latitude, longitude (or short forms lat, lng): decimal + +Additionally you can create an empty migrations file if you want full control +over what needs to be executed, by omitting to specify a columns definition: + +``` bash +bin/cake migrations create MyCustomMigration +``` + +See [Writing Migrations](writing-migrations) for more information on how to use `Table` +objects to interact with tables and define schema changes. + +### Adding columns to an existing table + +If the migration name in the command line is of the form "AddXXXToYYY" and is +followed by a list of column names and types then a migration file containing +the code for creating the columns will be generated: + +``` bash +bin/cake bake migration AddPriceToProducts price:decimal[5,2] +``` + +Executing the command line above will generate: + +``` php +table('products'); + $table->addColumn('price', 'decimal', [ + 'default' => null, + 'null' => false, + 'precision' => 5, + 'scale' => 2, + ]); + $table->update(); + } +} +``` + +### Adding a column with an index + +It is also possible to add indexes to columns: + +``` bash +bin/cake bake migration AddNameIndexToProducts name:string:index +``` + +will generate: + +``` php +table('products'); + $table->addColumn('name', 'string') + ->addColumn('email', 'string') + ->addIndex(['name']) + // add a unique index: + ->addIndex('email', ['unique' => true]) + ->update(); + } +} +``` + +### Adding a column with a default value + +You can specify default values for columns using the `default[value]` syntax: + +``` bash +bin/cake bake migration AddActiveToUsers active:boolean:default[true] +``` + +will generate: + +``` php +table('users'); + $table->addColumn('active', 'boolean', [ + 'default' => true, + 'null' => false, + ]); + $table->update(); + } +} +``` + +You can combine default values with other options like nullable and indexes: + +``` bash +bin/cake bake migration AddStatusToOrders status:string:default['pending']:unique +``` + +### Altering a column + +In the same way, you can generate a migration to alter a column by using the +command line, if the migration name is of the form "AlterXXXOnYYY": + +``` bash +bin/cake bake migration AlterPriceOnProducts name:float +``` + +will generate: + +``` php +table('products'); + $table->changeColumn('name', 'float'); + $table->update(); + } +} +``` + +> [!WARNING] +> Changing the type of a column can result in data loss if the +> current and target column type are not compatible. For example converting +> a varchar to float. + +### Removing a column + +In the same way, you can generate a migration to remove a column by using the +command line, if the migration name is of the form "RemoveXXXFromYYY": + +``` bash +bin/cake bake migration RemovePriceFromProducts price +``` + +creates the file: + +``` php +table('products'); + $table->removeColumn('price') + ->save(); + } +} +``` + +> [!NOTE] +> The removeColumn command is not reversible, so must be called in the +> up method. A corresponding addColumn call should be added to the +> down method. + +## Generating migration snapshots from an existing database + +If you have a pre-existing database and want to start using +migrations, or to version control the initial schema of your application's +database, you can run the `bake migration_snapshot` command: + +``` bash +bin/cake bake migration_snapshot Initial +``` + +It will generate a migration file called **YYYYMMDDHHMMSS_Initial.php** +containing all the create statements for all tables in your database. + +By default, the snapshot will be created by connecting to the database defined +in the `default` connection configuration. If you need to bake a snapshot from +a different datasource, you can use the `--connection` option: + +``` bash +bin/cake bake migration_snapshot Initial --connection my_other_connection +``` + +You can also make sure the snapshot includes only the tables for which you have +defined the corresponding model classes by using the `--require-table` flag: + +``` bash +bin/cake bake migration_snapshot Initial --require-table +``` + +When using the `--require-table` flag, the shell will look through your +application `Table` classes and will only add the model tables in the snapshot. + +If you want to generate a snapshot without marking it as migrated (for example, +for use in unit tests), you can use the `--generate-only` flag: + +``` bash +bin/cake bake migration_snapshot Initial --generate-only +``` + +This will create the migration file but will not add an entry to the migrations +tracking table, allowing you to move the file to a different location without causing +"MISSING" status issues. + +The same logic will be applied implicitly if you wish to bake a snapshot for a +plugin. To do so, you need to use the `--plugin` option: + +``` bash +bin/cake bake migration_snapshot Initial --plugin MyPlugin +``` + +Only the tables which have a `Table` object model class defined will be added +to the snapshot of your plugin. + +> [!NOTE] +> When baking a snapshot for a plugin, the migration files will be created +> in your plugin's **config/Migrations** directory. + +Be aware that when you bake a snapshot, it is automatically added to the +migrations log table as migrated. + +## Generating a diff + +As migrations are applied and rolled back, the migrations plugin will generate +a 'dump' file of your schema. If you make manual changes to your database schema +outside of migrations, you can use `bake migration_diff` to generate +a migration file that captures the difference between the current schema dump +file and database schema. To do so, you can use the following command: + +``` bash +bin/cake bake migration_diff NameOfTheMigrations +``` + +By default, the diff will be created by connecting to the database defined +in the `default` connection configuration. +If you need to bake a diff from a different datasource, you can use the +`--connection` option: + +``` bash +bin/cake bake migration_diff NameOfTheMigrations --connection my_other_connection +``` + +If you want to use the diff feature on an application that already has a +migrations history, you need to manually create the dump file that will be used +as comparison: + +``` bash +bin/cake migrations dump +``` + +The database state must be the same as it would be if you just migrated all +your migrations before you create a dump file. +Once the dump file is generated, you can start doing changes in your database +and use the `bake migration_diff` command whenever you see fit. + +> [!NOTE] +> Migration diff generation can not detect column renamings. + +## Applying Migrations + +Once you have generated or written your migration file, you need to execute the +following command to apply the changes to your database: + +``` bash +# Run all the migrations +bin/cake migrations migrate + +# Migrate to a specific version using the ``--target`` option +# or ``-t`` for short. +# The value is the timestamp that is prefixed to the migrations file name:: +bin/cake migrations migrate -t 20150103081132 + +# By default, migration files are looked for in the **config/Migrations** +# directory. You can specify the directory using the ``--source`` option +# or ``-s`` for short. +# The following example will run migrations in the **config/Alternate** +# directory +bin/cake migrations migrate -s Alternate + +# You can run migrations to a different connection than the ``default`` one +# using the ``--connection`` option or ``-c`` for short +bin/cake migrations migrate -c my_custom_connection + +# Migrations can also be run for plugins. Simply use the ``--plugin`` option +# or ``-p`` for short +bin/cake migrations migrate -p MyAwesomePlugin +``` + +## Reverting Migrations + +The rollback command is used to undo previous migrations executed by this +plugin. It is the reverse action of the `migrate` command: + +``` bash +# You can rollback to the previous migration by using the +# ``rollback`` command:: +bin/cake migrations rollback + +# You can also pass a migration version number to rollback +# to a specific version:: +bin/cake migrations rollback -t 20150103081132 +``` + +You can also use the `--source`, `--connection` and `--plugin` options +just like for the `migrate` command. + +## View Migrations Status + +The Status command prints a list of all migrations, along with their current +status. You can use this command to determine which migrations have been run: + +``` bash +bin/cake migrations status +``` + +You can also output the results as a JSON formatted string using the +`--format` option (or `-f` for short): + +``` bash +bin/cake migrations status --format json +``` + +You can also use the `--source`, `--connection` and `--plugin` options +just like for the `migrate` command. + +### Cleaning up missing migrations + +Sometimes migration files may be deleted from the filesystem but still exist +in the migrations tracking table. These migrations will be marked as **MISSING** in the +status output. You can remove these entries from the tracking table using the +`--cleanup` option: + +``` bash +bin/cake migrations status --cleanup +``` + +This will remove all migration entries from the tracking table that no longer +have corresponding migration files in the filesystem. + +## Marking a migration as migrated + +It can sometimes be useful to mark a set of migrations as migrated without +actually running them. In order to do this, you can use the `mark_migrated` +command. The command works seamlessly as the other commands. + +You can mark all migrations as migrated using this command: + +``` bash +bin/cake migrations mark_migrated +``` + +You can also mark all migrations up to a specific version as migrated using +the `--target` option: + +``` bash +bin/cake migrations mark_migrated --target=20151016204000 +``` + +If you do not want the targeted migration to be marked as migrated during the +process, you can use the `--exclude` flag with it: + +``` bash +bin/cake migrations mark_migrated --target=20151016204000 --exclude +``` + +Finally, if you wish to mark only the targeted migration as migrated, you can +use the `--only` flag: + +``` bash +bin/cake migrations mark_migrated --target=20151016204000 --only +``` + +You can also use the `--source`, `--connection` and `--plugin` options +just like for the `migrate` command. + +> [!NOTE] +> When you bake a snapshot with the `cake bake migration_snapshot` +> command, the created migration will automatically be marked as migrated. +> To prevent this behavior (e.g., for unit test migrations), use the +> `--generate-only` flag. + +This command expects the migration version number as argument: + +``` bash +bin/cake migrations mark_migrated 20150420082532 +``` + +If you wish to mark all migrations as migrated, you can use the `all` special +value. If you use it, it will mark all found migrations as migrated: + +``` bash +bin/cake migrations mark_migrated all +``` + +## Seeding your database + +Seed classes are a good way to populate your database with default or starter +data. They are also a great way to generate data for development environments. + +By default, seeds will be looked for in the `config/Seeds/` directory of +your application. See the [Database Seeding](seeding) for how to build and use seed classes. + +## Generating a dump file + +The dump command creates a file to be used with the `bake migration_diff` +command: + +``` bash +bin/cake migrations dump +``` + +Each generated dump file is specific to the Connection it is generated from (and +is suffixed as such). This allows the `bake migration_diff` command to +properly compute diff in case your application is dealing with multiple database +possibly from different database vendors. + +Dump files are created in the same directory as your migrations files. + +You can also use the `--source`, `--connection` and `--plugin` options +just like for the `migrate` command. + +## Using Migrations for Tests + +If you are using migrations for your application schema you can also use those +same migrations to build schema in your tests. In your application's +`tests/bootstrap.php` file you can use the `Migrator` class to build schema +when tests are run. The `Migrator` will use existing schema if it is current, +and if the migration history that is in the database differs from what is in the +filesystem, all tables will be dropped and migrations will be rerun from the +beginning: + +``` php +// in tests/bootstrap.php +use Migrations\TestSuite\Migrator; + +$migrator = new Migrator(); + +// Simple setup for with no plugins +$migrator->run(); + +// Run a non 'test' database +$migrator->run(['connection' => 'test_other']); + +// Run migrations for plugins +$migrator->run(['plugin' => 'Contacts']); + +// Run the Documents migrations on the test_docs connection. +$migrator->run(['plugin' => 'Documents', 'connection' => 'test_docs']); +``` + +If you need to run multiple sets of migrations, those can be run as follows: + +``` php +// Run migrations for plugin Contacts on the ``test`` connection, and Documents on the ``test_docs`` connection +$migrator->runMany([ + ['plugin' => 'Contacts'], + ['plugin' => 'Documents', 'connection' => 'test_docs'] +]); +``` + +If your database also contains tables that are not managed by your application +like those created by PostGIS, then you can exclude those tables from the drop +& truncate behavior using the `skip` option: + +``` php +$migrator->run(['connection' => 'test', 'skip' => ['postgis*']]); +``` + +The `skip` option accepts a `fnmatch()` compatible pattern to exclude tables +from drop & truncate operations. + +If you need to see additional debugging output from migrations are being run, +you can enable a `debug` level logger. + +## Using Migrations In Plugins + +Plugins can also provide migration files. This makes plugins that are intended +to be distributed much more portable and easy to install. All commands in the +Migrations plugin support the `--plugin` or `-p` option that will scope the +execution to the migrations relative to that plugin: + +``` bash +bin/cake migrations status -p PluginName + +bin/cake migrations migrate -p PluginName +``` + +## Running Migrations in a non-shell environment + +While typical usage of migrations is from the command line, you can also run +migrations from a non-shell environment, by using +`Migrations\Migrations` class. This can be handy in case you are developing a plugin +installer for a CMS for instance. The `Migrations` class allows you to run the +following commands from the migrations shell: + +- migrate +- rollback +- markMigrated +- status +- seed + +Each of these commands has a method defined in the `Migrations` class. + +Here is how to use it: + +``` php +use Migrations\Migrations; + +$migrations = new Migrations(); + +// Will return an array of all migrations and their status +$status = $migrations->status(); + +// Will return true if success. If an error occurred, an exception will be thrown +$migrate = $migrations->migrate(); + +// Will return true if success. If an error occurred, an exception will be thrown +$rollback = $migrations->rollback(); + +// Will return true if success. If an error occurred, an exception will be thrown +$markMigrated = $migrations->markMigrated(20150804222900); + +// Will return true if success. If an error occurred, an exception will be thrown +$seeded = $migrations->seed(); +``` + +The methods can accept an array of parameters that should match options from +the commands: + +``` php +use Migrations\Migrations; + +$migrations = new Migrations(); + +// Will return an array of all migrations and their status +$status = $migrations->status(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); +``` + +You can pass any options the shell commands would take. +The only exception is the `markMigrated` command which is expecting the +version number of the migrations to mark as migrated as first argument. Pass +the array of parameters as the second argument for this method. + +Optionally, you can pass these parameters in the constructor of the class. +They will be used as default and this will prevent you from having to pass +them on each method call: + +``` php +use Migrations\Migrations; + +$migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); + +// All the following calls will be done with the parameters passed to the Migrations class constructor +$status = $migrations->status(); +$migrate = $migrations->migrate(); +``` + +If you need to override one or more default parameters for one call, you can +pass them to the method: + +``` php +use Migrations\Migrations; + +$migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); + +// This call will be made with the "custom" connection +$status = $migrations->status(); +// This one with the "default" connection +$migrate = $migrations->migrate(['connection' => 'default']); +``` + + + +## Feature Flags + +Migrations offers a few feature flags for compatibility. These features are disabled by default but can be enabled if required: + +- `unsigned_primary_keys`: Should Migrations create primary keys as unsigned integers? (default: `false`) +- `unsigned_ints`: Should Migrations create all integer columns as unsigned? (default: `false`) +- `column_null_default`: Should Migrations create columns as null by default? (default: `false`) +- `add_timestamps_use_datetime`: Should Migrations use `DATETIME` type + columns for the columns added by `addTimestamps()`. + +Set them via Configure to enable (e.g. in `config/app.php`): + +``` text +'Migrations' => [ + 'unsigned_primary_keys' => true, + 'unsigned_ints' => true, + 'column_null_default' => true, +], +``` + +> [!NOTE] +> The `unsigned_primary_keys` and `unsigned_ints` options only affect MySQL databases. +> When generating migrations with `bake migration_snapshot` or `bake migration_diff`, +> the `signed` attribute will only be included in the output for unsigned columns +> (as `'signed' => false`). Signed is the default for integer columns in MySQL, so +> `'signed' => true` is never output. + +## Skipping the `schema.lock` file generation + +In order for the diff feature to work, a **.lock** file is generated everytime +you migrate, rollback or bake a snapshot, to keep track of the state of your +database schema at any given point in time. You can skip this file generation, +for instance when deploying on your production environment, by using the +`--no-lock` option for the aforementioned command: + +``` bash +bin/cake migrations migrate --no-lock + +bin/cake migrations rollback --no-lock + +bin/cake bake migration_snapshot MyMigration --no-lock +``` + +## Deployment + +You should update your deployment scripts to run migrations when new code is +deployed. Ideally you want to run migrations after the code is on your servers, +but before the application code becomes active. + +After running migrations remember to clear the ORM cache so it renews the column +metadata of your tables. Otherwise, you might end up having errors about +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 to perform this operation: + +``` bash +bin/cake migration migrate +bin/cake schema_cache clear +``` + +## Alert of missing migrations + +You can use the `Migrations.PendingMigrations` middleware in local development +to alert developers about new migrations that have not been applied: + +``` php +use Migrations\Middleware\PendingMigrationsMiddleware; + +$config = [ + 'plugins' => [ + ... // Optionally include a list of plugins with migrations to check. + ], +]; + +$middlewareQueue + ... // ErrorHandler middleware + ->add(new PendingMigrationsMiddleware($config)) + ... // rest +``` + +You can add `'app'` config key set to `false` if you are only interested in +checking plugin migrations. + +You can temporarily disable the migration check by adding +`skip-migration-check=1` to the URL query string + +## IDE autocomplete support + +The [IdeHelper plugin](https://github.com/dereuromark/cakephp-ide-helper) can help +you to get more IDE support for the tables, their column names and possible column types. +Specifically PHPStorm understands the meta information and can help you autocomplete those. diff --git a/docs/en/seeding.md b/docs/en/seeding.md new file mode 100644 index 000000000..c2d21c501 --- /dev/null +++ b/docs/en/seeding.md @@ -0,0 +1,585 @@ +# Database Seeding + +Seed classes are a great way to easily fill your database with data after +it's created. By default, they are stored in the `config/Seeds` directory. + +> [!NOTE] +> Database seeding is entirely optional, and Migrations does not create a Seeds +> directory by default. + +## Creating a New Seed Class + +Migrations includes a command to easily generate a new seed class: + +``` bash +$ bin/cake bake seed MyNewSeed +``` + +By default, it generates a traditional seed class with a named class: + +``` php +call('Articles');` or `$this->call('ArticlesSeed');` + +Using the short name is recommended for cleaner, more concise code. + +### Anonymous Seed Classes + +Migrations also supports generating anonymous seed 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 + +To generate an anonymous seed class, use the `--style anonymous` option: + +``` bash +$ bin/cake bake seed MyNewSeed --style anonymous +``` + +This generates a seed file using an anonymous class: + +``` php + [ + 'style' => 'anonymous', // or 'traditional' +], +``` + +### Seed Options + +``` bash +# you specify the name of the table the seed files will alter by using the ``--table`` option +bin/cake bake seed articles --table my_articles_table +# You can specify a plugin to bake into +bin/cake bake seed Articles --plugin PluginName + +# You can specify an alternative connection when generating a seed. +bin/cake bake seed Articles --connection connection + +# Include data from the Articles table in your seed. +bin/cake bake seed --data Articles +``` + +By default, it will export all the rows found in your table. You can limit the +number of rows exported by using the `--limit` option: + +``` bash +# Will only export the first 10 rows found +bin/cake bake seed --data --limit 10 Articles +``` + +If you only want to include a selection of fields from the table in your seed +file, you can use the `--fields` option. It takes the list of fields to +include as a comma separated value string: + +``` bash +# Will only export the fields `id`, `title` and `excerpt` +bin/cake bake seed --data --fields id,title,excerpt Articles +``` + +> [!TIP] +> Of course you can use both the `--limit` and `--fields` options in the +> same command call. + + + +### Customizing Seed and Migration templates + +Because migrations uses [bake](https://book.cakephp.org/bake) under the hood +you can customize the templates that migrations uses for creating seeds and +migrations by creating templates in your application. Custom templates for +migrations should be on one of the following paths: + +- `ROOT/templates/plugin/Migrations/bake/` +- `ROOT/templates/bake/` + +For example, the seed templates are: + +- Traditional: `Seed/seed.twig` at **ROOT/templates/plugin/Migrations/bake/Seed/seed.twig** +- Anonymous: `Seed/seed-anonymous.twig` at **ROOT/templates/plugin/Migrations/bake/Seed/seed-anonymous.twig** + +## The BaseSeed Class + +All Migrations seeds extend from the `BaseSeed` class. +It provides the necessary support to create your seed classes. Seed +classes are primarily used to insert test data. + +## The Run Method + +The run method is automatically invoked by Migrations when you execute the +`cake seeds run` command. You should use this method to insert your test +data. + +## Seed Execution Tracking + +Seeds track their execution state in the `cake_seeds` database table. By default, +a seed will only run once. If you attempt to run a seed that has already been +executed, it will be skipped with an "already executed" message. + +To re-run a seed that has already been executed, use the `--force` flag: + +``` bash +bin/cake seeds run Users --force +``` + +You can check which seeds have been executed using the status command: + +``` bash +bin/cake seeds status +``` + +To reset all seeds' execution state (allowing them to run again without `--force`): + +``` bash +bin/cake seeds reset +``` + +> [!NOTE] +> When re-running seeds with `--force`, be careful to ensure your seeds are +> idempotent (safe to run multiple times) or they may create duplicate data. + +### Customizing the Seed Tracking Table + +By default, seed execution is tracked in a table named `cake_seeds`. You can +customize this table name by configuring it in your `config/app.php` or +`config/app_local.php`: + +``` php +'Migrations' => [ + 'seed_table' => 'my_custom_seeds_table', +], +``` + +This is useful if you need to avoid table name conflicts or want to follow +a specific naming convention in your database. + +## Idempotent Seeds + +Some seeds are designed to be run multiple times safely (idempotent), such as seeds +that update configuration or reference data. For these seeds, you can override the +`isIdempotent()` method: + +``` php +execute(" + INSERT INTO settings (setting_key, setting_value) + VALUES ('app_version', '2.0.0') + ON DUPLICATE KEY UPDATE setting_value = '2.0.0' + "); + + // Or check before inserting + $exists = $this->fetchRow( + "SELECT COUNT(*) as count FROM settings WHERE setting_key = 'maintenance_mode'" + ); + + if ($exists['count'] == 0) { + $this->table('settings')->insert([ + 'setting_key' => 'maintenance_mode', + 'setting_value' => 'false', + ])->save(); + } + } +} +``` + +When `isIdempotent()` returns `true`: + +- The seed will run **every time** you execute `seeds run` +- The last execution time is still tracked in the `cake_seeds` table +- The `seeds status` command will show the seed as `(idempotent)` +- You must ensure the seed's `run()` method handles duplicate executions safely + +This is useful for: + +- Configuration seeds that should always reflect current values +- Reference data that may need periodic updates +- Seeds that use `INSERT ... ON DUPLICATE KEY UPDATE` or similar patterns +- Development/testing seeds that need to run repeatedly + +> [!WARNING] +> Only mark a seed as idempotent if you've verified it's safe to run multiple times. +> Otherwise, you may create duplicate data or other unexpected behavior. + +## The Init Method + +The `init()` method is run by Migrations before the run method if it exists. This +can be used to initialize properties of the Seed class before using run. + +## The Should Execute Method + +The `shouldExecute()` method is run by Migrations before executing the seed. +This can be used to prevent the seed from being executed at this time. It always +returns true by default. You can override it in your custom `BaseSeed` +implementation. + +## Foreign Key Dependencies + +Often you'll find that seeds need to run in a particular order, so they don't +violate foreign key constraints. To define this order, you can implement the +`getDependencies()` method that returns an array of seeds to run before the +current seed: + +``` php + [!NOTE] +> Dependencies that have already been executed (according to the `cake_seeds` +> table) will be skipped, unless you use the `--force` flag which will +> re-execute all seeds including dependencies. + +## Calling a Seed from another Seed + +Usually when seeding, the order in which to insert the data must be respected +to not encounter constraint violations. Since seeds are executed in an +alphabetical order by default, you can use the `\Migrations\BaseSeed::call()` +method to define your own sequence of seeds execution: + +``` php +call('Another'); // Short name without 'Seed' suffix + $this->call('YetAnother'); // Short name without 'Seed' suffix + + // You can use the plugin dot syntax to call seeds from a plugin + $this->call('PluginName.FromPlugin'); + } +} +``` + +You can also use the full seed name including the `Seed` suffix: + +``` php +$this->call('AnotherSeed'); +$this->call('YetAnotherSeed'); +$this->call('PluginName.FromPluginSeed'); +``` + +Both forms are supported and work identically. + +## Inserting Data + +Seed classes can also use the familiar `Table` object to insert data. You can +retrieve an instance of the Table object by calling the `table()` method from +within your seed class and then use the `insert()` method to insert data: + +``` php + 'foo', + 'created' => date('Y-m-d H:i:s'), + ],[ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->saveData(); + } +} +``` + +> [!NOTE] +> You must call the `saveData()` method to commit your data to the table. +> Migrations will buffer data until you do so. + +## Insert Modes + +In addition to the standard `insert()` method, Migrations provides specialized +insert methods for handling conflicts with existing data. + +### Insert or Skip + +The `insertOrSkip()` method inserts rows but silently skips any that would +violate a unique constraint: + +``` php + 'USD', 'name' => 'US Dollar'], + ['code' => 'EUR', 'name' => 'Euro'], + ]; + + $this->table('currencies') + ->insertOrSkip($data) + ->saveData(); + } +} +``` + +### Insert or Update (Upsert) + +The `insertOrUpdate()` method performs an "upsert" operation - inserting new +rows and updating existing rows that conflict on unique columns: + +``` php + 'USD', 'rate' => 1.0000], + ['code' => 'EUR', 'rate' => 0.9234], + ]; + + $this->table('exchange_rates') + ->insertOrUpdate($data, ['rate'], ['code']) + ->saveData(); + } +} +``` + +The method takes three arguments: + +- `$data`: The rows to insert (same format as `insert()`) +- `$updateColumns`: Which columns to update when a conflict occurs +- `$conflictColumns`: Which columns define uniqueness (must have a unique index) + +> [!WARNING] +> Database-specific behavior differences: +> +> **MySQL**: Uses `ON DUPLICATE KEY UPDATE`. The `$conflictColumns` parameter +> is ignored because MySQL automatically applies the update to *all* unique +> constraint violations on the table. Passing `$conflictColumns` will trigger +> a warning. If your table has multiple unique constraints, be aware that a +> conflict on *any* of them will trigger the update. +> +> **PostgreSQL/SQLite**: Uses `ON CONFLICT (...) DO UPDATE SET`. The +> `$conflictColumns` parameter is required and specifies exactly which unique +> constraint should trigger the update. A `RuntimeException` will be thrown +> if this parameter is empty. +> +> **SQL Server**: Not currently supported. Use separate insert/update logic. + +## Truncating Tables + +In addition to inserting data Migrations makes it trivial to empty your tables using the +SQL TRUNCATE command: + +``` php + 'foo', + 'created' => date('Y-m-d H:i:s'), + ], + [ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->saveData(); + + // empty the table + $posts->truncate(); + } +} +``` + +> [!NOTE] +> SQLite doesn't natively support the `TRUNCATE` command so behind the scenes +> `DELETE FROM` is used. It is recommended to call the `VACUUM` command +> after truncating a table. Migrations does not do this automatically. + +## Executing Seed Classes + +This is the easy part. To seed your database, simply use the `seeds run` command: + +``` bash +$ bin/cake seeds run +``` + +By default, Migrations will execute all available seed classes. If you would like to +run a specific seed, simply pass in the seed name as an argument. +You can use either the short name (without the `Seed` suffix) or the full name: + +``` bash +$ bin/cake seeds run User +# or +$ bin/cake seeds run UserSeed +``` + +Both commands work identically. + +You can also run multiple seeds by separating them with commas: + +``` bash +$ bin/cake seeds run User,Permission,Log +# or with full names +$ bin/cake seeds run UserSeed,PermissionSeed,LogSeed +``` + +You can also use the -v parameter for more output verbosity: + +``` bash +$ bin/cake seeds run -v +``` + +The Migrations seed functionality provides a simple mechanism to easily and repeatably +insert test data into your database, this is great for development environment +sample data or getting state for demos. diff --git a/docs/en/upgrading-to-builtin-backend.md b/docs/en/upgrading-to-builtin-backend.md new file mode 100644 index 000000000..59a034c1b --- /dev/null +++ b/docs/en/upgrading-to-builtin-backend.md @@ -0,0 +1,187 @@ +# Upgrading to the builtin backend + +As of migrations 4.3 there is a new migrations backend that uses CakePHP's +database abstractions and ORM. In 4.4, the `builtin` backend became the +default backend. As of migrations 5.0, phinx has been removed as a dependency +and only the builtin backend is supported. This greatly reduces the dependency +footprint of migrations. + +## What is the same? + +Your migrations shouldn't have to change much to adapt to the new backend. +The builtin backend provides similar functionality to what was available with +phinx. If your migrations don't work in a way that could be addressed by the +changes outlined below, please open an issue. + +## What is different? + +### Command Structure Changes + +As of migrations 5.0, the command structure has changed. The old phinx wrapper +commands have been removed and replaced with new command names: + +**Seeds:** + +``` bash +# Old (4.x and earlier) +bin/cake migrations seed +bin/cake migrations seed --seed Articles + +# New (5.x and later) +bin/cake seeds run +bin/cake seeds run Articles +``` + +The new commands are: + +- `bin/cake seeds run` - Run seed classes +- `bin/cake seeds status` - Show seed execution status +- `bin/cake seeds reset` - Reset seed execution tracking +- `bin/cake bake seed` - Generate new seed classes + +#### Maintaining Backward Compatibility + +If you need to maintain the old command structure for existing scripts or CI/CD +pipelines, you can add command aliases in your application. In your +`src/Application.php` file, add the following to the `console()` method: + +``` php +public function console(CommandCollection $commands): CommandCollection +{ + // Add your application's commands + $commands = $this->addConsoleCommands($commands); + + // Add backward compatibility aliases for migrations 4.x commands + $commands->add('migrations seed', \Migrations\Command\SeedCommand::class); + + return $commands; +} +``` + +For multiple aliases, you can add them all together: + +``` php +// Add multiple backward compatibility aliases +$commands->add('migrations seed', \Migrations\Command\SeedCommand::class); +$commands->add('migrations seed:run', \Migrations\Command\SeedCommand::class); +$commands->add('migrations seed:status', \Migrations\Command\SeedStatusCommand::class); +``` + +This allows gradual migration of scripts and documentation without modifying the +migrations plugin or creating wrapper command classes. + +### API Changes + +If your migrations are using the `AdapterInterface` to fetch rows or update +rows you will need to update your code. If you use `Adapter::query()` to +execute queries, the return of this method is now +`Cake\Database\StatementInterface` instead. This impacts `fetchAll()`, +and `fetch()`: + +``` php +// This +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetchAll(); + +// Now needs to be +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetchAll('assoc'); +``` + +Similar changes are for fetching a single row: + +``` php +// This +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetch(); + +// Now needs to be +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetch('assoc'); +``` + +## Unified Migrations Table + +As of migrations 5.x, there is a new unified `cake_migrations` table that +replaces the legacy `phinxlog` tables. This provides several benefits: + +- **Single table for all migrations**: Instead of separate `phinxlog` (app) + and `{plugin}_phinxlog` (plugins) tables, all migrations are tracked in + one `cake_migrations` table with a `plugin` column. +- **Simpler database schema**: Fewer migration tracking tables to manage. +- **Better plugin support**: Plugin migrations are properly namespaced. + +### Backward Compatibility + +For existing applications with `phinxlog` tables: + +- **Automatic detection**: If any `phinxlog` table exists, migrations will + continue using the legacy tables automatically. +- **No forced migration**: Existing applications don't need to change anything. +- **Opt-in upgrade**: You can migrate to the new table when you're ready. + +### Configuration + +The `Migrations.legacyTables` configuration option controls the behavior: + +``` php +// config/app.php or config/app_local.php +'Migrations' => [ + // null (default): Autodetect - use legacy if phinxlog tables exist + // false: Force use of new cake_migrations table + // true: Force use of legacy phinxlog tables + 'legacyTables' => null, +], +``` + +### Upgrading to the Unified Table + +To migrate from `phinxlog` tables to the new `cake_migrations` table: + +1. **Preview the upgrade** (dry run): + + ``` bash + bin/cake migrations upgrade --dry-run + ``` + +2. **Run the upgrade**: + + ``` bash + bin/cake migrations upgrade + ``` + +3. **Update your configuration**: + + ``` php + // config/app.php + 'Migrations' => [ + 'legacyTables' => false, + ], + ``` + +4. **Optionally drop phinx tables**: Your migration history is preserved + by default. Use `--drop-tables` to drop the `phinxlog`tables after + verifying your migrations run correctly. + + ``` bash + bin/cake migrations upgrade --drop-tables + ``` + +### Rolling Back + +If you need to revert to phinx tables after upgrading: + +1. Set `'legacyTables' => true` in your configuration. + +> [!WARNING] +> You cannot rollback after running `upgrade --drop-tables`. + +### New Installations + +For new applications without any existing `phinxlog` tables, the unified +`cake_migrations` table is used automatically. No configuration is needed. + +## Problems with the builtin backend? + +If your migrations contain errors when run with the builtin backend, please +open [an issue](https://github.com/cakephp/migrations/issues/new). diff --git a/docs/en/upgrading-to-builtin-backend.rst b/docs/en/upgrading-to-builtin-backend.rst deleted file mode 100644 index ea2909433..000000000 --- a/docs/en/upgrading-to-builtin-backend.rst +++ /dev/null @@ -1,198 +0,0 @@ -Upgrading to the builtin backend -################################ - -As of migrations 4.3 there is a new migrations backend that uses CakePHP's -database abstractions and ORM. In 4.4, the ``builtin`` backend became the -default backend. As of migrations 5.0, phinx has been removed as a dependency -and only the builtin backend is supported. This greatly reduces the dependency -footprint of migrations. - -What is the same? -================= - -Your migrations shouldn't have to change much to adapt to the new backend. -The builtin backend provides similar functionality to what was available with -phinx. If your migrations don't work in a way that could be addressed by the -changes outlined below, please open an issue. - -What is different? -================== - -Command Structure Changes -------------------------- - -As of migrations 5.0, the command structure has changed. The old phinx wrapper -commands have been removed and replaced with new command names: - -**Seeds:** - -.. code-block:: bash - - # Old (4.x and earlier) - bin/cake migrations seed - bin/cake migrations seed --seed Articles - - # New (5.x and later) - bin/cake seeds run - bin/cake seeds run Articles - -The new commands are: - -- ``bin/cake seeds run`` - Run seed classes -- ``bin/cake seeds status`` - Show seed execution status -- ``bin/cake seeds reset`` - Reset seed execution tracking -- ``bin/cake bake seed`` - Generate new seed classes - -Maintaining Backward Compatibility -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If you need to maintain the old command structure for existing scripts or CI/CD -pipelines, you can add command aliases in your application. In your -``src/Application.php`` file, add the following to the ``console()`` method: - -.. code-block:: php - - public function console(CommandCollection $commands): CommandCollection - { - // Add your application's commands - $commands = $this->addConsoleCommands($commands); - - // Add backward compatibility aliases for migrations 4.x commands - $commands->add('migrations seed', \Migrations\Command\SeedCommand::class); - - return $commands; - } - -For multiple aliases, you can add them all together: - -.. code-block:: php - - // Add multiple backward compatibility aliases - $commands->add('migrations seed', \Migrations\Command\SeedCommand::class); - $commands->add('migrations seed:run', \Migrations\Command\SeedCommand::class); - $commands->add('migrations seed:status', \Migrations\Command\SeedStatusCommand::class); - -This allows gradual migration of scripts and documentation without modifying the -migrations plugin or creating wrapper command classes. - -API Changes ------------ - -If your migrations are using the ``AdapterInterface`` to fetch rows or update -rows you will need to update your code. If you use ``Adapter::query()`` to -execute queries, the return of this method is now -``Cake\Database\StatementInterface`` instead. This impacts ``fetchAll()``, -and ``fetch()``:: - - // This - $stmt = $this->getAdapter()->query('SELECT * FROM articles'); - $rows = $stmt->fetchAll(); - - // Now needs to be - $stmt = $this->getAdapter()->query('SELECT * FROM articles'); - $rows = $stmt->fetchAll('assoc'); - -Similar changes are for fetching a single row:: - - // This - $stmt = $this->getAdapter()->query('SELECT * FROM articles'); - $rows = $stmt->fetch(); - - // Now needs to be - $stmt = $this->getAdapter()->query('SELECT * FROM articles'); - $rows = $stmt->fetch('assoc'); - -Unified Migrations Table -======================== - -As of migrations 5.x, there is a new unified ``cake_migrations`` table that -replaces the legacy ``phinxlog`` tables. This provides several benefits: - -- **Single table for all migrations**: Instead of separate ``phinxlog`` (app) - and ``{plugin}_phinxlog`` (plugins) tables, all migrations are tracked in - one ``cake_migrations`` table with a ``plugin`` column. -- **Simpler database schema**: Fewer migration tracking tables to manage. -- **Better plugin support**: Plugin migrations are properly namespaced. - -Backward Compatibility ----------------------- - -For existing applications with ``phinxlog`` tables: - -- **Automatic detection**: If any ``phinxlog`` table exists, migrations will - continue using the legacy tables automatically. -- **No forced migration**: Existing applications don't need to change anything. -- **Opt-in upgrade**: You can migrate to the new table when you're ready. - -Configuration -------------- - -The ``Migrations.legacyTables`` configuration option controls the behavior: - -.. code-block:: php - - // config/app.php or config/app_local.php - 'Migrations' => [ - // null (default): Autodetect - use legacy if phinxlog tables exist - // false: Force use of new cake_migrations table - // true: Force use of legacy phinxlog tables - 'legacyTables' => null, - ], - -Upgrading to the Unified Table ------------------------------- - -To migrate from ``phinxlog`` tables to the new ``cake_migrations`` table: - -1. **Preview the upgrade** (dry run): - - .. code-block:: bash - - bin/cake migrations upgrade --dry-run - -2. **Run the upgrade**: - - .. code-block:: bash - - bin/cake migrations upgrade - -3. **Update your configuration**: - - .. code-block:: php - - // config/app.php - 'Migrations' => [ - 'legacyTables' => false, - ], - -4. **Optionally drop phinx tables**: Your migration history is preserved - by default. Use ``--drop-tables`` to drop the ``phinxlog``tables after - verifying your migrations run correctly. - - .. code-block:: bash - - bin/cake migrations upgrade --drop-tables - -Rolling Back ------------- - -If you need to revert to phinx tables after upgrading: - -1. Set ``'legacyTables' => true`` in your configuration. - -.. warning:: - - You cannot rollback after running ``upgrade --drop-tables``. - - -New Installations ------------------ - -For new applications without any existing ``phinxlog`` tables, the unified -``cake_migrations`` table is used automatically. No configuration is needed. - -Problems with the builtin backend? -================================== - -If your migrations contain errors when run with the builtin backend, please -open `an issue `_. diff --git a/docs/en/upgrading.md b/docs/en/upgrading.md new file mode 100644 index 000000000..75b95a539 --- /dev/null +++ b/docs/en/upgrading.md @@ -0,0 +1,162 @@ +# Upgrading from 4.x to 5.x + +Migrations 5.x includes significant changes from 4.x. This guide outlines +the breaking changes and what you need to update when upgrading. + +## Requirements + +- **PHP 8.2+** is now required (was PHP 8.1+) +- **CakePHP 5.3+** is now required +- **Phinx has been removed** - The builtin backend is now the only supported backend + +If you were already using the builtin backend in 4.x (introduced in 4.3, default in 4.4), +the upgrade should be straightforward. See [Upgrading to the builtin backend](upgrading-to-builtin-backend) for more +details on API differences between the phinx and builtin backends. + +## Command Changes + +The phinx wrapper commands have been removed. The new command structure is: + +### Migrations + +The migration commands remain unchanged: + +``` bash +bin/cake migrations migrate +bin/cake migrations rollback +bin/cake migrations status +bin/cake migrations mark_migrated +bin/cake migrations dump +``` + +### Seeds + +Seed commands have changed: + +``` bash +# 4.x # 5.x +bin/cake migrations seed bin/cake seeds run +bin/cake migrations seed --seed X bin/cake seeds run X +``` + +The new seed commands are: + +- `bin/cake seeds run` - Run seed classes +- `bin/cake seeds run SeedName` - Run a specific seed +- `bin/cake seeds status` - Show seed execution status +- `bin/cake seeds reset` - Reset seed execution tracking + +### Maintaining Backward Compatibility + +If you need to maintain the old `migrations seed` command for existing scripts or +CI/CD pipelines, you can add command aliases in your `src/Application.php`: + +``` php +public function console(CommandCollection $commands): CommandCollection +{ + $commands = $this->addConsoleCommands($commands); + + // Add backward compatibility alias + $commands->add('migrations seed', \Migrations\Command\SeedCommand::class); + + return $commands; +} +``` + +## Removed Classes and Namespaces + +The following have been removed in 5.x: + +- `Migrations\Command\Phinx\*` - All phinx wrapper commands +- `Migrations\Command\MigrationsCommand` - Use `bin/cake migrations` entry point +- `Migrations\Command\MigrationsSeedCommand` - Use `bin/cake seeds run` +- `Migrations\Command\MigrationsCacheBuildCommand` - Schema cache is managed differently +- `Migrations\Command\MigrationsCacheClearCommand` - Schema cache is managed differently +- `Migrations\Command\MigrationsCreateCommand` - Use `bin/cake bake migration` + +If you have code that directly references any of these classes, you will need to update it. + +## API Changes + +### Adapter Query Results + +If your migrations use `AdapterInterface::query()` to fetch rows, the return type has +changed from a phinx result to `Cake\Database\StatementInterface`: + +``` php +// 4.x (phinx) +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetchAll(); +$row = $stmt->fetch(); + +// 5.x (builtin) +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetchAll('assoc'); +$row = $stmt->fetch('assoc'); +``` + +## New Features in 5.x + +5.x includes several new features: + +### Seed Tracking + +Seeds are now tracked in a `cake_seeds` table by default, preventing accidental re-runs. +Use `--force` to run a seed again, or `bin/cake seeds reset` to clear tracking. +See [Database Seeding](seeding) for more details. + +### Check Constraints + +Support for database check constraints via `addCheckConstraint()`. +See [Writing Migrations](writing-migrations) for usage details. + +### MySQL ALTER Options + +Support for `ALGORITHM` and `LOCK` options on MySQL ALTER TABLE operations, +allowing control over how MySQL performs schema changes. + +### insertOrSkip() for Seeds + +New `insertOrSkip()` method for seeds to insert records only if they don't already exist, +making seeds more idempotent. + +## Foreign Key Constraint Naming + +Starting in 5.x, when you use `addForeignKey()` without providing an explicit constraint +name, migrations will auto-generate a name using the pattern `{table}_{columns}`. + +Previously, MySQL would auto-generate constraint names (like `articles_ibfk_1`), while +PostgreSQL and SQL Server used migrations-generated names. Now all adapters use the same +consistent naming pattern. + +**Impact on existing migrations:** + +If you have existing migrations that use `addForeignKey()` without explicit names, and +later migrations that reference those constraints by name (e.g., in `dropForeignKey()`), +the generated names may differ between old and new migrations. This could cause +`dropForeignKey()` to fail if it's looking for a name that doesn't exist. + +**Recommendations:** + +1. For new migrations, you can rely on auto-generated names or provide explicit names +2. If you have rollback issues with existing migrations, you may need to update them + to use explicit constraint names +3. The auto-generated names include conflict resolution - if `{table}_{columns}` already + exists, a counter suffix is added (`_2`, `_3`, etc.) + +**Name length limits:** + +Auto-generated names are truncated to respect database limits: + +- MySQL: 61 characters (64 - 3 for counter suffix) +- PostgreSQL: 60 characters (63 - 3) +- SQL Server: 125 characters (128 - 3) +- SQLite: No limit + +## Migration File Compatibility + +Your existing migration files should work without changes in most cases. The builtin backend +provides the same API as phinx for common operations. + +If you encounter issues with existing migrations, please report them at + diff --git a/docs/en/using-the-query-builder.md b/docs/en/using-the-query-builder.md new file mode 100644 index 000000000..297d11359 --- /dev/null +++ b/docs/en/using-the-query-builder.md @@ -0,0 +1,260 @@ +# Using the Query Builder + +It is not uncommon to pair database structure changes with data changes. For example, you may want to +migrate the data in a couple columns from the users to a newly created table. For this type of scenarios, +Migrations provides access to a Query builder object, that you may use to execute complex `SELECT`, `UPDATE`, +`INSERT` or `DELETE` statements. + +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'\`: + +``` php +getQueryBuilder('select'); + $statement = $builder->select('*')->from('users')->execute(); + var_dump($statement->fetchAll()); + } +} +``` + +## Selecting Fields + +Adding fields to the SELECT clause: + +``` php +select(['id', 'title', 'body']); + +// Results in SELECT id AS pk, title AS aliased_title, body ... +$builder->select(['pk' => 'id', 'aliased_title' => 'title', 'body']); + +// Use a closure +$builder->select(function ($builder) { + return ['id', 'title', 'body']; +}); +``` + +## Where Conditions + +Generating conditions: + +``` php +// WHERE id = 1 +$builder->where(['id' => 1]); + +// WHERE id > 1 +$builder->where(['id >' => 1]); +``` + +As you can see you can use any operator by placing it with a space after the field name. Adding multiple conditions is easy as well: + +``` php +where(['id >' => 1])->andWhere(['title' => 'My Title']); + +// Equivalent to +$builder->where(['id >' => 1, 'title' => 'My title']); + +// WHERE id > 1 OR title = 'My title' +$builder->where(['OR' => ['id >' => 1, 'title' => 'My title']]); +``` + +For even more complex conditions you can use closures and expression objects: + +``` php +select('*') + ->from('articles') + ->where(function ($exp) { + return $exp + ->eq('author_id', 2) + ->eq('published', true) + ->notEq('spam', true) + ->gt('view_count', 10); + }); +``` + +Which results in: + +``` sql +SELECT * FROM articles +WHERE + author_id = 2 + AND published = 1 + AND spam != 1 + AND view_count > 10 +``` + +Combining expressions is also possible: + +``` php +select('*') + ->from('articles') + ->where(function ($exp) { + $orConditions = $exp->or_(['author_id' => 2]) + ->eq('author_id', 5); + return $exp + ->not($orConditions) + ->lte('view_count', 10); + }); +``` + +It generates: + +``` sql +SELECT * +FROM articles +WHERE + NOT (author_id = 2 OR author_id = 5) + AND view_count <= 10 +``` + +When using the expression objects you can use the following methods to create conditions: + +- `eq()` Creates an equality condition. +- `notEq()` Create an inequality condition +- `like()` Create a condition using the `LIKE` operator. +- `notLike()` Create a negated `LIKE` condition. +- `in()` Create a condition using `IN`. +- `notIn()` Create a negated condition using `IN`. +- `gt()` Create a `>` condition. +- `gte()` Create a `>=` condition. +- `lt()` Create a `<` condition. +- `lte()` Create a `<=` condition. +- `isNull()` Create an `IS NULL` condition. +- `isNotNull()` Create a negated `IS NULL` condition. + +## Aggregates and SQL Functions + +``` php +select(['count' => $builder->func()->count('*')]); +``` + +A number of commonly used functions can be created with the func() method: + +- `sum()` Calculate a sum. The arguments will be treated as literal values. +- `avg()` Calculate an average. The arguments will be treated as literal values. +- `min()` Calculate the min of a column. The arguments will be treated as literal values. +- `max()` Calculate the max of a column. The arguments will be treated as literal values. +- `count()` Calculate the count. The arguments will be treated as literal values. +- `concat()` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal. +- `coalesce()` Coalesce values. The arguments are treated as bound parameters unless marked as literal. +- `dateDiff()` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal. +- `now()` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date. + +When providing arguments for SQL functions, there are two kinds of parameters you can use, +literal arguments and bound parameters. Literal parameters allow you to reference columns or +other SQL literals. Bound parameters can be used to safely add user data to SQL functions. For example: + +``` php +func()->concat([ + 'title' => 'literal', + ' NEW' +]); +$query->select(['title' => $concat]); +``` + +## Getting Results out of a Query + +Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this: + +``` php +execute()->fetchAll('assoc'); +``` + +## Creating an Insert Query + +Creating insert queries is also possible: + +``` php +getQueryBuilder('insert'); +$builder + ->insert(['first_name', 'last_name']) + ->into('users') + ->values(['first_name' => 'Steve', 'last_name' => 'Jobs']) + ->values(['first_name' => 'Jon', 'last_name' => 'Snow']) + ->execute(); +``` + +For increased performance, you can use another builder object as the values for an insert query: + +``` php +getQueryBuilder('select'); +$namesQuery + ->select(['fname', 'lname']) + ->from('users') + ->where(['is_active' => true]); + +$builder = $this->getQueryBuilder('insert'); +$st = $builder + ->insert(['first_name', 'last_name']) + ->into('names') + ->values($namesQuery) + ->execute(); + +var_dump($st->lastInsertId('names', 'id')); +``` + +The above code will generate: + +``` sql +INSERT INTO names (first_name, last_name) + (SELECT fname, lname FROM USERS where is_active = 1) +``` + +## Creating an update Query + +Creating update queries is similar to both inserting and selecting: + +``` php +getQueryBuilder('update'); +$builder + ->update('users') + ->set('fname', 'Snow') + ->where(['fname' => 'Jon']) + ->execute(); +``` + +## Creating a Delete Query + +Finally, delete queries: + +``` php +getQueryBuilder('delete'); +$builder + ->delete('users') + ->where(['accepted_gdpr' => false]) + ->execute(); +``` diff --git a/docs/en/using-the-query-builder.rst b/docs/en/using-the-query-builder.rst deleted file mode 100644 index 64a7617cb..000000000 --- a/docs/en/using-the-query-builder.rst +++ /dev/null @@ -1,272 +0,0 @@ -Using the Query Builder -####################### - -It is not uncommon to pair database structure changes with data changes. For example, you may want to -migrate the data in a couple columns from the users to a newly created table. For this type of scenarios, -Migrations provides access to a Query builder object, that you may use to execute complex ``SELECT``, ``UPDATE``, -``INSERT`` or ``DELETE`` statements. - -The Query builder is provided by the `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('select'); - $statement = $builder->select('*')->from('users')->execute(); - var_dump($statement->fetchAll()); - } - } - -Selecting Fields ----------------- - -Adding fields to the SELECT clause:: - - select(['id', 'title', 'body']); - - // Results in SELECT id AS pk, title AS aliased_title, body ... - $builder->select(['pk' => 'id', 'aliased_title' => 'title', 'body']); - - // Use a closure - $builder->select(function ($builder) { - return ['id', 'title', 'body']; - }); - - -Where Conditions ----------------- - -Generating conditions:: - - // WHERE id = 1 - $builder->where(['id' => 1]); - - // WHERE id > 1 - $builder->where(['id >' => 1]); - - -As you can see you can use any operator by placing it with a space after the field name. Adding multiple conditions is easy as well:: - - where(['id >' => 1])->andWhere(['title' => 'My Title']); - - // Equivalent to - $builder->where(['id >' => 1, 'title' => 'My title']); - - // WHERE id > 1 OR title = 'My title' - $builder->where(['OR' => ['id >' => 1, 'title' => 'My title']]); - - -For even more complex conditions you can use closures and expression objects:: - - select('*') - ->from('articles') - ->where(function ($exp) { - return $exp - ->eq('author_id', 2) - ->eq('published', true) - ->notEq('spam', true) - ->gt('view_count', 10); - }); - - -Which results in: - -.. code-block:: sql - - SELECT * FROM articles - WHERE - author_id = 2 - AND published = 1 - AND spam != 1 - AND view_count > 10 - - -Combining expressions is also possible:: - - select('*') - ->from('articles') - ->where(function ($exp) { - $orConditions = $exp->or_(['author_id' => 2]) - ->eq('author_id', 5); - return $exp - ->not($orConditions) - ->lte('view_count', 10); - }); - -It generates: - -.. code-block:: sql - - SELECT * - FROM articles - WHERE - NOT (author_id = 2 OR author_id = 5) - AND view_count <= 10 - - -When using the expression objects you can use the following methods to create conditions: - -* ``eq()`` Creates an equality condition. -* ``notEq()`` Create an inequality condition -* ``like()`` Create a condition using the ``LIKE`` operator. -* ``notLike()`` Create a negated ``LIKE`` condition. -* ``in()`` Create a condition using ``IN``. -* ``notIn()`` Create a negated condition using ``IN``. -* ``gt()`` Create a ``>`` condition. -* ``gte()`` Create a ``>=`` condition. -* ``lt()`` Create a ``<`` condition. -* ``lte()`` Create a ``<=`` condition. -* ``isNull()`` Create an ``IS NULL`` condition. -* ``isNotNull()`` Create a negated ``IS NULL`` condition. - - -Aggregates and SQL Functions ----------------------------- - -.. code-block:: php - - select(['count' => $builder->func()->count('*')]); - -A number of commonly used functions can be created with the func() method: - -* ``sum()`` Calculate a sum. The arguments will be treated as literal values. -* ``avg()`` Calculate an average. The arguments will be treated as literal values. -* ``min()`` Calculate the min of a column. The arguments will be treated as literal values. -* ``max()`` Calculate the max of a column. The arguments will be treated as literal values. -* ``count()`` Calculate the count. The arguments will be treated as literal values. -* ``concat()`` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal. -* ``coalesce()`` Coalesce values. The arguments are treated as bound parameters unless marked as literal. -* ``dateDiff()`` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal. -* ``now()`` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date. - -When providing arguments for SQL functions, there are two kinds of parameters you can use, -literal arguments and bound parameters. Literal parameters allow you to reference columns or -other SQL literals. Bound parameters can be used to safely add user data to SQL functions. For example: - - -.. code-block:: php - - func()->concat([ - 'title' => 'literal', - ' NEW' - ]); - $query->select(['title' => $concat]); - - -Getting Results out of a Query ------------------------------- - -Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this: - - -.. code-block:: php - - execute()->fetchAll('assoc'); - - -Creating an Insert Query ------------------------- - -Creating insert queries is also possible: - - -.. code-block:: php - - getQueryBuilder('insert'); - $builder - ->insert(['first_name', 'last_name']) - ->into('users') - ->values(['first_name' => 'Steve', 'last_name' => 'Jobs']) - ->values(['first_name' => 'Jon', 'last_name' => 'Snow']) - ->execute(); - - -For increased performance, you can use another builder object as the values for an insert query: - -.. code-block:: php - - getQueryBuilder('select'); - $namesQuery - ->select(['fname', 'lname']) - ->from('users') - ->where(['is_active' => true]); - - $builder = $this->getQueryBuilder('insert'); - $st = $builder - ->insert(['first_name', 'last_name']) - ->into('names') - ->values($namesQuery) - ->execute(); - - var_dump($st->lastInsertId('names', 'id')); - - -The above code will generate: - -.. code-block:: sql - - INSERT INTO names (first_name, last_name) - (SELECT fname, lname FROM USERS where is_active = 1) - - -Creating an update Query ------------------------- - -Creating update queries is similar to both inserting and selecting: - -.. code-block:: php - - getQueryBuilder('update'); - $builder - ->update('users') - ->set('fname', 'Snow') - ->where(['fname' => 'Jon']) - ->execute(); - - -Creating a Delete Query ------------------------ - -Finally, delete queries: - -.. code-block:: php - - getQueryBuilder('delete'); - $builder - ->delete('users') - ->where(['accepted_gdpr' => false]) - ->execute(); diff --git a/docs/en/writing-migrations.md b/docs/en/writing-migrations.md new file mode 100644 index 000000000..162515f1b --- /dev/null +++ b/docs/en/writing-migrations.md @@ -0,0 +1,2527 @@ +# Writing Migrations + +Migrations are a declarative API that helps you transform your database. Each migration +is represented by a PHP class in a unique file. It is preferred that you write +your migrations using the Migrations API, but raw SQL is also supported. + +## Creating a New Migration + +Let's start by creating a new migration with `bake`: + +``` bash +$ bin/cake bake migration +``` + +This will create a new migration in the format +`YYYYMMDDHHMMSS_my_new_migration.php`, where the first 14 characters are +replaced with the current timestamp down to the second. + +If you have specified multiple migration paths, you will be asked to select +which path to create the new migration in. + +Bake will automatically creates a skeleton migration file with a single method: + +``` php + [ + 'style' => 'anonymous', // or 'traditional' +], +``` + +This configuration also applies to seeds, allowing you to use consistent styling +across your entire project. + +## The Change Method + +Migrations supports 'reversible migrations'. In many scenarios, you +only need to define the `up` logic, and Migrations can figure out how to +generate the rollback operations for you. For example: + +``` php +table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } +} +``` + +When executing this migration, Migrations will create the `user_logins` table on +the way up and automatically figure out how to drop the table on the way down. +Please be aware that when a `change` method exists, Migrations will +ignore the `up` and `down` methods. If you need to use these methods it is +recommended to create a separate migration file. + +> [!NOTE] +> When creating or updating tables inside a `change()` method you must use +> the Table `create()` and `update()` methods. Migrations cannot automatically +> determine whether a `save()` call is creating a new table or modifying an +> existing one. + +The following actions are reversible when done through the Table API in +Migrations, and will be automatically reversed: + +- Creating a table +- Renaming a table +- Adding a column +- Renaming a column +- Adding an index +- Adding a foreign key +- Adding a check constraint + +If a command cannot be reversed then Migrations will throw an +`IrreversibleMigrationException` when it's migrating down. If you wish to +use a command that cannot be reversed in the change function, you can use an +if statement with `$this->isMigratingUp()` to only run things in the +up or down direction. For example: + +``` php +table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + if ($this->isMigratingUp()) { + $table->insert([['user_id' => 1, 'created' => '2020-01-19 03:14:07']]) + ->save(); + } + } +} +``` + +## The Up Method + +The up method is automatically run by Migrations when you are migrating up and it +detects the given migration hasn't been executed previously. You should use the +up method to transform the database with your intended changes. + +## The Down Method + +The down method is automatically run by Migrations when you are migrating down and +it detects the given migration has been executed in the past. You should use +the down method to reverse/undo the transformations described in the up method. + +## The Init Method + +The `init()` method is run by Migrations before the migration methods if it exists. +This can be used for setting common class properties that are then used within +the migration methods. + +## The Should Execute Method + +The `shouldExecute()` method is run by Migrations before executing the migration. +This can be used to prevent the migration from being executed at this time. It always +returns true by default. You can override it in your custom `BaseMigration` +implementation. + +## Working With Tables + +The Table object enables you to easily manipulate database tables using PHP +code. You can retrieve an instance of the Table object by calling the +`table()` method from within your database migration: + +``` php +table('tableName'); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +You can then manipulate this table using the methods provided by the Table +object. + + + +## Adding Columns + +Column types are specified as strings and can be one of: + +- binary +- boolean +- char +- date +- datetime +- decimal +- float +- double +- smallinteger +- integer +- biginteger +- string +- text +- time +- timestamp +- uuid +- binaryuuid +- nativeuuid + +In addition, the MySQL adapter supports `enum`, `set`, `blob`, +`tinyblob`, `mediumblob`, `longblob`, `bit` and `json` column types +(`json` in MySQL 5.7 and above). When providing a limit value and using +`binary`, `varbinary` or `blob` and its subtypes, the retained column type +will be based on required length (see [Limit Option and MySQL](#limit-option-and-mysql) for details). + +With most adapters, the `uuid` and `nativeuuid` column types are aliases, +however with the MySQL adapter + MariaDB, the `nativeuuid` type maps to +a native uuid column instead of `CHAR(36)` like `uuid` does. + +In addition, the Postgres adapter supports `interval`, `json`, `jsonb`, +`uuid`, `cidr`, `inet` and `macaddr` column types (PostgreSQL 9.3 and +above). + +### Valid Column Options + +The following are valid column options: + +For any column type: + +| Option | Description | +|----|----| +| limit | set maximum length for strings, also hints column types in adapters (see note below) | +| length | alias for `limit` | +| default | set default value or action | +| null | allow `NULL` values, defaults to `true` (setting `identity` will override default to `false`) | +| after | specify the column that a new column should be placed after, or use `\Migrations\Db\Adapter\MysqlAdapter::FIRST` to place the column at the start of the table *(only applies to MySQL)* | +| comment | set a text comment on the column | + +For `decimal` and `float` columns: + +| Option | Description | +|----|----| +| precision | total number of digits (e.g., 10 in `DECIMAL(10,2)`) | +| scale | number of digits after the decimal point (e.g., 2 in `DECIMAL(10,2)`) | +| signed | enable or disable the `unsigned` option *(only applies to MySQL)* | + +> [!NOTE] +> **Precision and Scale Terminology** +> +> Migrations follows the SQL standard where `precision` represents the total number of digits, +> and `scale` represents digits after the decimal point. For example, to create `DECIMAL(10,2)` +> (10 total digits with 2 decimal places): +> +> ``` php +> $table->addColumn('price', 'decimal', [ +> 'precision' => 10, // Total digits +> 'scale' => 2, // Decimal places +> ]); +> ``` +> +> This differs from CakePHP's TableSchema which uses `length` for total digits and +> `precision` for decimal places. The migration adapter handles this conversion automatically. + +For `enum` and `set` columns: + +| Option | Description | +|--------|-----------------------------------------------------| +| values | Can be a comma separated list or an array of values | + +For `smallinteger`, `integer` and `biginteger` columns: + +| Option | Description | +|----|----| +| identity | enable or disable automatic incrementing (if enabled, will set `null: false` if `null` option is not set) | +| signed | enable or disable the `unsigned` option *(only applies to MySQL)* | + +For Postgres, when using `identity`, it will utilize the `serial` type +appropriate for the integer size, so that `smallinteger` will give you +`smallserial`, `integer` gives `serial`, and `biginteger` gives +`bigserial`. + +For `date` columns: + +| Option | Description | +|---------|---------------------------------------------| +| default | set default value (use with `CURRENT_DATE`) | + +For `time` columns: + +| Option | Description | +|----|----| +| default | set default value (use with `CURRENT_TIME`) | +| timezone | enable or disable the `with time zone` option *(only applies to Postgres)* | + +For `datetime` columns: + +| Option | Description | +|----|----| +| default | set default value (use with `CURRENT_TIMESTAMP`) | +| timezone | enable or disable the `with time zone` option *(only applies to Postgres)* | + +For `timestamp` columns: + +| Option | Description | +|----|----| +| default | set default value (use with `CURRENT_TIMESTAMP`) | +| update | set an action to be triggered when the row is updated (use with `CURRENT_TIMESTAMP`) *(only applies to MySQL)* | +| timezone | enable or disable the `with time zone` option for `time` and `timestamp` columns *(only applies to Postgres)* | + +You can add `created` and `updated` timestamps to a table using the +`addTimestamps()` method. This method accepts three arguments, where the first +two allow setting alternative names for the columns while the third argument +allows you to enable the `timezone` option for the columns. The defaults for +these arguments are `created`, `updated`, and `false` respectively. For +the first and second argument, if you provide `null`, then the default name +will be used, and if you provide `false`, then that column will not be +created. Please note that attempting to set both to `false` will throw +a `\RuntimeException`. Additionally, you can use the +`addTimestampsWithTimezone()` method, which is an alias to `addTimestamps()` +that will always set the third argument to `true` (see examples below). The +`created` column will have a default set to `CURRENT_TIMESTAMP`. For MySQL +only, `updated` column will have update set to +`CURRENT_TIMESTAMP`: + +``` php +table('users')->addTimestamps()->create(); + // Use defaults (with timezones) + $table = $this->table('users')->addTimestampsWithTimezone()->create(); + + // Override the 'created' column name with 'recorded_at'. + $table = $this->table('books')->addTimestamps('recorded_at')->create(); + + // Override the 'updated' column name with 'amended_at', preserving timezones. + // The two lines below do the same, the second one is simply cleaner. + $table = $this->table('books')->addTimestamps(null, 'amended_at', true)->create(); + $table = $this->table('users')->addTimestampsWithTimezone(null, 'amended_at')->create(); + + // Only add the created column to the table + $table = $this->table('books')->addTimestamps(null, false); + // Only add the updated column to the table + $table = $this->table('users')->addTimestamps(false); + // Note, setting both false will throw a \RuntimeError + } +} +``` + +For `boolean` columns: + +| Option | Description | +|--------|-------------------------------------------------------------------| +| signed | enable or disable the `unsigned` option *(only applies to MySQL)* | + +For `string` and `text` columns: + +| Option | Description | +|----|----| +| collation | set collation that differs from table defaults *(only applies to MySQL)* | +| encoding | set character set that differs from table defaults *(only applies to MySQL)* | + +### Limit Option and MySQL + +When using the MySQL adapter, there are a couple things to consider when working with limits: + +- When using a `string` primary key or index on MySQL 5.7 or below, or the + MyISAM storage engine, and the default charset of `utf8mb4_unicode_ci`, you + must specify a limit less than or equal to 191, or use a different charset. +- Additional hinting of database column type can be made for `integer`, + `text`, `blob`, `tinyblob`, `mediumblob`, `longblob` columns. Using + `limit` with one the following options will modify the column type + accordingly: + +| Limit | Column Type | +|--------------|-------------| +| BLOB_TINY | TINYBLOB | +| BLOB_REGULAR | BLOB | +| BLOB_MEDIUM | MEDIUMBLOB | +| BLOB_LONG | LONGBLOB | +| TEXT_TINY | TINYTEXT | +| TEXT_REGULAR | TEXT | +| TEXT_MEDIUM | MEDIUMTEXT | +| TEXT_LONG | LONGTEXT | +| INT_TINY | TINYINT | +| INT_SMALL | SMALLINT | +| INT_MEDIUM | MEDIUMINT | +| INT_REGULAR | INT | +| INT_BIG | BIGINT | + +For `binary` or `varbinary` types, if limit is set greater than allowed 255 +bytes, the type will be changed to the best matching blob type given the +length: + +``` php +table('cart_items'); +$table->addColumn('user_id', 'integer') + ->addColumn('product_id', 'integer', ['limit' => MysqlAdapter::INT_BIG]) + ->addColumn('subtype_id', 'integer', ['limit' => MysqlAdapter::INT_SMALL]) + ->addColumn('quantity', 'integer', ['limit' => MysqlAdapter::INT_TINY]) + ->create(); +``` + +### Default values with expressions + +If you need to set a default to an expression, you can use a `Literal` to have +the column's default value used without any quoting or escaping. This is helpful +when you want to use a function as a default value: + +``` php +use Migrations\BaseMigration; +use Migrations\Db\Literal; + +class AddSomeColumns extends BaseMigration +{ + public function change(): void + { + $this->table('users') + ->addColumn('uniqid', 'uuid', [ + 'default' => Literal::from('uuid_generate_v4()') + ]) + ->create(); + } +} +``` + + + +### Creating a Table + +Creating a table is really easy using the Table object. Let's create a table to +store a collection of users: + +``` php +table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->create(); + } +} +``` + +Columns are added using the `addColumn()` method. We create a unique index +for both the username and email columns using the `addIndex()` method. +Finally calling `create()` commits the changes to the database. + +> [!NOTE] +> Migrations automatically creates an auto-incrementing primary key column called `id` for every +> table. + +The `id` option sets the name of the automatically created identity field, +while the `primary_key` option selects the field or fields used for primary +key. `id` will always override the `primary_key` option unless it's set to +false. If you don't need a primary key set `id` to false without specifying +a `primary_key`, and no primary key will be created. + +To specify an alternate primary key, you can specify the `primary_key` option +when accessing the Table object. Let's disable the automatic `id` column and +create a primary key using two columns instead: + +``` php +table('followers', ['id' => false, 'primary_key' => ['user_id', 'follower_id']]); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } +} +``` + +Setting a single `primary_key` doesn't enable the `AUTO_INCREMENT` option. +To simply change the name of the primary key, we need to override the default `id` field name: + +``` php +table('followers', ['id' => 'user_id']); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->create(); + } +} +``` + +In addition, the MySQL adapter supports following options: + +| Option | Platform | Description | +|----|----|----| +| comment | MySQL, Postgres | set a text comment on the table | +| collation | MySQL, SqlServer | set the table collation *(defaults to database collation)* | +| row_format | MySQL | set the table row format | +| engine | MySQL | define table engine *(defaults to \`\`InnoDB\`\`)* | +| signed | MySQL | whether the primary key is `signed` *(defaults to \`\`true\`\`)* | +| limit | MySQL | set the maximum length for the primary key | + +By default, the primary key is `signed`. +To set it to be unsigned, pass the `signed` option with a `false` +value, or enable the `Migrations.unsigned_primary_keys` and +`Migrations.unsigned_ints` feature flags (see [Feature Flags](index#feature-flags)). +Both flags should be used together so that foreign key columns match +the primary keys they reference: + +``` php +table('followers', ['signed' => false]); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->create(); + } +} +``` + +If you need to create a table with a different collation than the database, +use: + +``` php +table('categories', [ + 'collation' => 'latin1_german1_ci' + ]) + ->addColumn('title', 'string') + ->create(); + } +} +``` + +Note however this can only be done on table creation : there is currently no way +of adding a column to an existing table with a different collation than the +table or the database. Only `MySQL` and `SqlServer` supports this +configuration key for the time being. + +To view available column types and options, see [Adding Columns](#adding-columns) for details. + +### MySQL ALTER TABLE Options + +::: info Added in version 5.0.0 +`ALGORITHM` and `LOCK` options were added in 5.0.0. +::: + +When modifying tables in MySQL, you can control how the ALTER TABLE operation is +performed using the `algorithm` and `lock` options. This is useful for performing +zero-downtime schema changes on large tables in production environments. + +``` php +table('large_table'); + $table->addIndex(['status'], [ + 'name' => 'idx_status', + ]); + $table->update([ + 'algorithm' => 'INPLACE', + 'lock' => 'NONE', + ]); + } +} +``` + +Available `algorithm` values: + +| Algorithm | Description | +|----|----| +| DEFAULT | Let MySQL choose the algorithm (default behavior) | +| INPLACE | Modify the table in place without copying data (when possible) | +| COPY | Create a copy of the table with the changes (legacy method) | +| INSTANT | Only modify metadata, no table rebuild (MySQL 8.0+, limited operations) | + +Available `lock` values: + +| Lock | Description | +|-----------|----------------------------------------------------------| +| DEFAULT | Use minimal locking for the algorithm (default behavior) | +| NONE | Allow concurrent reads and writes during the operation | +| SHARED | Allow concurrent reads but block writes | +| EXCLUSIVE | Block all reads and writes during the operation | + +> [!NOTE] +> Not all operations support all algorithm/lock combinations. MySQL will raise +> an error if the requested combination is not possible for the operation. +> The `INSTANT` algorithm is only available in MySQL 8.0+ and only for specific +> operations like adding columns at the end of a table. + +> [!WARNING] +> Using `ALGORITHM=INPLACE, LOCK=NONE` does not guarantee zero-downtime for +> all operations. Some operations may still require a table copy or exclusive lock. +> Always test schema changes on a staging environment first. + +### Table Partitioning + +Migrations supports table partitioning for MySQL and PostgreSQL. Partitioning helps +manage large tables by splitting them into smaller, more manageable pieces. + +> [!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(); + } +} +``` + +#### KEY Partitioning (MySQL only) + +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 + { + // Drop the partition + $this->table('orders') + ->dropPartition('p2025') + ->update(); + } +} +``` + +### Saving Changes + +When working with the Table object, Migrations stores certain operations in a +pending changes cache. Once you have made the changes you want to the table, +you must save them. To perform this operation, Migrations provides three methods, +`create()`, `update()`, and `save()`. `create()` will first create +the table and then run the pending changes. `update()` will just run the +pending changes, and should be used when the table already exists. `save()` +is a helper function that checks first if the table exists and if it does not +will run `create()`, else it will run `update()`. + +As stated above, when using the `change()` migration method, you should always +use `create()` or `update()`, and never `save()` as otherwise migrating +and rolling back may result in different states, due to `save()` calling +`create()` when running migrate and then `update()` on rollback. When +using the `up()`/`down()` methods, it is safe to use either `save()` or +the more explicit methods. + +When in doubt with working with tables, it is always recommended to call +the appropriate function and commit any pending changes to the database. + +### Renaming a Column + +To rename a column, access an instance of the Table object then call the +`renameColumn()` method: + +``` php +table('users'); + $table->renameColumn('bio', 'biography') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $table = $this->table('users'); + $table->renameColumn('biography', 'bio') + ->save(); + } +} +``` + +### Adding a Column After Another Column + +When adding a column with the MySQL adapter, you can dictate its position using +the `after` option, where its value is the name of the column to position it +after: + +``` php +table('users'); + $table->addColumn('city', 'string', ['after' => 'email']) + ->update(); + } +} +``` + +This would create the new column `city` and position it after the `email` +column. The `\Migrations\Db\Adapter\MysqlAdapter::FIRST` constant can be used +to specify that the new column should be created as the first column in that +table. + +### Dropping a Column + +To drop a column, use the `removeColumn()` method: + +``` php +table('users'); + $table->removeColumn('short_name') + ->save(); + } +} +``` + +### Specifying a Column Limit + +You can limit the maximum length of a column by using the `limit` option: + +``` php +table('tags'); + $table->addColumn('short_name', 'string', ['limit' => 30]) + ->update(); + } +} +``` + +### Changing Column Attributes + +There are two methods for modifying existing columns: + +#### Updating Columns (Recommended) + +To modify specific column attributes while preserving others, use the `updateColumn()` method. +This method automatically preserves unspecified attributes like defaults, nullability, limits, etc.: + +``` php +table('users'); + // Make email nullable, preserving all other attributes + $users->updateColumn('email', null, ['null' => true]) + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $users = $this->table('users'); + $users->updateColumn('email', null, ['null' => false]) + ->save(); + } +} +``` + +You can pass `null` as the column type to preserve the existing type, or specify a new type: + +``` php +// Preserve type and other attributes, only change nullability +$table->updateColumn('email', null, ['null' => true]); + +// Change type to biginteger, preserve default and other attributes +$table->updateColumn('user_id', 'biginteger'); + +// Change default value, preserve everything else +$table->updateColumn('status', null, ['default' => 'active']); +``` + +The following attributes are automatically preserved by `updateColumn()`: + +- Default values +- NULL/NOT NULL constraint +- Column limit/length +- Decimal scale/precision +- Comments +- Signed/unsigned (for numeric types) +- Collation and encoding +- Enum/set values + +#### Changing Columns (Traditional) + +To completely replace a column definition, use the `changeColumn()` method. +This method requires you to specify all desired column attributes. +See [Valid Column Types](#valid-column-types) and [Valid Column Options](#valid-column-options) for allowed values: + +``` php +table('users'); + // Must specify all attributes + $users->changeColumn('email', 'string', [ + 'limit' => 255, + 'null' => true, + 'default' => null, + ]) + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +You can enable attribute preservation with `changeColumn()` by passing +`'preserveUnspecified' => true` in the options: + +``` php +$table->changeColumn('email', 'string', [ + 'null' => true, + 'preserveUnspecified' => true, +]); +``` + +> [!NOTE] +> For most use cases, `updateColumn()` is recommended as it is safer and requires +> less code. Use `changeColumn()` when you need to completely redefine a column +> or when working with legacy code that expects the traditional behavior. + +### Working With Indexes + +To add an index to a table you can simply call the `addIndex()` method on the +table object: + +``` php +table('users'); + $table->addColumn('city', 'string') + ->addIndex(['city']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +By default Migrations instructs the database adapter to create a simple index. We +can pass an additional parameter `unique` to the `addIndex()` method to +specify a unique index. We can also explicitly specify a name for the index +using the `name` parameter, the index columns sort order can also be specified using +the `order` parameter. The order parameter takes an array of column names and sort order key/value pairs: + +``` php +table('users'); + $table->addColumn('email', 'string') + ->addColumn('username','string') + ->addIndex(['email', 'username'], [ + 'unique' => true, + 'name' => 'idx_users_email', + 'order' => ['email' => 'DESC', 'username' => 'ASC']] + ) + ->save(); + } +} +``` + +As of 4.6.0, you can use `BaseMigration::index()` to get a fluent builder to +define indexes: + +``` php +table('users'); + $table->addColumn('email', 'string') + ->addColumn('username','string') + ->addIndex( + $this->index(['email', 'username']) + ->setType('unique') + ->setName('idx_users_email') + ->setOrder(['email' => 'DESC', 'username' => 'ASC']) + ) + ->save(); + } +} +``` + +The MySQL adapter also supports `fulltext` indexes. If you are using a version before 5.6 you must +ensure the table uses the `MyISAM` engine: + +``` php +table('users', ['engine' => 'MyISAM']); + $table->addColumn('email', 'string') + ->addIndex('email', ['type' => 'fulltext']) + ->create(); + } +} +``` + +MySQL adapter supports setting the index length defined by limit option. +When you are using a multi-column index, you are able to define each column index length. +The single column index can define its index length with or without defining column name in limit option: + +``` php +table('users'); + $table->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(); + } +} +``` + +The SQL Server and PostgreSQL adapters support `include` (non-key) columns on indexes: + +``` php +table('users'); + $table->addColumn('email', 'string') + ->addColumn('firstname','string') + ->addColumn('lastname','string') + ->addIndex(['email'], ['include' => ['firstname', 'lastname']]) + ->create(); + } +} +``` + +PostgreSQL, SQLServer, and SQLite support partial indexes by defining where +clauses for the index: + +``` php +table('users'); + $table->addColumn('email', 'string') + ->addColumn('is_verified','boolean') + ->addIndex( + $this->index('email') + ->setName('user_email_verified_idx') + ->setType('unique') + ->setWhere('is_verified = true') + ) + ->create(); + } +} +``` + +PostgreSQL can create indexes concurrently which avoids taking disruptive locks +during index creation: + +``` php +table('users'); + $table->addColumn('email', 'string') + ->addIndex( + $this->index('email') + ->setName('user_email_unique_idx') + ->setType('unique') + ->setConcurrently(true) + ) + ->create(); + } +} +``` + +PostgreSQL adapters also supports Generalized Inverted Index `gin` indexes: + +``` php +table('users'); + $table->addColumn('address', 'string') + ->addIndex('address', ['type' => 'gin']) + ->create(); + } +} +``` + +Removing indexes is as easy as calling the `removeIndex()` method. You must +call this method for each index: + +``` php +table('users'); + $table->removeIndex(['email']) + ->save(); + + // alternatively, you can delete an index by its name, ie: + $table->removeIndexByName('idx_users_email') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +::: info Added in version 4.6.0 +`Index::setWhere()`, and `Index::setConcurrently()` were added. +::: + +### Working With Foreign Keys + +Migrations has support for creating foreign key constraints on your database tables. +Let's add a foreign key to an example table: + +``` php +table('tags'); + $table->addColumn('tag_name', 'string') + ->save(); + + $refTable = $this->table('tag_relationships'); + $refTable->addColumn('tag_id', 'integer', ['null' => true]) + ->addForeignKey( + 'tag_id', + 'tags', + 'id', + ['delete'=> 'SET_NULL', 'update'=> 'NO_ACTION'], + ) + ->save(); + + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +The 'delete' and 'update' options allow you to define the `ON UPDATE` and `ON DELETE` behavior. Possibles values are 'SET_NULL', 'NO_ACTION', 'CASCADE' and +'RESTRICT'. If 'SET_NULL' is used then the column must be created as nullable +with the option `['null' => true]`. + +Foreign keys can be defined with arrays of columns to build constraints between +tables with composite keys: + +``` php +table('follower_events'); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('event_id', 'integer') + ->addForeignKey( + ['user_id', 'follower_id'], + 'followers', + ['user_id', 'follower_id'], + [ + 'delete'=> 'NO_ACTION', + 'update'=> 'NO_ACTION', + 'constraint' => 'user_follower_id', + ] + ) + ->save(); + } +} +``` + +The options parameter of `addForeignKey()` supports the following options: + +| 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) | + +Using the `foreignKey()` method provides a fluent builder to define a foreign +key: + +``` php +table('articles'); + $table->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. +::: + +We can also easily check if a foreign key exists: + +``` php +table('tag_relationships'); + $exists = $table->hasForeignKey('tag_id'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +Finally, to delete a foreign key, use the `dropForeignKey` method. + +Note that like other methods in the `Table` class, `dropForeignKey` also +needs `save()` to be called at the end in order to be executed. This allows +Migrations to intelligently plan migrations when more than one table is +involved: + +``` php +table('tag_relationships'); + $table->dropForeignKey('tag_id')->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +### 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. +They are particularly useful for ensuring data integrity across your application. + +> [!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 + +You can add a check constraint to a table using the `addCheckConstraint()` method: + +``` php +table('products'); + $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) + ->addCheckConstraint('price_positive', 'price > 0') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $table = $this->table('products'); + $table->dropCheckConstraint('price_positive') + ->save(); + } +} +``` + +The first argument is the constraint name, and the second is the SQL expression +that defines the constraint. The expression should evaluate to a boolean value. + +#### Using the CheckConstraint Fluent Builder + +For more complex scenarios, you can use the `checkConstraint()` method to get +a fluent builder: + +``` php +table('users'); + $table->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(); + } +} +``` + +#### Auto-Generated Constraint Names + +If you don't specify a constraint name, one will be automatically generated based +on the table name and expression hash: + +``` php +table('inventory'); + $table->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 +table('date_ranges'); + $table->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 if a Check Constraint Exists + +You can verify if a check constraint exists using the `hasCheckConstraint()` method: + +``` php +table('products'); + $exists = $table->hasCheckConstraint('price_positive'); + if ($exists) { + // do something + } else { + $table->addCheckConstraint('price_positive', 'price > 0') + ->save(); + } + } +} +``` + +#### Dropping a Check Constraint + +To remove a check constraint, use the `dropCheckConstraint()` method with the +constraint name: + +``` php +table('products'); + $table->dropCheckConstraint('price_positive') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $table = $this->table('products'); + $table->addCheckConstraint('price_positive', 'price > 0') + ->save(); + } +} +``` + +> [!NOTE] +> Like other table operations, `dropCheckConstraint()` requires `save()` +> to be called to execute the change. + +#### Database-Specific Behavior + +**MySQL (8.0.16+)** + +Check constraints are fully supported. MySQL stores constraint metadata in the +`INFORMATION_SCHEMA.CHECK_CONSTRAINTS` table. + +**PostgreSQL** + +Check constraints are fully supported and stored in the `pg_constraint` catalog. +PostgreSQL allows the most flexible expressions in check constraints. + +**SQLite** + +Check constraints are supported but with some limitations. SQLite does not support +`ALTER TABLE` operations for check constraints, so adding or dropping constraints +requires recreating the entire table. This is handled automatically by the adapter. + +**SQL Server** + +Check constraint support for SQL Server is planned for a future release. + +### Determining Whether a Table Exists + +You can determine whether or not a table exists by using the `hasTable()` +method: + +``` php +hasTable('users'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +### Dropping a Table + +Tables can be dropped quite easily using the `drop()` method. It is a +good idea to recreate the table again in the `down()` method. + +Note that like other methods in the `Table` class, `drop` also needs `save()` +to be called at the end in order to be executed. This allows Migrations to intelligently +plan migrations when more than one table is involved: + +``` php +table('users')->drop()->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $users = $this->table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->save(); + } +} +``` + +### Renaming a Table + +To rename a table access an instance of the Table object then call the +`rename()` method: + +``` php +table('users'); + $table + ->rename('legacy_users') + ->update(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $table = $this->table('legacy_users'); + $table + ->rename('users') + ->update(); + } +} +``` + +### Changing the Primary Key + +To change the primary key on an existing table, use the `changePrimaryKey()` +method. Pass in a column name or array of columns names to include in the +primary key, or `null` to drop the primary key. Note that the mentioned +columns must be added to the table, they will not be added implicitly: + +``` php +table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->addColumn('new_id', 'integer', ['null' => false]) + ->changePrimaryKey(['new_id', 'username']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +### Creating Custom Primary Keys + +You can specify a `autoId` property in the Migration class and set it to +`false`, which will turn off the automatic `id` column creation. You will +need to manually create the column that will be used as a primary key and add +it to the table declaration: + +``` php +table('products'); + $table + ->addColumn('id', 'uuid') + ->addPrimaryKey('id') + ->addColumn('name', 'string') + ->addColumn('description', 'text') + ->create(); + } +} +``` + +The above will create a `CHAR(36)` `id` column that is also the primary key. + +When specifying a custom primary key on the command line, you must note +it as the primary key in the id field, otherwise you may get an error +regarding duplicate id fields, i.e.: + +``` bash +bin/cake bake migration CreateProducts id:uuid:primary name:string description:text created modified +``` + +All baked migrations and snapshot will use this new way when necessary. + +> [!WARNING] +> Dealing with primary key can only be done on table creation operations. +> This is due to limitations for some database servers the plugin supports. + +### Changing the Table Comment + +To change the comment on an existing table, use the `changeComment()` method. +Pass in a string to set as the new table comment, or `null` to drop the existing comment: + +``` php +table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->changeComment('This is the table with users auth information, password should be encrypted') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +## Checking Columns + +`BaseMigration` also provides methods for introspecting the current schema, +allowing you to conditionally make changes to schema, or read data. +Schema is inspected **when the migration is run**. + +### Get a column list + +To retrieve all table columns, simply create a `table` object and call +`getColumns()` method. This method will return an array of Column classes with +basic info. Example below: + +``` php +table('users')->getColumns(); + ... + } + + /** + * Migrate Down. + */ + public function down(): void + { + ... + } +} +``` + +### Get a column by name + +To retrieve one table column, simply create a `table` object and call the +`getColumn()` method. This method will return a Column class with basic info +or NULL when the column doesn't exist. Example below: + +``` php +table('users')->getColumn('email'); + ... + } + + /** + * Migrate Down. + */ + public function down(): void + { + ... + } +} +``` + +### Checking whether a column exists + +You can check if a table already has a certain column by using the +`hasColumn()` method: + +``` php +table('user'); + $column = $table->hasColumn('username'); + + if ($column) { + // do something + } + + } +} + + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +### Dropping a Table + +Tables can be dropped quite easily using the `drop()` method. It is a +good idea to recreate the table again in the `down()` method. + +Note that like other methods in the `Table` class, `drop` also needs `save()` +to be called at the end in order to be executed. This allows Migrations to intelligently +plan migrations when more than one table is involved: + +``` php +table('users')->drop()->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $users = $this->table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->save(); + } +} +``` + +### Renaming a Table + +To rename a table access an instance of the Table object then call the +`rename()` method: + +``` php +table('users'); + $table + ->rename('legacy_users') + ->update(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $table = $this->table('legacy_users'); + $table + ->rename('users') + ->update(); + } +} +``` + +### Changing the Primary Key + +To change the primary key on an existing table, use the `changePrimaryKey()` +method. Pass in a column name or array of columns names to include in the +primary key, or `null` to drop the primary key. Note that the mentioned +columns must be added to the table, they will not be added implicitly: + +``` php +table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->addColumn('new_id', 'integer', ['null' => false]) + ->changePrimaryKey(['new_id', 'username']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +### Creating Custom Primary Keys + +You can specify a `autoId` property in the Migration class and set it to +`false`, which will turn off the automatic `id` column creation. You will +need to manually create the column that will be used as a primary key and add +it to the table declaration: + +``` php +table('products'); + $table + ->addColumn('id', 'uuid') + ->addPrimaryKey('id') + ->addColumn('name', 'string') + ->addColumn('description', 'text') + ->create(); + } +} +``` + +The above will create a `CHAR(36)` `id` column that is also the primary key. + +When specifying a custom primary key on the command line, you must note +it as the primary key in the id field, otherwise you may get an error +regarding duplicate id fields, i.e.: + +``` bash +bin/cake bake migration CreateProducts id:uuid:primary name:string description:text created modified +``` + +All baked migrations and snapshot will use this new way when necessary. + +> [!WARNING] +> Dealing with primary key can only be done on table creation operations. +> This is due to limitations for some database servers the plugin supports. + +### Changing the Table Comment + +To change the comment on an existing table, use the `changeComment()` method. +Pass in a string to set as the new table comment, or `null` to drop the existing comment: + +``` php +table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->changeComment('This is the table with users auth information, password should be encrypted') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +## Checking Columns + +`BaseMigration` also provides methods for introspecting the current schema, +allowing you to conditionally make changes to schema, or read data. +Schema is inspected **when the migration is run**. + +### Get a column list + +To retrieve all table columns, simply create a `table` object and call +`getColumns()` method. This method will return an array of Column classes with +basic info. Example below: + +``` php +table('users')->getColumns(); + ... + } + + /** + * Migrate Down. + */ + public function down(): void + { + ... + } +} +``` + +### Get a column by name + +To retrieve one table column, simply create a `table` object and call the +`getColumn()` method. This method will return a Column class with basic info +or NULL when the column doesn't exist. Example below: + +``` php +table('users')->getColumn('email'); + ... + } + + /** + * Migrate Down. + */ + public function down(): void + { + ... + } +} +``` + +### Checking whether a column exists + +You can check if a table already has a certain column by using the +`hasColumn()` method: + +``` php +table('user'); + $column = $table->hasColumn('username'); + + if ($column) { + // do something + } + + } +} +``` + +### Changing templates + +See [Custom Seed Migration Templates](seeding#custom-seed-migration-templates) for how to customize the templates +used to generate migrations. + +## Database-Specific Limitations + +While Migrations aims to provide a database-agnostic API, some features have +database-specific limitations or are not available on all platforms. + +### SQL Server + +The following features are not supported on SQL Server: + +**Check Constraints** + +Check constraints are not currently implemented for SQL Server. Attempting to +use `addCheckConstraint()` or `dropCheckConstraint()` will throw a +`BadMethodCallException`. + +**Table Comments** + +SQL Server does not support table comments. Attempting to use `changeComment()` +will throw a `BadMethodCallException`. + +**INSERT IGNORE / insertOrSkip()** + +SQL Server does not support the `INSERT IGNORE` syntax used by `insertOrSkip()`. +This method will throw a `RuntimeException` on SQL Server. Use `insertOrUpdate()` +instead for upsert operations, which uses `MERGE` statements on SQL Server. + +### SQLite + +**Foreign Key Names** + +SQLite does not support named foreign keys. The foreign key constraint name option +is ignored when creating foreign keys on SQLite. + +**Table Comments** + +SQLite does not support table comments directly. Comments are stored as metadata +but not in the database itself. + +**Check Constraint Modifications** + +SQLite does not support `ALTER TABLE` operations for check constraints. Adding or +dropping check constraints requires recreating the entire table, which is handled +automatically by the adapter. + +**Table Partitioning** + +SQLite does not support table partitioning. + +### PostgreSQL + +**KEY Partitioning** + +PostgreSQL does not support MySQL's `KEY` partitioning type. Use `HASH` +partitioning instead for similar distribution behavior. + +### MySQL/MariaDB + +**insertOrUpdate() Conflict Columns** + +For MySQL, the `$conflictColumns` parameter in `insertOrUpdate()` is ignored +because MySQL's `ON DUPLICATE KEY UPDATE` automatically applies to all unique +constraints. PostgreSQL and SQLite require this parameter to be specified. + +**MariaDB GIS/Geometry** + +Some geometry column features may not work correctly on MariaDB due to differences +in GIS implementation compared to MySQL. diff --git a/docs/en/writing-migrations.rst b/docs/en/writing-migrations.rst deleted file mode 100644 index 6da4195d1..000000000 --- a/docs/en/writing-migrations.rst +++ /dev/null @@ -1,2458 +0,0 @@ -Writing Migrations -################## - -Migrations are a declarative API that helps you transform your database. Each migration -is represented by a PHP class in a unique file. It is preferred that you write -your migrations using the Migrations API, but raw SQL is also supported. - -Creating a New Migration -======================== - -Let's start by creating a new migration with ``bake``: - -.. code-block:: bash - - $ bin/cake bake migration - -This will create a new migration in the format -``YYYYMMDDHHMMSS_my_new_migration.php``, where the first 14 characters are -replaced with the current timestamp down to the second. - -If you have specified multiple migration paths, you will be asked to select -which path to create the new migration in. - -Bake will automatically creates a skeleton migration file with a single method:: - - table('user_logins'); - $table->addColumn('user_id', 'integer') - ->addColumn('created', 'datetime') - ->create(); - } - } - -When executing this migration, Migrations will create the ``user_logins`` table on -the way up and automatically figure out how to drop the table on the way down. -Please be aware that when a ``change`` method exists, Migrations will -ignore the ``up`` and ``down`` methods. If you need to use these methods it is -recommended to create a separate migration file. - -.. note:: - - When creating or updating tables inside a ``change()`` method you must use - the Table ``create()`` and ``update()`` methods. Migrations cannot automatically - determine whether a ``save()`` call is creating a new table or modifying an - existing one. - -The following actions are reversible when done through the Table API in -Migrations, and will be automatically reversed: - -- Creating a table -- Renaming a table -- Adding a column -- Renaming a column -- Adding an index -- Adding a foreign key -- Adding a check constraint - -If a command cannot be reversed then Migrations will throw an -``IrreversibleMigrationException`` when it's migrating down. If you wish to -use a command that cannot be reversed in the change function, you can use an -if statement with ``$this->isMigratingUp()`` to only run things in the -up or down direction. For example:: - - table('user_logins'); - $table->addColumn('user_id', 'integer') - ->addColumn('created', 'datetime') - ->create(); - if ($this->isMigratingUp()) { - $table->insert([['user_id' => 1, 'created' => '2020-01-19 03:14:07']]) - ->save(); - } - } - } - - -The Up Method -============= - -The up method is automatically run by Migrations when you are migrating up and it -detects the given migration hasn't been executed previously. You should use the -up method to transform the database with your intended changes. - -The Down Method -=============== - -The down method is automatically run by Migrations when you are migrating down and -it detects the given migration has been executed in the past. You should use -the down method to reverse/undo the transformations described in the up method. - -The Init Method -=============== - -The ``init()`` method is run by Migrations before the migration methods if it exists. -This can be used for setting common class properties that are then used within -the migration methods. - -The Should Execute Method -========================= - -The ``shouldExecute()`` method is run by Migrations before executing the migration. -This can be used to prevent the migration from being executed at this time. It always -returns true by default. You can override it in your custom ``BaseMigration`` -implementation. - -Working With Tables -=================== - -The Table object enables you to easily manipulate database tables using PHP -code. You can retrieve an instance of the Table object by calling the -``table()`` method from within your database migration:: - - table('tableName'); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -You can then manipulate this table using the methods provided by the Table -object. - -.. _adding-columns: - -Adding Columns -============== - -Column types are specified as strings and can be one of: - -- binary -- boolean -- char -- date -- datetime -- decimal -- float -- double -- smallinteger -- integer -- biginteger -- string -- text -- time -- timestamp -- uuid -- binaryuuid -- nativeuuid - -In addition, the MySQL adapter supports ``enum``, ``set``, ``blob``, -``tinyblob``, ``mediumblob``, ``longblob``, ``bit`` and ``json`` column types -(``json`` in MySQL 5.7 and above). When providing a limit value and using -``binary``, ``varbinary`` or ``blob`` and its subtypes, the retained column type -will be based on required length (see `Limit Option and MySQL`_ for details). - -With most adapters, the ``uuid`` and ``nativeuuid`` column types are aliases, -however with the MySQL adapter + MariaDB, the ``nativeuuid`` type maps to -a native uuid column instead of ``CHAR(36)`` like ``uuid`` does. - -In addition, the Postgres adapter supports ``interval``, ``json``, ``jsonb``, -``uuid``, ``cidr``, ``inet`` and ``macaddr`` column types (PostgreSQL 9.3 and -above). - -Valid Column Options --------------------- - -The following are valid column options: - -For any column type: - -======= =========== -Option Description -======= =========== -limit set maximum length for strings, also hints column types in adapters (see note below) -length alias for ``limit`` -default set default value or action -null allow ``NULL`` values, defaults to ``true`` (setting ``identity`` will override default to ``false``) -after specify the column that a new column should be placed after, or use ``\Migrations\Db\Adapter\MysqlAdapter::FIRST`` to place the column at the start of the table *(only applies to MySQL)* -comment set a text comment on the column -======= =========== - -For ``decimal`` and ``float`` columns: - -========= =========== -Option Description -========= =========== -precision total number of digits (e.g., 10 in ``DECIMAL(10,2)``) -scale number of digits after the decimal point (e.g., 2 in ``DECIMAL(10,2)``) -signed enable or disable the ``unsigned`` option *(only applies to MySQL)* -========= =========== - -.. note:: - - **Precision and Scale Terminology** - - Migrations follows the SQL standard where ``precision`` represents the total number of digits, - and ``scale`` represents digits after the decimal point. For example, to create ``DECIMAL(10,2)`` - (10 total digits with 2 decimal places): - - .. code-block:: php - - $table->addColumn('price', 'decimal', [ - 'precision' => 10, // Total digits - 'scale' => 2, // Decimal places - ]); - - This differs from CakePHP's TableSchema which uses ``length`` for total digits and - ``precision`` for decimal places. The migration adapter handles this conversion automatically. - -For ``enum`` and ``set`` columns: - -========= =========== -Option Description -========= =========== -values Can be a comma separated list or an array of values -========= =========== - -For ``smallinteger``, ``integer`` and ``biginteger`` columns: - -======== =========== -Option Description -======== =========== -identity enable or disable automatic incrementing (if enabled, will set ``null: false`` if ``null`` option is not set) -signed enable or disable the ``unsigned`` option *(only applies to MySQL)* -======== - -For Postgres, when using ``identity``, it will utilize the ``serial`` type -appropriate for the integer size, so that ``smallinteger`` will give you -``smallserial``, ``integer`` gives ``serial``, and ``biginteger`` gives -``bigserial``. - -For ``date`` columns: - -======== =========== -Option Description -======== =========== -default set default value (use with ``CURRENT_DATE``) -======== =========== - -For ``time`` columns: - -======== =========== -Option Description -======== =========== -default set default value (use with ``CURRENT_TIME``) -timezone enable or disable the ``with time zone`` option *(only applies to Postgres)* -======== =========== - -For ``datetime`` columns: - -======== =========== -Option Description -======== =========== -default set default value (use with ``CURRENT_TIMESTAMP``) -timezone enable or disable the ``with time zone`` option *(only applies to Postgres)* -======== =========== - -For ``timestamp`` columns: - -======== =========== -Option Description -======== =========== -default set default value (use with ``CURRENT_TIMESTAMP``) -update set an action to be triggered when the row is updated (use with ``CURRENT_TIMESTAMP``) *(only applies to MySQL)* -timezone enable or disable the ``with time zone`` option for ``time`` and ``timestamp`` columns *(only applies to Postgres)* -======== =========== - -You can add ``created`` and ``updated`` timestamps to a table using the -``addTimestamps()`` method. This method accepts three arguments, where the first -two allow setting alternative names for the columns while the third argument -allows you to enable the ``timezone`` option for the columns. The defaults for -these arguments are ``created``, ``updated``, and ``false`` respectively. For -the first and second argument, if you provide ``null``, then the default name -will be used, and if you provide ``false``, then that column will not be -created. Please note that attempting to set both to ``false`` will throw -a ``\RuntimeException``. Additionally, you can use the -``addTimestampsWithTimezone()`` method, which is an alias to ``addTimestamps()`` -that will always set the third argument to ``true`` (see examples below). The -``created`` column will have a default set to ``CURRENT_TIMESTAMP``. For MySQL -only, ``updated`` column will have update set to -``CURRENT_TIMESTAMP``:: - - table('users')->addTimestamps()->create(); - // Use defaults (with timezones) - $table = $this->table('users')->addTimestampsWithTimezone()->create(); - - // Override the 'created' column name with 'recorded_at'. - $table = $this->table('books')->addTimestamps('recorded_at')->create(); - - // Override the 'updated' column name with 'amended_at', preserving timezones. - // The two lines below do the same, the second one is simply cleaner. - $table = $this->table('books')->addTimestamps(null, 'amended_at', true)->create(); - $table = $this->table('users')->addTimestampsWithTimezone(null, 'amended_at')->create(); - - // Only add the created column to the table - $table = $this->table('books')->addTimestamps(null, false); - // Only add the updated column to the table - $table = $this->table('users')->addTimestamps(false); - // Note, setting both false will throw a \RuntimeError - } - } - -For ``boolean`` columns: - -======== =========== -Option Description -======== =========== -signed enable or disable the ``unsigned`` option *(only applies to MySQL)* -======== =========== - -For ``string`` and ``text`` columns: - -========= =========== -Option Description -========= =========== -collation set collation that differs from table defaults *(only applies to MySQL)* -encoding set character set that differs from table defaults *(only applies to MySQL)* -========= =========== - -Limit Option and MySQL ----------------------- - -When using the MySQL adapter, there are a couple things to consider when working with limits: - -- When using a ``string`` primary key or index on MySQL 5.7 or below, or the - MyISAM storage engine, and the default charset of ``utf8mb4_unicode_ci``, you - must specify a limit less than or equal to 191, or use a different charset. -- Additional hinting of database column type can be made for ``integer``, - ``text``, ``blob``, ``tinyblob``, ``mediumblob``, ``longblob`` columns. Using - ``limit`` with one the following options will modify the column type - accordingly: - -============ ============== -Limit Column Type -============ ============== -BLOB_TINY TINYBLOB -BLOB_REGULAR BLOB -BLOB_MEDIUM MEDIUMBLOB -BLOB_LONG LONGBLOB -TEXT_TINY TINYTEXT -TEXT_REGULAR TEXT -TEXT_MEDIUM MEDIUMTEXT -TEXT_LONG LONGTEXT -INT_TINY TINYINT -INT_SMALL SMALLINT -INT_MEDIUM MEDIUMINT -INT_REGULAR INT -INT_BIG BIGINT -============ ============== - -For ``binary`` or ``varbinary`` types, if limit is set greater than allowed 255 -bytes, the type will be changed to the best matching blob type given the -length:: - - table('cart_items'); - $table->addColumn('user_id', 'integer') - ->addColumn('product_id', 'integer', ['limit' => MysqlAdapter::INT_BIG]) - ->addColumn('subtype_id', 'integer', ['limit' => MysqlAdapter::INT_SMALL]) - ->addColumn('quantity', 'integer', ['limit' => MysqlAdapter::INT_TINY]) - ->create(); - -Default values with expressions -------------------------------- - -If you need to set a default to an expression, you can use a ``Literal`` to have -the column's default value used without any quoting or escaping. This is helpful -when you want to use a function as a default value:: - - use Migrations\BaseMigration; - use Migrations\Db\Literal; - - class AddSomeColumns extends BaseMigration - { - public function change(): void - { - $this->table('users') - ->addColumn('uniqid', 'uuid', [ - 'default' => Literal::from('uuid_generate_v4()') - ]) - ->create(); - } - } - -.. _creating-a-table:: - -Creating a Table ----------------- - -Creating a table is really easy using the Table object. Let's create a table to -store a collection of users:: - - table('users'); - $users->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->addColumn('password_salt', 'string', ['limit' => 40]) - ->addColumn('email', 'string', ['limit' => 100]) - ->addColumn('first_name', 'string', ['limit' => 30]) - ->addColumn('last_name', 'string', ['limit' => 30]) - ->addColumn('created', 'datetime') - ->addColumn('updated', 'datetime', ['null' => true]) - ->addIndex(['username', 'email'], ['unique' => true]) - ->create(); - } - } - -Columns are added using the ``addColumn()`` method. We create a unique index -for both the username and email columns using the ``addIndex()`` method. -Finally calling ``create()`` commits the changes to the database. - -.. note:: - - Migrations automatically creates an auto-incrementing primary key column called ``id`` for every - table. - -The ``id`` option sets the name of the automatically created identity field, -while the ``primary_key`` option selects the field or fields used for primary -key. ``id`` will always override the ``primary_key`` option unless it's set to -false. If you don't need a primary key set ``id`` to false without specifying -a ``primary_key``, and no primary key will be created. - -To specify an alternate primary key, you can specify the ``primary_key`` option -when accessing the Table object. Let's disable the automatic ``id`` column and -create a primary key using two columns instead:: - - table('followers', ['id' => false, 'primary_key' => ['user_id', 'follower_id']]); - $table->addColumn('user_id', 'integer') - ->addColumn('follower_id', 'integer') - ->addColumn('created', 'datetime') - ->create(); - } - } - -Setting a single ``primary_key`` doesn't enable the ``AUTO_INCREMENT`` option. -To simply change the name of the primary key, we need to override the default ``id`` field name:: - - table('followers', ['id' => 'user_id']); - $table->addColumn('follower_id', 'integer') - ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) - ->create(); - } - } - -In addition, the MySQL adapter supports following options: - -========== ================ =========== -Option Platform Description -========== ================ =========== -comment MySQL, Postgres set a text comment on the table -collation MySQL, SqlServer set the table collation *(defaults to database collation)* -row_format MySQL set the table row format -engine MySQL define table engine *(defaults to ``InnoDB``)* -signed MySQL whether the primary key is ``signed`` *(defaults to ``true``)* -limit MySQL set the maximum length for the primary key -========== ================ =========== - -By default, the primary key is ``signed``. -To set it to be unsigned, pass the ``signed`` option with a ``false`` -value, or enable the ``Migrations.unsigned_primary_keys`` and -``Migrations.unsigned_ints`` feature flags (see :ref:`feature-flags`). -Both flags should be used together so that foreign key columns match -the primary keys they reference:: - - table('followers', ['signed' => false]); - $table->addColumn('follower_id', 'integer') - ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) - ->create(); - } - } - -If you need to create a table with a different collation than the database, -use:: - - table('categories', [ - 'collation' => 'latin1_german1_ci' - ]) - ->addColumn('title', 'string') - ->create(); - } - } - -Note however this can only be done on table creation : there is currently no way -of adding a column to an existing table with a different collation than the -table or the database. Only ``MySQL`` and ``SqlServer`` supports this -configuration key for the time being. - -To view available column types and options, see :ref:`adding-columns` for details. - -MySQL ALTER TABLE Options -------------------------- - -.. versionadded:: 5.0.0 - ``ALGORITHM`` and ``LOCK`` options were added in 5.0.0. - -When modifying tables in MySQL, you can control how the ALTER TABLE operation is -performed using the ``algorithm`` and ``lock`` options. This is useful for performing -zero-downtime schema changes on large tables in production environments. - -.. code-block:: php - - table('large_table'); - $table->addIndex(['status'], [ - 'name' => 'idx_status', - ]); - $table->update([ - 'algorithm' => 'INPLACE', - 'lock' => 'NONE', - ]); - } - } - -Available ``algorithm`` values: - -============ =========== -Algorithm Description -============ =========== -DEFAULT Let MySQL choose the algorithm (default behavior) -INPLACE Modify the table in place without copying data (when possible) -COPY Create a copy of the table with the changes (legacy method) -INSTANT Only modify metadata, no table rebuild (MySQL 8.0+, limited operations) -============ =========== - -Available ``lock`` values: - -========= =========== -Lock Description -========= =========== -DEFAULT Use minimal locking for the algorithm (default behavior) -NONE Allow concurrent reads and writes during the operation -SHARED Allow concurrent reads but block writes -EXCLUSIVE Block all reads and writes during the operation -========= =========== - -.. note:: - - Not all operations support all algorithm/lock combinations. MySQL will raise - an error if the requested combination is not possible for the operation. - The ``INSTANT`` algorithm is only available in MySQL 8.0+ and only for specific - operations like adding columns at the end of a table. - -.. warning:: - - Using ``ALGORITHM=INPLACE, LOCK=NONE`` does not guarantee zero-downtime for - all operations. Some operations may still require a table copy or exclusive lock. - Always test schema changes on a staging environment first. - -Table Partitioning ------------------- - -Migrations supports table partitioning for MySQL and PostgreSQL. Partitioning helps -manage large tables by splitting them into smaller, more manageable pieces. - -.. 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:: - - 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:: - - 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:: - - table('sessions'); - $table->addColumn('user_id', 'integer') - ->addColumn('data', 'text') - ->partitionBy(Partition::TYPE_HASH, 'user_id', ['count' => 8]) - ->create(); - } - } - -KEY Partitioning (MySQL only) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -KEY partitioning is similar to HASH but uses MySQL's internal hashing function:: - - 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:: - - 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:: - - table('orders') - ->addPartitionToExisting('p2025', '2026-01-01') - ->update(); - } - - public function down(): void - { - // Drop the partition - $this->table('orders') - ->dropPartition('p2025') - ->update(); - } - } - -Saving Changes --------------- - -When working with the Table object, Migrations stores certain operations in a -pending changes cache. Once you have made the changes you want to the table, -you must save them. To perform this operation, Migrations provides three methods, -``create()``, ``update()``, and ``save()``. ``create()`` will first create -the table and then run the pending changes. ``update()`` will just run the -pending changes, and should be used when the table already exists. ``save()`` -is a helper function that checks first if the table exists and if it does not -will run ``create()``, else it will run ``update()``. - -As stated above, when using the ``change()`` migration method, you should always -use ``create()`` or ``update()``, and never ``save()`` as otherwise migrating -and rolling back may result in different states, due to ``save()`` calling -``create()`` when running migrate and then ``update()`` on rollback. When -using the ``up()``/``down()`` methods, it is safe to use either ``save()`` or -the more explicit methods. - -When in doubt with working with tables, it is always recommended to call -the appropriate function and commit any pending changes to the database. - - -Renaming a Column ------------------ - -To rename a column, access an instance of the Table object then call the -``renameColumn()`` method:: - - table('users'); - $table->renameColumn('bio', 'biography') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('users'); - $table->renameColumn('biography', 'bio') - ->save(); - } - } - -Adding a Column After Another Column ------------------------------------- - -When adding a column with the MySQL adapter, you can dictate its position using -the ``after`` option, where its value is the name of the column to position it -after:: - - table('users'); - $table->addColumn('city', 'string', ['after' => 'email']) - ->update(); - } - } - -This would create the new column ``city`` and position it after the ``email`` -column. The ``\Migrations\Db\Adapter\MysqlAdapter::FIRST`` constant can be used -to specify that the new column should be created as the first column in that -table. - -Dropping a Column ------------------ - -To drop a column, use the ``removeColumn()`` method:: - - table('users'); - $table->removeColumn('short_name') - ->save(); - } - } - - -Specifying a Column Limit -------------------------- - -You can limit the maximum length of a column by using the ``limit`` option:: - - table('tags'); - $table->addColumn('short_name', 'string', ['limit' => 30]) - ->update(); - } - } - -Changing Column Attributes --------------------------- - -There are two methods for modifying existing columns: - -Updating Columns (Recommended) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To modify specific column attributes while preserving others, use the ``updateColumn()`` method. -This method automatically preserves unspecified attributes like defaults, nullability, limits, etc.:: - - table('users'); - // Make email nullable, preserving all other attributes - $users->updateColumn('email', null, ['null' => true]) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $users = $this->table('users'); - $users->updateColumn('email', null, ['null' => false]) - ->save(); - } - } - -You can pass ``null`` as the column type to preserve the existing type, or specify a new type:: - - // Preserve type and other attributes, only change nullability - $table->updateColumn('email', null, ['null' => true]); - - // Change type to biginteger, preserve default and other attributes - $table->updateColumn('user_id', 'biginteger'); - - // Change default value, preserve everything else - $table->updateColumn('status', null, ['default' => 'active']); - -The following attributes are automatically preserved by ``updateColumn()``: - -- Default values -- NULL/NOT NULL constraint -- Column limit/length -- Decimal scale/precision -- Comments -- Signed/unsigned (for numeric types) -- Collation and encoding -- Enum/set values - -Changing Columns (Traditional) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To completely replace a column definition, use the ``changeColumn()`` method. -This method requires you to specify all desired column attributes. -See :ref:`valid-column-types` and `Valid Column Options`_ for allowed values:: - - table('users'); - // Must specify all attributes - $users->changeColumn('email', 'string', [ - 'limit' => 255, - 'null' => true, - 'default' => null, - ]) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -You can enable attribute preservation with ``changeColumn()`` by passing -``'preserveUnspecified' => true`` in the options:: - - $table->changeColumn('email', 'string', [ - 'null' => true, - 'preserveUnspecified' => true, - ]); - -.. note:: - - For most use cases, ``updateColumn()`` is recommended as it is safer and requires - less code. Use ``changeColumn()`` when you need to completely redefine a column - or when working with legacy code that expects the traditional behavior. - -Working With Indexes --------------------- - -To add an index to a table you can simply call the ``addIndex()`` method on the -table object:: - - table('users'); - $table->addColumn('city', 'string') - ->addIndex(['city']) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -By default Migrations instructs the database adapter to create a simple index. We -can pass an additional parameter ``unique`` to the ``addIndex()`` method to -specify a unique index. We can also explicitly specify a name for the index -using the ``name`` parameter, the index columns sort order can also be specified using -the ``order`` parameter. The order parameter takes an array of column names and sort order key/value pairs:: - - table('users'); - $table->addColumn('email', 'string') - ->addColumn('username','string') - ->addIndex(['email', 'username'], [ - 'unique' => true, - 'name' => 'idx_users_email', - 'order' => ['email' => 'DESC', 'username' => 'ASC']] - ) - ->save(); - } - } - -As of 4.6.0, you can use ``BaseMigration::index()`` to get a fluent builder to -define indexes:: - - table('users'); - $table->addColumn('email', 'string') - ->addColumn('username','string') - ->addIndex( - $this->index(['email', 'username']) - ->setType('unique') - ->setName('idx_users_email') - ->setOrder(['email' => 'DESC', 'username' => 'ASC']) - ) - ->save(); - } - } - - -The MySQL adapter also supports ``fulltext`` indexes. If you are using a version before 5.6 you must -ensure the table uses the ``MyISAM`` engine:: - - table('users', ['engine' => 'MyISAM']); - $table->addColumn('email', 'string') - ->addIndex('email', ['type' => 'fulltext']) - ->create(); - } - } - -MySQL adapter supports setting the index length defined by limit option. -When you are using a multi-column index, you are able to define each column index length. -The single column index can define its index length with or without defining column name in limit option:: - - table('users'); - $table->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(); - } - } - -The SQL Server and PostgreSQL adapters support ``include`` (non-key) columns on indexes:: - - table('users'); - $table->addColumn('email', 'string') - ->addColumn('firstname','string') - ->addColumn('lastname','string') - ->addIndex(['email'], ['include' => ['firstname', 'lastname']]) - ->create(); - } - } - -PostgreSQL, SQLServer, and SQLite support partial indexes by defining where -clauses for the index:: - - table('users'); - $table->addColumn('email', 'string') - ->addColumn('is_verified','boolean') - ->addIndex( - $this->index('email') - ->setName('user_email_verified_idx') - ->setType('unique') - ->setWhere('is_verified = true') - ) - ->create(); - } - } - -PostgreSQL can create indexes concurrently which avoids taking disruptive locks -during index creation:: - - table('users'); - $table->addColumn('email', 'string') - ->addIndex( - $this->index('email') - ->setName('user_email_unique_idx') - ->setType('unique') - ->setConcurrently(true) - ) - ->create(); - } - } - -PostgreSQL adapters also supports Generalized Inverted Index ``gin`` indexes:: - - table('users'); - $table->addColumn('address', 'string') - ->addIndex('address', ['type' => 'gin']) - ->create(); - } - } - -Removing indexes is as easy as calling the ``removeIndex()`` method. You must -call this method for each index:: - - table('users'); - $table->removeIndex(['email']) - ->save(); - - // alternatively, you can delete an index by its name, ie: - $table->removeIndexByName('idx_users_email') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -.. versionadded:: 4.6.0 - ``Index::setWhere()``, and ``Index::setConcurrently()`` were added. - - -Working With Foreign Keys -------------------------- - -Migrations has support for creating foreign key constraints on your database tables. -Let's add a foreign key to an example table:: - - table('tags'); - $table->addColumn('tag_name', 'string') - ->save(); - - $refTable = $this->table('tag_relationships'); - $refTable->addColumn('tag_id', 'integer', ['null' => true]) - ->addForeignKey( - 'tag_id', - 'tags', - 'id', - ['delete'=> 'SET_NULL', 'update'=> 'NO_ACTION'], - ) - ->save(); - - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -The 'delete' and 'update' options allow you to define the ``ON UPDATE`` and ``ON -DELETE`` behavior. Possibles values are 'SET_NULL', 'NO_ACTION', 'CASCADE' and -'RESTRICT'. If 'SET_NULL' is used then the column must be created as nullable -with the option ``['null' => true]``. - -Foreign keys can be defined with arrays of columns to build constraints between -tables with composite keys:: - - table('follower_events'); - $table->addColumn('user_id', 'integer') - ->addColumn('follower_id', 'integer') - ->addColumn('event_id', 'integer') - ->addForeignKey( - ['user_id', 'follower_id'], - 'followers', - ['user_id', 'follower_id'], - [ - 'delete'=> 'NO_ACTION', - 'update'=> 'NO_ACTION', - 'constraint' => 'user_follower_id', - ] - ) - ->save(); - } - } - -The options parameter of ``addForeignKey()`` supports the following options: - -========== =========== -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) -========== =========== - -Using the ``foreignKey()`` method provides a fluent builder to define a foreign -key:: - - table('articles'); - $table->addForeignKey( - $this->foreignKey() - ->setColumns('user_id') - ->setReferencedTable('users') - ->setReferencedColumns('user_id') - ->setDelete(ForeignKey::CASCADE) - ->setName('article_user_fk') - ) - ->save(); - } - } - -.. versionadded:: 4.6.0 - The ``foreignKey`` method was added. - -We can also easily check if a foreign key exists:: - - table('tag_relationships'); - $exists = $table->hasForeignKey('tag_id'); - if ($exists) { - // do something - } - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Finally, to delete a foreign key, use the ``dropForeignKey`` method. - -Note that like other methods in the ``Table`` class, ``dropForeignKey`` also -needs ``save()`` to be called at the end in order to be executed. This allows -Migrations to intelligently plan migrations when more than one table is -involved:: - - table('tag_relationships'); - $table->dropForeignKey('tag_id')->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Working With Check Constraints -------------------------------- - -.. versionadded:: 5.0.0 - Check constraints were added in 5.0.0. - -Check constraints allow you to enforce data validation rules at the database level. -They are particularly useful for ensuring data integrity across your application. - -.. 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 -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can add a check constraint to a table using the ``addCheckConstraint()`` method:: - - table('products'); - $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) - ->addCheckConstraint('price_positive', 'price > 0') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('products'); - $table->dropCheckConstraint('price_positive') - ->save(); - } - } - -The first argument is the constraint name, and the second is the SQL expression -that defines the constraint. The expression should evaluate to a boolean value. - -Using the CheckConstraint Fluent Builder -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For more complex scenarios, you can use the ``checkConstraint()`` method to get -a fluent builder:: - - table('users'); - $table->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(); - } - } - -Auto-Generated Constraint Names -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you don't specify a constraint name, one will be automatically generated based -on the table name and expression hash:: - - table('inventory'); - $table->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:: - - table('date_ranges'); - $table->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 if a Check Constraint Exists -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can verify if a check constraint exists using the ``hasCheckConstraint()`` method:: - - table('products'); - $exists = $table->hasCheckConstraint('price_positive'); - if ($exists) { - // do something - } else { - $table->addCheckConstraint('price_positive', 'price > 0') - ->save(); - } - } - } - -Dropping a Check Constraint -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To remove a check constraint, use the ``dropCheckConstraint()`` method with the -constraint name:: - - table('products'); - $table->dropCheckConstraint('price_positive') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('products'); - $table->addCheckConstraint('price_positive', 'price > 0') - ->save(); - } - } - -.. note:: - - Like other table operations, ``dropCheckConstraint()`` requires ``save()`` - to be called to execute the change. - -Database-Specific Behavior -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**MySQL (8.0.16+)** - -Check constraints are fully supported. MySQL stores constraint metadata in the -``INFORMATION_SCHEMA.CHECK_CONSTRAINTS`` table. - -**PostgreSQL** - -Check constraints are fully supported and stored in the ``pg_constraint`` catalog. -PostgreSQL allows the most flexible expressions in check constraints. - -**SQLite** - -Check constraints are supported but with some limitations. SQLite does not support -``ALTER TABLE`` operations for check constraints, so adding or dropping constraints -requires recreating the entire table. This is handled automatically by the adapter. - -**SQL Server** - -Check constraint support for SQL Server is planned for a future release. - -Determining Whether a Table Exists ----------------------------------- - -You can determine whether or not a table exists by using the ``hasTable()`` -method:: - - hasTable('users'); - if ($exists) { - // do something - } - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Dropping a Table ----------------- - -Tables can be dropped quite easily using the ``drop()`` method. It is a -good idea to recreate the table again in the ``down()`` method. - -Note that like other methods in the ``Table`` class, ``drop`` also needs ``save()`` -to be called at the end in order to be executed. This allows Migrations to intelligently -plan migrations when more than one table is involved:: - - table('users')->drop()->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $users = $this->table('users'); - $users->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->addColumn('password_salt', 'string', ['limit' => 40]) - ->addColumn('email', 'string', ['limit' => 100]) - ->addColumn('first_name', 'string', ['limit' => 30]) - ->addColumn('last_name', 'string', ['limit' => 30]) - ->addColumn('created', 'datetime') - ->addColumn('updated', 'datetime', ['null' => true]) - ->addIndex(['username', 'email'], ['unique' => true]) - ->save(); - } - } - -Renaming a Table ----------------- - -To rename a table access an instance of the Table object then call the -``rename()`` method:: - - table('users'); - $table - ->rename('legacy_users') - ->update(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('legacy_users'); - $table - ->rename('users') - ->update(); - } - } - -Changing the Primary Key ------------------------- - -To change the primary key on an existing table, use the ``changePrimaryKey()`` -method. Pass in a column name or array of columns names to include in the -primary key, or ``null`` to drop the primary key. Note that the mentioned -columns must be added to the table, they will not be added implicitly:: - - table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->addColumn('new_id', 'integer', ['null' => false]) - ->changePrimaryKey(['new_id', 'username']) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Creating Custom Primary Keys ----------------------------- - -You can specify a ``autoId`` property in the Migration class and set it to -``false``, which will turn off the automatic ``id`` column creation. You will -need to manually create the column that will be used as a primary key and add -it to the table declaration:: - - table('products'); - $table - ->addColumn('id', 'uuid') - ->addPrimaryKey('id') - ->addColumn('name', 'string') - ->addColumn('description', 'text') - ->create(); - } - } - -The above will create a ``CHAR(36)`` ``id`` column that is also the primary key. - -When specifying a custom primary key on the command line, you must note -it as the primary key in the id field, otherwise you may get an error -regarding duplicate id fields, i.e.: - -.. code-block:: bash - - bin/cake bake migration CreateProducts id:uuid:primary name:string description:text created modified - - -All baked migrations and snapshot will use this new way when necessary. - -.. warning:: - - Dealing with primary key can only be done on table creation operations. - This is due to limitations for some database servers the plugin supports. - -Changing the Table Comment --------------------------- - -To change the comment on an existing table, use the ``changeComment()`` method. -Pass in a string to set as the new table comment, or ``null`` to drop the existing comment:: - - table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->changeComment('This is the table with users auth information, password should be encrypted') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Checking Columns -================ - -``BaseMigration`` also provides methods for introspecting the current schema, -allowing you to conditionally make changes to schema, or read data. -Schema is inspected **when the migration is run**. - -Get a column list ------------------ - -To retrieve all table columns, simply create a ``table`` object and call -``getColumns()`` method. This method will return an array of Column classes with -basic info. Example below:: - - table('users')->getColumns(); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } - } - -Get a column by name --------------------- - -To retrieve one table column, simply create a ``table`` object and call the -``getColumn()`` method. This method will return a Column class with basic info -or NULL when the column doesn't exist. Example below:: - - table('users')->getColumn('email'); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } - } - -Checking whether a column exists --------------------------------- - -You can check if a table already has a certain column by using the -``hasColumn()`` method:: - - table('user'); - $column = $table->hasColumn('username'); - - if ($column) { - // do something - } - - } - } - - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Dropping a Table ----------------- - -Tables can be dropped quite easily using the ``drop()`` method. It is a -good idea to recreate the table again in the ``down()`` method. - -Note that like other methods in the ``Table`` class, ``drop`` also needs ``save()`` -to be called at the end in order to be executed. This allows Migrations to intelligently -plan migrations when more than one table is involved:: - - table('users')->drop()->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $users = $this->table('users'); - $users->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->addColumn('password_salt', 'string', ['limit' => 40]) - ->addColumn('email', 'string', ['limit' => 100]) - ->addColumn('first_name', 'string', ['limit' => 30]) - ->addColumn('last_name', 'string', ['limit' => 30]) - ->addColumn('created', 'datetime') - ->addColumn('updated', 'datetime', ['null' => true]) - ->addIndex(['username', 'email'], ['unique' => true]) - ->save(); - } - } - -Renaming a Table ----------------- - -To rename a table access an instance of the Table object then call the -``rename()`` method:: - - table('users'); - $table - ->rename('legacy_users') - ->update(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('legacy_users'); - $table - ->rename('users') - ->update(); - } - } - -Changing the Primary Key ------------------------- - -To change the primary key on an existing table, use the ``changePrimaryKey()`` -method. Pass in a column name or array of columns names to include in the -primary key, or ``null`` to drop the primary key. Note that the mentioned -columns must be added to the table, they will not be added implicitly:: - - table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->addColumn('new_id', 'integer', ['null' => false]) - ->changePrimaryKey(['new_id', 'username']) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Creating Custom Primary Keys ----------------------------- - -You can specify a ``autoId`` property in the Migration class and set it to -``false``, which will turn off the automatic ``id`` column creation. You will -need to manually create the column that will be used as a primary key and add -it to the table declaration:: - - table('products'); - $table - ->addColumn('id', 'uuid') - ->addPrimaryKey('id') - ->addColumn('name', 'string') - ->addColumn('description', 'text') - ->create(); - } - } - -The above will create a ``CHAR(36)`` ``id`` column that is also the primary key. - -When specifying a custom primary key on the command line, you must note -it as the primary key in the id field, otherwise you may get an error -regarding duplicate id fields, i.e.: - -.. code-block:: bash - - bin/cake bake migration CreateProducts id:uuid:primary name:string description:text created modified - - -All baked migrations and snapshot will use this new way when necessary. - -.. warning:: - - Dealing with primary key can only be done on table creation operations. - This is due to limitations for some database servers the plugin supports. - -Changing the Table Comment --------------------------- - -To change the comment on an existing table, use the ``changeComment()`` method. -Pass in a string to set as the new table comment, or ``null`` to drop the existing comment:: - - table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->changeComment('This is the table with users auth information, password should be encrypted') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Checking Columns -================ - -``BaseMigration`` also provides methods for introspecting the current schema, -allowing you to conditionally make changes to schema, or read data. -Schema is inspected **when the migration is run**. - -Get a column list ------------------ - -To retrieve all table columns, simply create a ``table`` object and call -``getColumns()`` method. This method will return an array of Column classes with -basic info. Example below:: - - table('users')->getColumns(); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } - } - -Get a column by name --------------------- - -To retrieve one table column, simply create a ``table`` object and call the -``getColumn()`` method. This method will return a Column class with basic info -or NULL when the column doesn't exist. Example below:: - - table('users')->getColumn('email'); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } - } - -Checking whether a column exists --------------------------------- - -You can check if a table already has a certain column by using the -``hasColumn()`` method:: - - table('user'); - $column = $table->hasColumn('username'); - - if ($column) { - // do something - } - - } - } - - -Changing templates ------------------- - -See :ref:`custom-seed-migration-templates` for how to customize the templates -used to generate migrations. - -Database-Specific Limitations -============================= - -While Migrations aims to provide a database-agnostic API, some features have -database-specific limitations or are not available on all platforms. - -SQL Server ----------- - -The following features are not supported on SQL Server: - -**Check Constraints** - -Check constraints are not currently implemented for SQL Server. Attempting to -use ``addCheckConstraint()`` or ``dropCheckConstraint()`` will throw a -``BadMethodCallException``. - -**Table Comments** - -SQL Server does not support table comments. Attempting to use ``changeComment()`` -will throw a ``BadMethodCallException``. - -**INSERT IGNORE / insertOrSkip()** - -SQL Server does not support the ``INSERT IGNORE`` syntax used by ``insertOrSkip()``. -This method will throw a ``RuntimeException`` on SQL Server. Use ``insertOrUpdate()`` -instead for upsert operations, which uses ``MERGE`` statements on SQL Server. - -SQLite ------- - -**Foreign Key Names** - -SQLite does not support named foreign keys. The foreign key constraint name option -is ignored when creating foreign keys on SQLite. - -**Table Comments** - -SQLite does not support table comments directly. Comments are stored as metadata -but not in the database itself. - -**Check Constraint Modifications** - -SQLite does not support ``ALTER TABLE`` operations for check constraints. Adding or -dropping check constraints requires recreating the entire table, which is handled -automatically by the adapter. - -**Table Partitioning** - -SQLite does not support table partitioning. - -PostgreSQL ----------- - -**KEY Partitioning** - -PostgreSQL does not support MySQL's ``KEY`` partitioning type. Use ``HASH`` -partitioning instead for similar distribution behavior. - -MySQL/MariaDB -------------- - -**insertOrUpdate() Conflict Columns** - -For MySQL, the ``$conflictColumns`` parameter in ``insertOrUpdate()`` is ignored -because MySQL's ``ON DUPLICATE KEY UPDATE`` automatically applies to all unique -constraints. PostgreSQL and SQLite require this parameter to be specified. - -**MariaDB GIS/Geometry** - -Some geometry column features may not work correctly on MariaDB due to differences -in GIS implementation compared to MySQL. diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 000000000..f22cc50b3 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,2104 @@ +{ + "name": "docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "docs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@cakephp/docs-skeleton": "github:cakephp/docs-skeleton#node-package", + "vitepress": "^2.0.0-alpha.16" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cakephp/docs-skeleton": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/cakephp/docs-skeleton.git#beb4b50177ce8fd81d5113d2e3fc49b107ddf2f2", + "bin": { + "cakedocs": "bin/cakedocs.js" + }, + "peerDependencies": { + "vitepress": "^2.0.0-alpha.15" + } + }, + "node_modules/@docsearch/css": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.6.2.tgz", + "integrity": "sha512-fH/cn8BjEEdM2nJdjNMHIvOVYupG6AIDtFVDgIZrNzdCSj4KXr9kd+hsehqsNGYjpUjObeKYKvgy/IwCb1jZYQ==", + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.6.2.tgz", + "integrity": "sha512-qj1yoxl3y4GKoK7+VM6fq/rQqPnvUmg3IKzJ9x0VzN14QVzdB/SG/J6VfV1BWT5RcPUFxIcVwoY1fwHM2fSRRw==", + "license": "MIT" + }, + "node_modules/@docsearch/sidepanel-js": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.6.2.tgz", + "integrity": "sha512-Pni85AP/GwRj7fFg8cBJp0U04tzbueBvWSd3gysgnOsVnQVSZwSYncfErUScLE1CAtR+qocPDFjmYR9AMRNJtQ==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.75", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.75.tgz", + "integrity": "sha512-KvcCUbvcBWb0sbqLIxHoY8z5/piXY08wcY9gfMhF+ph3AfzGMaSmZFkUY71HSXAljQngXkgs4bdKdekO0HQWvg==", + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.23.0.tgz", + "integrity": "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz", + "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.31", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz", + "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz", + "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.31", + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz", + "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz", + "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.1" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", + "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", + "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz", + "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz", + "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz", + "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/runtime-core": "3.5.31", + "@vue/shared": "3.5.31", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz", + "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "vue": "3.5.31" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz", + "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", + "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/integrations": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.2.1.tgz", + "integrity": "sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7 || ^8", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", + "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", + "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/focus-trap": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.0.1.tgz", + "integrity": "sha512-9ptSG6z51YQOstI/oN4XuVGP/03u2nh0g//qz7L6zX0i6PZiPnkcf3GenXq7N2hZnASXaMxTPpbKwdI+PFvxlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", + "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", + "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/engine-javascript": "3.23.0", + "@shikijs/engine-oniguruma": "3.23.0", + "@shikijs/langs": "3.23.0", + "@shikijs/themes": "3.23.0", + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.17.tgz", + "integrity": "sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@docsearch/css": "^4.5.3", + "@docsearch/js": "^4.5.3", + "@docsearch/sidepanel-js": "^4.5.3", + "@iconify-json/simple-icons": "^1.2.69", + "@shikijs/core": "^3.22.0", + "@shikijs/transformers": "^3.22.0", + "@shikijs/types": "^3.22.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^6.0.4", + "@vue/devtools-api": "^8.0.5", + "@vue/shared": "^3.5.27", + "@vueuse/core": "^14.2.0", + "@vueuse/integrations": "^14.2.0", + "focus-trap": "^8.0.0", + "mark.js": "8.11.1", + "minisearch": "^7.2.0", + "shiki": "^3.22.0", + "vite": "^7.3.1", + "vue": "^3.5.27" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "oxc-minify": "*", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "oxc-minify": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz", + "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-sfc": "3.5.31", + "@vue/runtime-dom": "3.5.31", + "@vue/server-renderer": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..fb3f3a14b --- /dev/null +++ b/docs/package.json @@ -0,0 +1,19 @@ +{ + "name": "docs", + "version": "1.0.0", + "description": "", + "main": "config.js", + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@cakephp/docs-skeleton": "github:cakephp/docs-skeleton#node-package", + "vitepress": "^2.0.0-alpha.16" + } +}