diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 000000000..ec319b782
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,24 @@
+# Security Policy
+
+## Supported Versions
+
+Security fixes are applied to all active versions listed in the
+[version map](https://github.com/cakephp/migrations/wiki#version-map).
+Versions marked as EOL no longer receive fixes.
+
+## Reporting a Vulnerability
+
+If you've found a security issue in CakePHP Migrations, please use the following procedure
+instead of the normal bug reporting system. Instead of using the bug tracker,
+or one of the support forums please send an email to security [at] cakephp.org. Emails
+sent to this address go to the CakePHP core team on a private mailing list.
+
+For each report, we try to first confirm the vulnerability. Once confirmed,
+the CakePHP team will take the following actions:
+
+* Acknowledge to the reporter that we've received the issue, and are
+ working on a fix. We ask that the reporter keep the issue confidential until we announce it.
+* Get a fix/patch prepared.
+* Prepare a post describing the vulnerability, and the possible exploits.
+* Release new versions of all affected versions.
+* Prominently feature the problem in the release announcement
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0d2da9cfc..b1ca9ed4b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,13 +3,17 @@ name: CI
on:
push:
branches:
- - 3.x
- 4.x
- 4.next
- 5.x
+ - 5.next
+ paths-ignore:
+ - docs/**
pull_request:
branches:
- '*'
+ paths-ignore:
+ - docs/**
workflow_dispatch:
permissions:
@@ -54,7 +58,7 @@ jobs:
--health-retries 5
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
persist-credentials: false
@@ -114,7 +118,7 @@ jobs:
run: echo "date=$(date +'%Y-%m')" >> $GITHUB_OUTPUT
- name: Cache composer dependencies
- uses: actions/cache@v5
+ uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }}
@@ -162,7 +166,7 @@ jobs:
- name: Code Coverage Report
if: success() && matrix.php-version == '8.1' && matrix.db-type == 'mysql'
- uses: codecov/codecov-action@v5
+ uses: codecov/codecov-action@v7
testsuite-windows:
runs-on: windows-2022
@@ -173,7 +177,7 @@ jobs:
PHP_VERSION: '8.2'
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
with:
persist-credentials: false
@@ -191,7 +195,7 @@ jobs:
key: ${{ steps.key-date.outputs.date }}
- name: Cache PHP extensions
- uses: actions/cache@v5
+ uses: actions/cache@v6
with:
path: ${{ steps.php-ext-cache.outputs.dir }}
key: ${{ runner.os }}-php-ext-${{ steps.php-ext-cache.outputs.key }}
@@ -219,7 +223,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer dependencies
- uses: actions/cache@v5
+ uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }}
@@ -236,7 +240,7 @@ jobs:
vendor/bin/phpunit --coverage-clover=coverage.xml
- name: Submit code coverage
- uses: codecov/codecov-action@v5
+ uses: codecov/codecov-action@v7
cs-stan:
uses: cakephp/.github/.github/workflows/cs-stan.yml@5.x
diff --git a/.github/workflows/deploy_docs_3x.yml b/.github/workflows/deploy_docs_3x.yml
index 1bfda3583..dff7cc3e5 100644
--- a/.github/workflows/deploy_docs_3x.yml
+++ b/.github/workflows/deploy_docs_3x.yml
@@ -7,17 +7,20 @@ on:
- 3.x
workflow_dispatch:
+permissions:
+ contents: read
+
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Cloning repo
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Push to dokku
- uses: dokku/github-action@master
+ uses: dokku/github-action@cc7dec1d2b9fed249a14ae462bc953bba436f78c # v1.10.0
with:
git_remote_url: 'ssh://dokku@apps.cakephp.org:22/migrations-docs-3'
ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }}
diff --git a/.github/workflows/deploy_docs_4x.yml b/.github/workflows/deploy_docs_4x.yml
index 8f04a50db..2cd9660f1 100644
--- a/.github/workflows/deploy_docs_4x.yml
+++ b/.github/workflows/deploy_docs_4x.yml
@@ -7,17 +7,20 @@ on:
- 4.x
workflow_dispatch:
+permissions:
+ contents: read
+
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Cloning repo
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Push to dokku
- uses: dokku/github-action@master
+ uses: dokku/github-action@cc7dec1d2b9fed249a14ae462bc953bba436f78c # v1.10.0
with:
git_remote_url: 'ssh://dokku@apps.cakephp.org:22/migrations-docs-4'
ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }}
diff --git a/.github/workflows/deploy_docs_5x.yml b/.github/workflows/deploy_docs_5x.yml
index bc6f5ad9c..96a729890 100644
--- a/.github/workflows/deploy_docs_5x.yml
+++ b/.github/workflows/deploy_docs_5x.yml
@@ -7,17 +7,21 @@ on:
- 5.x
workflow_dispatch:
+permissions:
+ contents: read
+
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Cloning repo
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Push to dokku
- uses: dokku/github-action@master
+ uses: dokku/github-action@cc7dec1d2b9fed249a14ae462bc953bba436f78c # v1.10.0
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/.github/workflows/docs-validation.yml b/.github/workflows/docs-validation.yml
new file mode 100644
index 000000000..439a52c18
--- /dev/null
+++ b/.github/workflows/docs-validation.yml
@@ -0,0 +1,27 @@
+name: Documentation Validation
+
+on:
+ push:
+ branches:
+ - 5.x
+ paths:
+ - 'docs/**'
+ - '.github/**'
+ pull_request:
+ paths:
+ - 'docs/**'
+ - '.github/**'
+
+jobs:
+ validate:
+ uses: cakephp/.github/.github/workflows/docs-validation.yml@5.x
+ with:
+ docs-path: 'docs'
+ vitepress-path: 'docs/.vitepress'
+ enable-config-js-check: true
+ enable-json-lint: true
+ enable-toc-check: true
+ enable-spell-check: true
+ enable-markdown-lint: true
+ enable-link-check: true
+ tools-ref: '5.x'
diff --git a/.gitignore b/.gitignore
index 1cb7676d0..7d1294ec9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ error.log
/tests/test_app/config/TestsMigrations/schema-dump-test.lock
/tests/test_app/Plugin/TestBlog/config/Migrations/*
.phpunit.cache
+.phpcs.cache
.ddev
# IDE and editor specific files #
diff --git a/.phive/phars.xml b/.phive/phars.xml
index 4d447d1a0..e116501f8 100644
--- a/.phive/phars.xml
+++ b/.phive/phars.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/Dockerfile b/Dockerfile
index 70ceb413d..d1f129524 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:24-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 (angie)
+# ----------------------
+FROM docker.angie.software/angie:latest AS runner
+
+# Copy built files
+COPY --from=builder /app/docs/.vitepress/dist /usr/share/angie/html
+
+# Expose port
+EXPOSE 80
+
+# Health check (optional)
+HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1
+
+# Start angie
+CMD ["angie", "-g", "daemon off;"]
diff --git a/composer.json b/composer.json
index 1dac84d00..8cdefb5e9 100644
--- a/composer.json
+++ b/composer.json
@@ -24,20 +24,20 @@
"require": {
"php": ">=8.2",
"cakephp/cache": "^5.3.0",
- "cakephp/database": "^5.3.0",
+ "cakephp/database": "^5.3.2",
"cakephp/orm": "^5.3.0"
},
"require-dev": {
"cakephp/bake": "^3.3",
"cakephp/cakephp": "^5.3.2",
"cakephp/cakephp-codesniffer": "^5.0",
- "phpunit/phpunit": "^11.5.3 || ^12.1.3"
+ "phpunit/phpunit": "^11.5.3 || ^12.1.3 || ^13.0"
},
"suggest": {
"cakephp/bake": "If you want to generate migrations.",
"dereuromark/cakephp-ide-helper": "If you want to have IDE suggest/autocomplete when creating migrations."
},
- "minimum-stability": "dev",
+ "minimum-stability": "stable",
"prefer-stable": true,
"autoload": {
"psr-4": {
@@ -68,14 +68,17 @@
"@stan",
"@test"
],
- "cs-check": "phpcs --parallel=16 -p",
- "cs-fix": "phpcbf --parallel=16 -p",
+ "cs-check": "phpcs",
+ "cs-fix": "phpcbf",
"phpstan": "tools/phpstan analyse",
"stan": "@phpstan",
"stan-baseline": "tools/phpstan --generate-baseline",
"stan-setup": "phive install",
"lowest": "validate-prefer-lowest",
"lowest-setup": "composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction && cp composer.json composer.backup && composer require --dev dereuromark/composer-prefer-lowest && mv composer.backup composer.json",
+ "rector-setup": "cp composer.json composer.backup && composer require --dev rector/rector:\"~2.3.1\" phpstan/phpstan:\"~2.1.40\" && mv composer.backup composer.json",
+ "rector-check": "vendor/bin/rector process --dry-run",
+ "rector-fix": "vendor/bin/rector process",
"test": "phpunit",
"test-coverage": "phpunit --coverage-clover=clover.xml"
}
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..b32992f07
--- /dev/null
+++ b/docs/.vitepress/config.js
@@ -0,0 +1,53 @@
+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/", 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: '/migrations/5/',
+ rewrites: {
+ "en/:slug*": ":slug*",
+ },
+ sitemap: {
+ hostname: "https://book.cakephp.org/migrations/5/",
+ },
+ themeConfig: {
+ siteTitle: false,
+ pluginName: "Migrations",
+ 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..91d6d65e8
--- /dev/null
+++ b/docs/.vitepress/toc_en.json
@@ -0,0 +1,45 @@
+{
+ "/": [
+ {
+ "text": "Getting Started",
+ "collapsed": false,
+ "items": [
+ { "text": "Migrations 5.x", "link": "/" },
+ { "text": "Installation and Overview", "link": "/getting-started/installation-and-overview" },
+ { "text": "Creating Migrations", "link": "/getting-started/creating-migrations" },
+ { "text": "Snapshots and Diffs", "link": "/getting-started/snapshots-and-diffs" },
+ { "text": "Running and Managing Migrations", "link": "/getting-started/running-and-managing-migrations" }
+ ]
+ },
+ {
+ "text": "Guides",
+ "collapsed": false,
+ "items": [
+ { "text": "Integration and Deployment", "link": "/advanced/integration-and-deployment" },
+ {
+ "text": "Writing Migrations",
+ "link": "/guides/writing-migrations/migration-methods",
+ "items": [
+ { "text": "Migration Methods", "link": "/guides/writing-migrations/migration-methods" },
+ { "text": "Columns and Table Operations", "link": "/guides/writing-migrations/columns-and-table-operations" },
+ { "text": "Indexes and Constraints", "link": "/guides/writing-migrations/indexes-and-constraints" },
+ { "text": "Schema Introspection and Platform Limitations", "link": "/guides/writing-migrations/schema-introspection-and-platform-limitations" }
+ ]
+ },
+ { "text": "Using the Query Builder", "link": "/guides/using-the-query-builder" },
+ { "text": "Executing Queries", "link": "/guides/executing-queries" },
+ { "text": "Inserting Data", "link": "/guides/inserting-data" },
+ { "text": "Database Seeding", "link": "/guides/seeding" }
+ ]
+ },
+ {
+ "text": "Upgrade Guides",
+ "collapsed": true,
+ "items": [
+ { "text": "Upgrading from 4.x to 5.x", "link": "/upgrades/upgrading-from-4-x" },
+ { "text": "Upgrading to the Builtin Backend", "link": "/upgrades/upgrading-to-builtin-backend" },
+ { "text": "Upgrading to Capability Interfaces", "link": "/upgrades/upgrading-to-capability-interfaces" }
+ ]
+ }
+ ]
+}
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/advanced/integration-and-deployment.md b/docs/en/advanced/integration-and-deployment.md
new file mode 100644
index 000000000..4426968d9
--- /dev/null
+++ b/docs/en/advanced/integration-and-deployment.md
@@ -0,0 +1,242 @@
+# Integration and Deployment
+
+This guide covers test integration, plugin usage, programmatic execution, and
+deployment-related operational concerns.
+
+## 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 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 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, use `runMany()`:
+
+```php
+$migrator->runMany([
+ ['plugin' => 'Contacts'],
+ ['plugin' => 'Documents', 'connection' => 'test_docs'],
+]);
+```
+
+If your database also contains tables that are not managed by your application,
+such as those created by PostGIS, you can exclude those tables from drop and
+truncate behavior using the `skip` option:
+
+```php
+$migrator->run(['connection' => 'test', 'skip' => ['postgis*']]);
+```
+
+The `skip` option accepts an `fnmatch()` compatible pattern.
+
+## Using Migrations in Plugins
+
+Plugins can also provide migration files. All commands in the Migrations plugin
+support the `--plugin` or `-p` option to scope execution to the migrations
+relative to that plugin:
+
+```bash
+bin/cake migrations status -p PluginName
+bin/cake migrations migrate -p PluginName
+```
+
+To check the app and every loaded plugin in a single call — a typical need on
+deploy gates — use `migrations status --all` instead of looping over plugins
+manually:
+
+```bash
+bin/cake migrations status --all
+```
+
+The command prints a compact per-section summary and exits non-zero when
+anything is pending, so it slots straight into a CI step. See
+[Checking All Plugins at Once](../getting-started/running-and-managing-migrations#checking-all-plugins-at-once)
+for the full description of the output and exit codes.
+
+## 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 the `Migrations\Migrations`
+class. This can be handy when you are developing a plugin installer for a CMS,
+for instance.
+
+The `Migrations` class allows you to run the following commands:
+
+- `migrate`
+- `rollback`
+- `markMigrated`
+- `status`
+- `seed`
+
+Each of these commands has a corresponding method defined in the
+`Migrations\Migrations` class.
+
+```php
+use Migrations\Migrations;
+
+$migrations = new Migrations();
+
+$status = $migrations->status();
+$migrate = $migrations->migrate();
+$rollback = $migrations->rollback();
+$markMigrated = $migrations->markMigrated(20150804222900);
+$seeded = $migrations->seed();
+```
+
+The methods can accept an array of parameters that match the shell command
+options:
+
+```php
+use Migrations\Migrations;
+
+$migrations = new Migrations();
+$status = $migrations->status([
+ 'connection' => 'custom',
+ 'source' => 'MyMigrationsFolder',
+]);
+```
+
+The only exception is `markMigrated()`, which expects the version number as the
+first argument and the options array as the second.
+
+Optionally, you can pass default parameters in the constructor:
+
+```php
+use Migrations\Migrations;
+
+$migrations = new Migrations([
+ 'connection' => 'custom',
+ 'source' => 'MyMigrationsFolder',
+]);
+
+$status = $migrations->status();
+$migrate = $migrations->migrate();
+```
+
+If you need to override one or more default parameters for one call, pass them
+to the method:
+
+```php
+use Migrations\Migrations;
+
+$migrations = new Migrations([
+ 'connection' => 'custom',
+ 'source' => 'MyMigrationsFolder',
+]);
+
+$status = $migrations->status();
+$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 nullable by
+ default? Default: `false`
+- `add_timestamps_use_datetime`: Should Migrations use `DATETIME` type columns
+ for the columns added by `addTimestamps()`?
+
+Set them via `Configure`, for example 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`.
+
+## Skipping the `schema.lock` File Generation
+
+In order for the diff feature to work, a `.lock` file is generated every time
+you migrate, roll back, 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 to production, by using the `--no-lock` option:
+
+```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, 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 with 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/console-commands/schema-cache.html) that you can use:
+
+```bash
+bin/cake migrations 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));
+```
+
+You can add the `'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 get more IDE support for 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/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.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.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/getting-started/creating-migrations.md b/docs/en/getting-started/creating-migrations.md
new file mode 100644
index 000000000..ae18c9088
--- /dev/null
+++ b/docs/en/getting-started/creating-migrations.md
@@ -0,0 +1,362 @@
+# Creating Migrations
+
+Migration files are stored in the `config/Migrations` directory of your
+application. The names of the migration files are prefixed with the date in
+which they were created, in the format
+`YYYYMMDDHHMMSS_MigrationName.php`.
+
+Examples:
+
+- `20160121163850_CreateProducts.php`
+- `20160210133047_AddRatingToProducts.php`
+
+The easiest way to create a migration file is by using `bin/cake bake migration`:
+
+```bash
+bin/cake bake migration CreateProducts
+```
+
+This creates an empty migration that you can edit to add any columns, indexes,
+and foreign keys you need. See [Writing Migrations](../guides/writing-migrations)
+for more information on using `Table` objects to define schema changes.
+
+> [!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 and 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 create-table and
+ add-field generation.
+- `/^(Alter).*(?:On)(.*)/` Alters fields from the specified table.
+
+You can also use the `underscore_form` as the name for your migrations, such as
+`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 that case, you may need to
+> rename the migration manually.
+
+## Anonymous Migration Classes
+
+Migrations also supports generating anonymous migration classes, which use PHP's
+anonymous class feature instead of named classes. This style is useful for:
+
+- Avoiding namespace declarations
+- Better PHPCS compatibility (no class name to filename matching required)
+- Simpler file structure without named class constraints
+- More readable filenames like `2024_12_08_120000_CreateProducts.php`
+
+To generate an anonymous migration class, use the `--style anonymous` option:
+
+```bash
+bin/cake bake migration CreateProducts --style anonymous
+```
+
+This generates a migration file using an anonymous class:
+
+```php
+ [
+ 'style' => 'anonymous', // or 'traditional'
+],
+```
+
+This configuration also applies to seeds, allowing you to use consistent
+styling across your entire project.
+
+## Creating a Table
+
+You can use `bake migration` to create a table:
+
+```bash
+bin/cake bake migration CreateProducts name:string description:text created modified
+```
+
+The command 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 generated migration afterwards to customize the columns further.
+
+Columns on the command line follow this pattern:
+
+```text
+fieldName:fieldType?[length]:default[value]:indexType:indexName
+```
+
+Examples of valid email field definitions:
+
+- `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 include precision and scale:
+
+- `amount:decimal[5,2]`
+- `amount:decimal?[5,2]`
+
+Columns with a question mark after the field type are nullable.
+
+The `length` part is optional and should always be written between brackets.
+
+The `default[value]` part is optional and sets the default value for the
+column. Supported value types include:
+
+- Booleans: `true` or `false` such as `active:boolean:default[true]`
+- Integers: `0`, `123`, `-456` such as `count:integer:default[0]`
+- Floats: `1.5`, `-2.75` such as `rate:decimal:default[1.5]`
+- Strings: `'hello'` or `"world"` such as
+ `status:string:default['pending']`
+- Null: `null` or `NULL` such as `description:text?:default[null]`
+- SQL expressions: `CURRENT_TIMESTAMP` such as
+ `created_at:datetime:default[CURRENT_TIMESTAMP]`
+
+Fields named `created` and `modified`, as well as any field with an `_at`
+suffix, will automatically be set to the type `datetime`.
+
+There are some heuristics for choosing field types when left unspecified or set
+to an invalid value. The default field type is `string`:
+
+- `id`: `integer`
+- `created`, `modified`, `updated`: `datetime`
+- `latitude`, `longitude`, `lat`, `lng`: `decimal`
+
+Additionally, you can create an empty migration file if you want full control
+over what needs to be executed by omitting column definitions:
+
+```bash
+bin/cake migrations create MyCustomMigration
+```
+
+## Adding Columns to an Existing Table
+
+If the migration name is of the form `AddXXXToYYY` and is followed by a list of
+column names and types, a migration file containing the code for creating the
+columns will be generated:
+
+```bash
+bin/cake bake migration AddPriceToProducts price:decimal[5,2]
+```
+
+This generates:
+
+```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
+```
+
+This 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]
+```
+
+This 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 if the
+migration name is of the form `AlterXXXOnYYY`:
+
+```bash
+bin/cake bake migration AlterPriceOnProducts name:float
+```
+
+This 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 a
+> float.
+
+## Removing a Column
+
+In the same way, you can generate a migration to remove a column if the
+migration name is of the form `RemoveXXXFromYYY`:
+
+```bash
+bin/cake bake migration RemovePriceFromProducts price
+```
+
+This creates:
+
+```php
+table('products');
+ $table->removeColumn('price')
+ ->save();
+ }
+}
+```
+
+> [!NOTE]
+> `removeColumn()` is not reversible, so it must be called in the `up()`
+> method. Add a corresponding `addColumn()` call to the `down()` method.
diff --git a/docs/en/getting-started/installation-and-overview.md b/docs/en/getting-started/installation-and-overview.md
new file mode 100644
index 000000000..940a197ac
--- /dev/null
+++ b/docs/en/getting-started/installation-and-overview.md
@@ -0,0 +1,101 @@
+# Installation and Overview
+
+This guide covers installation, plugin loading, and the basic migration model
+used by the Migrations plugin.
+
+## Installation
+
+By default Migrations is installed with the application skeleton. If you've
+removed it and want to re-install it, run the following from your application's
+root directory:
+
+```bash
+php composer.phar require cakephp/migrations "@stable"
+
+# Or if composer is installed globally
+composer require cakephp/migrations "@stable"
+```
+
+To use the plugin, load it in your application's `config/bootstrap.php` file:
+
+```bash
+bin/cake plugin load Migrations
+```
+
+Or load the plugin in `src/Application.php`:
+
+```php
+$this->addPlugin('Migrations');
+```
+
+Additionally, configure the default database connection in `config/app.php` as
+explained in the [Database Configuration section](https://book.cakephp.org/5/en/orm/database-basics.html#database-configuration).
+
+## 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. Use the CLI commands to apply and
+> roll back migrations.
+
+Once the file has been created in the `config/Migrations` folder, you can apply
+it:
+
+```bash
+bin/cake migrations migrate
+```
+
+## Next Steps
+
+- Use [Creating Migrations](creating-migrations) to generate migration files
+ with `bake`
+- Use [Running and Managing Migrations](running-and-managing-migrations) to
+ apply, roll back, and inspect migrations
+- Use [Writing Migrations](../guides/writing-migrations) for the full Table API and
+ migration authoring reference
diff --git a/docs/en/getting-started/running-and-managing-migrations.md b/docs/en/getting-started/running-and-managing-migrations.md
new file mode 100644
index 000000000..7c51a4f9a
--- /dev/null
+++ b/docs/en/getting-started/running-and-managing-migrations.md
@@ -0,0 +1,167 @@
+# Running and Managing Migrations
+
+This guide covers the commands you use after writing or generating migration
+files.
+
+## Applying Migrations
+
+Once you have generated or written your migration file, apply the changes to
+your database:
+
+```bash
+# Run all the migrations
+bin/cake migrations migrate
+
+# Migrate to a specific version using the --target option
+bin/cake migrations migrate -t 20150103081132
+
+# Run migrations from a custom source directory
+bin/cake migrations migrate -s Alternate
+
+# Run migrations against a different connection
+bin/cake migrations migrate -c my_custom_connection
+
+# Run migrations for a plugin
+bin/cake migrations migrate -p MyAwesomePlugin
+```
+
+## Reverting Migrations
+
+The `rollback` command undoes previously executed migrations:
+
+```bash
+# Roll back to the previous migration
+bin/cake migrations rollback
+
+# Roll back 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.
+
+## Viewing Migration Status
+
+The `status` command prints a list of all migrations, along with their current
+status:
+
+```bash
+bin/cake migrations status
+```
+
+You can also output the results as JSON:
+
+```bash
+bin/cake migrations status --format json
+```
+
+You can also use the `--source`, `--connection`, and `--plugin` options just
+like for the `migrate` command.
+
+### Checking All Plugins at Once
+
+The `--all` flag prints the status for the app and every loaded plugin that
+ships migrations in a single call:
+
+```bash
+bin/cake migrations status --all
+```
+
+The default output is a compact summary listing only sections that need
+action:
+
+```text
+Summary: 2 of 3 sections require action:
+ - APP: 2 pending
+ - Migrator: 1 pending
+```
+
+When everything is migrated, it collapses to a single
+`Summary: all N sections are up to date.` line. Add `-v` to also print the
+full per-section migration tables before the summary.
+
+The exit code reflects the worst state across all sections — `0` when clean,
+`3` (`CODE_STATUS_DOWN`) when migrations are pending, `2`
+(`CODE_STATUS_MISSING`) when entries in the tracking table no longer have
+matching files. This makes `status --all` directly usable as a deploy gate
+in CI.
+
+`--format json` returns one combined object keyed by section name,
+e.g. `{"app": [...], "PluginName": [...]}`. `--all` cannot be combined with
+`--plugin` or `--cleanup`.
+
+### 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 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, use the `mark_migrated` command.
+
+You can mark all migrations as migrated:
+
+```bash
+bin/cake migrations mark_migrated
+```
+
+You can also mark all migrations up to a specific version 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, use the `--exclude` flag:
+
+```bash
+bin/cake migrations mark_migrated --target=20151016204000 --exclude
+```
+
+If you wish to mark only the targeted migration as migrated, 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 `cake bake migration_snapshot`, the created
+> migration will automatically be marked as migrated. To prevent this behavior,
+> for example for unit test migrations, use the `--generate-only` flag.
+
+This command also accepts the migration version number as a positional
+argument:
+
+```bash
+bin/cake migrations mark_migrated 20150420082532
+```
+
+If you wish to mark all migrations as migrated, you can use the `all` special
+value:
+
+```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 useful for generating data for development environments.
+
+By default, seeds are looked for in the `config/Seeds/` directory of your
+application. See [Database Seeding](../guides/seeding) for how to build and use seed
+classes.
diff --git a/docs/en/getting-started/snapshots-and-diffs.md b/docs/en/getting-started/snapshots-and-diffs.md
new file mode 100644
index 000000000..a5b8f1d9c
--- /dev/null
+++ b/docs/en/getting-started/snapshots-and-diffs.md
@@ -0,0 +1,116 @@
+# Snapshots and Diffs
+
+This guide covers bootstrapping migrations from an existing database and
+generating migration diffs from schema changes.
+
+## Generating Migration Snapshots from an Existing Database
+
+If you have a pre-existing database and want to start using migrations, or want
+to version-control the initial schema of your application, run
+`bake migration_snapshot`:
+
+```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, 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 `--require-table`, the shell will look through your application's
+`Table` classes and will only add the model tables to the snapshot.
+
+If you want to generate a snapshot without marking it as migrated, for example
+for use in unit tests, 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.
+
+To bake a snapshot for a plugin, use the `--plugin` option:
+
+```bash
+bin/cake bake migration_snapshot Initial --plugin MyPlugin
+```
+
+Only the tables that 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 unless you use `--generate-only`.
+
+## 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 the database schema:
+
+```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, 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 had 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
+`bake migration_diff` whenever you need to capture them.
+
+> [!NOTE]
+> Migration diff generation cannot detect column renamings.
+
+## Generating a Dump File
+
+The dump command creates a file to be used with `bake migration_diff`:
+
+```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 `bake migration_diff` to properly compute
+diffs when your application is dealing with multiple databases, possibly from
+different vendors.
+
+Dump files are created in the same directory as your migration files.
+
+You can also use the `--source`, `--connection`, and `--plugin` options just
+like for the `migrate` command.
diff --git a/docs/en/guides/executing-queries.md b/docs/en/guides/executing-queries.md
new file mode 100644
index 000000000..6f3d605ce
--- /dev/null
+++ b/docs/en/guides/executing-queries.md
@@ -0,0 +1,78 @@
+# 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
+ {
+
+ }
+}
+```
diff --git a/docs/en/guides/inserting-data.md b/docs/en/guides/inserting-data.md
new file mode 100644
index 000000000..d9661c035
--- /dev/null
+++ b/docs/en/guides/inserting-data.md
@@ -0,0 +1,56 @@
+# 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 insert methods inside `change()`. Use `up()` and `down()`
+> instead.
diff --git a/docs/en/guides/seeding.md b/docs/en/guides/seeding.md
new file mode 100644
index 000000000..13411cd7d
--- /dev/null
+++ b/docs/en/guides/seeding.md
@@ -0,0 +1,593 @@
+# 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.
+
+Plugin seeds are tracked with their plugin name, so the `plugin` option is
+required to see or reset them:
+
+```bash
+bin/cake seeds status --plugin PluginName
+bin/cake seeds reset --plugin PluginName
+```
+
+### 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/guides/using-the-query-builder.md b/docs/en/guides/using-the-query-builder.md
new file mode 100644
index 000000000..1f65cbc17
--- /dev/null
+++ b/docs/en/guides/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/guides/writing-migrations/columns-and-table-operations.md b/docs/en/guides/writing-migrations/columns-and-table-operations.md
new file mode 100644
index 000000000..1d83fdc4a
--- /dev/null
+++ b/docs/en/guides/writing-migrations/columns-and-table-operations.md
@@ -0,0 +1,552 @@
+# Columns and Table Operations
+
+
+
+## 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`, `macaddr` and `citext` column types (PostgreSQL 9.3 and above).
+The `citext` type requires the `citext` extension to be enabled on the database;
+the migration does not create the extension automatically — run
+`CREATE EXTENSION IF NOT EXISTS citext` first (for example from an earlier
+migration via `$this->execute(...)`).
+
+### 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 |
+| length | alias for `limit` |
+| default | set default value or action |
+| null | allow `NULL` values, defaults to `true` |
+| after | specify the column that a new column should be placed after *(MySQL only)* |
+| comment | set a text comment on the column |
+
+For `decimal` and `float` columns:
+
+| Option | Description |
+|-----------|--------------------------------------------------------|
+| precision | total number of digits |
+| scale | number of digits after the decimal point |
+| signed | enable or disable the `unsigned` option *(MySQL only)* |
+
+> [!NOTE]
+> Migrations follows the SQL standard where `precision` is the total number of
+> digits and `scale` is digits after the decimal point.
+
+For `enum` and `set` columns:
+
+| Option | Description |
+|--------|--------------------------------------------|
+| values | comma separated list or an array of values |
+
+For `smallinteger`, `integer` and `biginteger` columns:
+
+| Option | Description |
+|----------|--------------------------------------------------------|
+| identity | enable or disable automatic incrementing |
+| signed | enable or disable the `unsigned` option *(MySQL only)* |
+
+For `date`, `time`, `datetime`, and `timestamp` columns, default/timezone/update
+options are supported as appropriate for the adapter. For `string` and `text`,
+MySQL also supports `collation` and `encoding`.
+
+You can add `created` and `updated` timestamps using `addTimestamps()` or
+`addTimestampsWithTimezone()`:
+
+```php
+$this->table('users')->addTimestamps()->create();
+$this->table('users')->addTimestampsWithTimezone()->create();
+```
+
+### 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.
+
+```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 quoting or escaping:
+
+```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 straightforward using the Table object:
+
+```php
+table('users');
+ $users->addColumn('username', 'string', ['limit' => 20])
+ ->addColumn('password', 'string', ['limit' => 40])
+ ->addColumn('email', 'string', ['limit' => 100])
+ ->addColumn('created', 'datetime')
+ ->addColumn('updated', 'datetime', ['null' => true])
+ ->addIndex(['username', 'email'], ['unique' => true])
+ ->create();
+ }
+}
+```
+
+Migrations automatically creates an auto-incrementing primary key column called
+`id` for every table unless configured otherwise.
+
+You can disable the automatic `id` column and define a custom primary key:
+
+```php
+$table = $this->table('followers', [
+ 'id' => false,
+ 'primary_key' => ['user_id', 'follower_id'],
+]);
+```
+
+The MySQL adapter also supports table options such as `comment`, `collation`,
+`row_format`, `engine`, `signed`, and `limit`.
+
+To set unsigned primary keys, pass the `signed` option with `false`, or enable
+the `Migrations.unsigned_primary_keys` and `Migrations.unsigned_ints` feature
+flags together. See [Feature Flags](../../advanced/integration-and-deployment#feature-flags).
+
+## MySQL ALTER TABLE Options
+
+When modifying tables in MySQL, you can control how the `ALTER TABLE`
+operation is performed using the `algorithm` and `lock` options:
+
+```php
+$table->update([
+ 'algorithm' => 'INPLACE',
+ 'lock' => 'NONE',
+]);
+```
+
+Not all operations support all `algorithm` and `lock` combinations, and MySQL
+will raise an error if the requested combination is not possible.
+
+## 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
+ {
+ $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. Migrations provides three methods:
+
+- `create()` creates the table and runs pending changes
+- `update()` runs pending changes on an existing table
+- `save()` creates or updates depending on whether the table exists
+
+When using `change()`, you should always use `create()` or `update()`, and
+never `save()`.
+
+## Renaming a Column
+
+To rename a column, call `renameColumn()`:
+
+```php
+$this->table('users')
+ ->renameColumn('bio', 'biography')
+ ->save();
+```
+
+## Adding a Column After Another Column
+
+When adding a column with the MySQL adapter, you can dictate its position using
+the `after` option:
+
+```php
+$this->table('users')
+ ->addColumn('city', 'string', ['after' => 'email'])
+ ->update();
+```
+
+The `\Migrations\Db\Adapter\MysqlAdapter::FIRST` constant can be used to place
+the new column first.
+
+## Dropping a Column
+
+To drop a column, use `removeColumn()`:
+
+```php
+$this->table('users')
+ ->removeColumn('short_name')
+ ->save();
+```
+
+## Specifying a Column Limit
+
+You can limit the maximum length of a column using the `limit` option:
+
+```php
+$this->table('tags')
+ ->addColumn('short_name', 'string', ['limit' => 30])
+ ->update();
+```
+
+## Changing Column Attributes
+
+There are two methods for modifying existing columns:
+
+### Updating Columns
+
+To modify specific column attributes while preserving others, use
+`updateColumn()`:
+
+```php
+$this->table('users')
+ ->updateColumn('email', null, ['null' => true])
+ ->save();
+```
+
+This automatically preserves unspecified attributes such as defaults,
+nullability, limits, comments, signedness, collation, and enum/set values.
+
+### Changing Columns
+
+To completely replace a column definition, use `changeColumn()` and specify all
+desired attributes:
+
+```php
+$this->table('users')
+ ->changeColumn('email', 'string', [
+ 'limit' => 255,
+ 'null' => true,
+ 'default' => null,
+ ])
+ ->save();
+```
+
+You can enable attribute preservation with `'preserveUnspecified' => true`.
+
+## Determining Whether a Table Exists
+
+Use `hasTable()` to check whether a table exists:
+
+```php
+if ($this->hasTable('users')) {
+ // do something
+}
+```
+
+## Dropping a Table
+
+Tables can be dropped using `drop()`:
+
+```php
+$this->table('users')->drop()->save();
+```
+
+## Renaming a Table
+
+To rename a table, call `rename()`:
+
+```php
+$this->table('users')
+ ->rename('legacy_users')
+ ->update();
+```
+
+## Changing the Primary Key
+
+To change the primary key on an existing table, use `changePrimaryKey()`:
+
+```php
+$this->table('users')
+ ->changePrimaryKey(['new_id', 'username'])
+ ->save();
+```
+
+## Creating Custom Primary Keys
+
+You can specify an `autoId` property in the migration class and set it to
+`false`, which turns off automatic `id` column creation:
+
+```php
+table('products')
+ ->addColumn('id', 'uuid')
+ ->addPrimaryKey('id')
+ ->addColumn('name', 'string')
+ ->addColumn('description', 'text')
+ ->create();
+ }
+}
+```
+
+> [!WARNING]
+> Dealing with primary keys can only be done on table creation operations for
+> some database servers.
+
+## Changing the Table Comment
+
+To change the comment on an existing table, use `changeComment()`:
+
+```php
+$this->table('users')
+ ->changeComment('This is the table with users auth information')
+ ->save();
+```
+
+## Next Steps
+
+- [Indexes and Constraints](indexes-and-constraints)
+- [Schema Introspection and Platform Limitations](schema-introspection-and-platform-limitations)
diff --git a/docs/en/guides/writing-migrations/index.md b/docs/en/guides/writing-migrations/index.md
new file mode 100644
index 000000000..7d25425c4
--- /dev/null
+++ b/docs/en/guides/writing-migrations/index.md
@@ -0,0 +1,21 @@
+# 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.
+
+For generating migration files with `bake`, naming patterns, anonymous
+migration classes, and command-line column syntax, see
+[Creating Migrations](../../getting-started/creating-migrations).
+
+## Guide Map
+
+This section has been split into focused reference pages:
+
+- [Migration Methods](migration-methods)
+- [Columns and Table Operations](columns-and-table-operations)
+- [Indexes and Constraints](indexes-and-constraints)
+- [Schema Introspection and Platform Limitations](schema-introspection-and-platform-limitations)
+
+Use these pages as the API reference for authoring migrations.
diff --git a/docs/en/guides/writing-migrations/indexes-and-constraints.md b/docs/en/guides/writing-migrations/indexes-and-constraints.md
new file mode 100644
index 000000000..3c15e688b
--- /dev/null
+++ b/docs/en/guides/writing-migrations/indexes-and-constraints.md
@@ -0,0 +1,457 @@
+# Indexes and Constraints
+
+## Working With Indexes
+
+To add an index to a table, call `addIndex()` on the Table object:
+
+```php
+table('users')
+ ->addColumn('city', 'string')
+ ->addIndex(['city'])
+ ->save();
+ }
+}
+```
+
+By default, Migrations creates a simple index. You can pass `unique` to create
+a unique index, specify a `name`, and control `order` (an array of column
+names and sort order key/value pairs):
+
+```php
+$this->table('users')
+ ->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:
+
+```php
+$this->table('users')
+ ->addIndex(
+ $this->index(['email', 'username'])
+ ->setType('unique')
+ ->setName('idx_users_email')
+ ->setOrder(['email' => 'DESC', 'username' => 'ASC'])
+ )
+ ->save();
+```
+
+### MySQL Fulltext Indexes
+
+The MySQL adapter supports `fulltext` indexes. If you are using a version
+before 5.6 the table must use the `MyISAM` engine:
+
+```php
+$table = $this->table('users', ['engine' => 'MyISAM']);
+$table->addColumn('email', 'string')
+ ->addIndex('email', ['type' => 'fulltext'])
+ ->create();
+```
+
+### MySQL Index Length
+
+The MySQL adapter supports setting the index length via the `limit` option.
+For multi-column indexes you can define the length per column:
+
+```php
+$this->table('users')
+ ->addColumn('email', 'string')
+ ->addColumn('username', 'string')
+ ->addColumn('user_guid', 'string', ['limit' => 36])
+ ->addIndex(['email', 'username'], ['limit' => ['email' => 5, 'username' => 2]])
+ ->addIndex('user_guid', ['limit' => 6])
+ ->create();
+```
+
+### Include Columns (SQL Server and PostgreSQL)
+
+The SQL Server and PostgreSQL adapters support `include` (non-key) columns on
+indexes:
+
+```php
+$this->table('users')
+ ->addColumn('email', 'string')
+ ->addColumn('firstname', 'string')
+ ->addColumn('lastname', 'string')
+ ->addIndex(['email'], ['include' => ['firstname', 'lastname']])
+ ->create();
+```
+
+### Partial Indexes
+
+PostgreSQL, SQL Server, and SQLite support partial indexes via a `WHERE`
+clause:
+
+```php
+$this->table('users')
+ ->addColumn('email', 'string')
+ ->addColumn('is_verified', 'boolean')
+ ->addIndex(
+ $this->index('email')
+ ->setName('user_email_verified_idx')
+ ->setType('unique')
+ ->setWhere('is_verified = true')
+ )
+ ->create();
+```
+
+### Concurrent Index Creation (PostgreSQL)
+
+PostgreSQL can create indexes concurrently, which avoids taking disruptive
+locks during index creation:
+
+```php
+$this->table('users')
+ ->addColumn('email', 'string')
+ ->addIndex(
+ $this->index('email')
+ ->setName('user_email_unique_idx')
+ ->setType('unique')
+ ->setConcurrently(true)
+ )
+ ->create();
+```
+
+### PostgreSQL Index Access Methods
+
+PostgreSQL supports several index access methods beyond the default B-tree.
+Use the `type` option to specify the access method.
+
+#### GIN (Generalized Inverted Index)
+
+GIN indexes are useful for full-text search, arrays, and JSONB columns:
+
+```php
+$this->table('articles')
+ ->addColumn('tags', 'jsonb')
+ ->addIndex('tags', ['type' => 'gin'])
+ ->create();
+```
+
+#### GiST (Generalized Search Tree)
+
+GiST indexes support geometric data, range types, and full-text search.
+For trigram similarity searches (requires the `pg_trgm` extension), use the
+`opclass` option:
+
+```php
+$this->table('products')
+ ->addColumn('name', 'string')
+ ->addIndex('name', [
+ 'type' => 'gist',
+ 'opclass' => ['name' => 'gist_trgm_ops'],
+ ])
+ ->create();
+```
+
+#### BRIN (Block Range Index)
+
+BRIN indexes are highly efficient for large, naturally-ordered tables like
+time-series data. They are much smaller than B-tree indexes but only work well
+when data is physically ordered by the indexed column:
+
+```php
+$this->table('sensor_readings')
+ ->addColumn('recorded_at', 'timestamp')
+ ->addColumn('value', 'decimal')
+ ->addIndex('recorded_at', ['type' => 'brin'])
+ ->create();
+```
+
+#### SP-GiST (Space-Partitioned GiST)
+
+SP-GiST indexes work well for data with natural clustering, like IP addresses
+or phone numbers:
+
+```php
+$this->table('access_logs')
+ ->addColumn('client_ip', 'inet')
+ ->addIndex('client_ip', ['type' => 'spgist'])
+ ->create();
+```
+
+#### Hash
+
+Hash indexes handle simple equality comparisons. They are rarely needed since
+B-tree handles equality efficiently too:
+
+```php
+$this->table('sessions')
+ ->addColumn('session_id', 'string', ['limit' => 64])
+ ->addIndex('session_id', ['type' => 'hash'])
+ ->create();
+```
+
+::: warning
+Until CakePHP 5.4 ships the reflection side of these access methods, schema
+snapshots generated from tables using non-default index types may be lossy —
+the `USING ` clause and `opclass` options are not yet reflected back
+into generated migration code.
+:::
+
+### Removing Indexes
+
+Use `removeIndex()` with the list of columns, or `removeIndexByName()` if you
+named the index:
+
+```php
+$table = $this->table('users');
+$table->removeIndex(['email'])
+ ->save();
+
+$table->removeIndexByName('idx_users_email')
+ ->save();
+```
+
+::: info Added in version 4.6.0
+`Index::setWhere()` and `Index::setConcurrently()` were added.
+:::
+
+## Working With Foreign Keys
+
+Migrations supports creating foreign key constraints on database tables:
+
+```php
+table('tags')
+ ->addColumn('tag_name', 'string')
+ ->save();
+
+ $this->table('tag_relationships')
+ ->addColumn('tag_id', 'integer', ['null' => true])
+ ->addForeignKey(
+ 'tag_id',
+ 'tags',
+ 'id',
+ ['delete' => 'SET_NULL', 'update' => 'NO_ACTION'],
+ )
+ ->save();
+ }
+}
+```
+
+The `delete` and `update` options define the `ON DELETE` and `ON UPDATE`
+behavior. Valid values are `SET_NULL`, `NO_ACTION`, `CASCADE`, and
+`RESTRICT`. If `SET_NULL` is used, the column must be created as nullable
+with `['null' => true]`.
+
+### Composite Foreign Keys
+
+Foreign keys can be defined with arrays of columns to build constraints
+between tables with composite keys:
+
+```php
+$this->table('follower_events')
+ ->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:
+
+| Option | Description |
+|------------|--------------------------------------------------------|
+| update | set an action to be triggered when the row is updated |
+| delete | set an action to be triggered when the row is deleted |
+| constraint | set a name to be used by foreign key constraint |
+| deferrable | define deferred constraint application (postgres only) |
+
+### Fluent Foreign Key Builder
+
+`foreignKey()` returns a fluent builder for more complex cases:
+
+```php
+table('articles')
+ ->addForeignKey(
+ $this->foreignKey()
+ ->setColumns('user_id')
+ ->setReferencedTable('users')
+ ->setReferencedColumns('user_id')
+ ->setDelete(ForeignKey::CASCADE)
+ ->setName('article_user_fk')
+ )
+ ->save();
+ }
+}
+```
+
+::: info Added in version 4.6.0
+The `foreignKey` method was added.
+:::
+
+### Checking and Dropping Foreign Keys
+
+Use `hasForeignKey()` to check whether a foreign key exists:
+
+```php
+$table = $this->table('tag_relationships');
+if ($table->hasForeignKey('tag_id')) {
+ // do something
+}
+```
+
+To delete a foreign key, use `dropForeignKey()`. Like other Table methods, it
+needs `save()` to be called at the end:
+
+```php
+$this->table('tag_relationships')
+ ->dropForeignKey('tag_id')
+ ->save();
+```
+
+## Working With Check Constraints
+
+::: info Added in version 5.0.0
+Check constraints were added in 5.0.0.
+:::
+
+Check constraints allow you to enforce data validation rules at the database
+level.
+
+> [!NOTE]
+> Check constraints are supported by MySQL 8.0.16+, PostgreSQL, and SQLite.
+> SQL Server support is planned for a future release.
+
+### Adding a Check Constraint
+
+The first argument is the constraint name, and the second is the SQL
+expression that defines the constraint:
+
+```php
+table('products')
+ ->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2])
+ ->addCheckConstraint('price_positive', 'price > 0')
+ ->save();
+ }
+
+ public function down(): void
+ {
+ $this->table('products')
+ ->dropCheckConstraint('price_positive')
+ ->save();
+ }
+}
+```
+
+### Using the CheckConstraint Fluent Builder
+
+For more complex scenarios, `checkConstraint()` returns a fluent builder:
+
+```php
+$this->table('users')
+ ->addColumn('age', 'integer')
+ ->addColumn('status', 'string', ['limit' => 20])
+ ->addCheckConstraint(
+ $this->checkConstraint()
+ ->setName('age_valid')
+ ->setExpression('age >= 18 AND age <= 120')
+ )
+ ->addCheckConstraint(
+ $this->checkConstraint()
+ ->setName('status_valid')
+ ->setExpression("status IN ('active', 'inactive', 'pending')")
+ )
+ ->save();
+```
+
+### Auto-Generated Constraint Names
+
+If you do not specify a constraint name, one will be automatically generated
+based on the table name and expression hash:
+
+```php
+$this->table('inventory')
+ ->addColumn('quantity', 'integer')
+ // Name will be auto-generated like 'inventory_chk_a1b2c3d4'
+ ->addCheckConstraint(
+ $this->checkConstraint()
+ ->setExpression('quantity >= 0')
+ )
+ ->save();
+```
+
+### Complex Check Constraints
+
+Check constraints can reference multiple columns and use complex SQL
+expressions:
+
+```php
+$this->table('date_ranges')
+ ->addColumn('start_date', 'date')
+ ->addColumn('end_date', 'date')
+ ->addColumn('discount', 'decimal', ['precision' => 5, 'scale' => 2])
+ ->addCheckConstraint('valid_date_range', 'end_date >= start_date')
+ ->addCheckConstraint('valid_discount', 'discount BETWEEN 0 AND 100')
+ ->save();
+```
+
+### Checking and Dropping Check Constraints
+
+Use `hasCheckConstraint()` to check whether a constraint exists, and
+`dropCheckConstraint()` to remove one:
+
+```php
+$table = $this->table('products');
+if ($table->hasCheckConstraint('price_positive')) {
+ $table->dropCheckConstraint('price_positive')
+ ->save();
+}
+```
+
+### Database-Specific Behavior
+
+- MySQL stores check constraint metadata in
+ `INFORMATION_SCHEMA.CHECK_CONSTRAINTS`
+- PostgreSQL stores constraints in `pg_constraint`
+- SQLite recreates the table when altering check constraints
+- SQL Server support is planned for a future release
+
+## Next Steps
+
+- [Columns and Table Operations](columns-and-table-operations)
+- [Schema Introspection and Platform Limitations](schema-introspection-and-platform-limitations)
diff --git a/docs/en/guides/writing-migrations/migration-methods.md b/docs/en/guides/writing-migrations/migration-methods.md
new file mode 100644
index 000000000..fbff46710
--- /dev/null
+++ b/docs/en/guides/writing-migrations/migration-methods.md
@@ -0,0 +1,175 @@
+# Migration Methods
+
+A migration declares its style by implementing one of two capability interfaces:
+
+- `Migrations\ReversibleMigrationInterface` for migrations that define a
+ single `change()` method.
+- `Migrations\DirectionalMigrationInterface` for migrations that define
+ separate `up()` and `down()` methods.
+
+A migration implements **one** of the two, never both. Bake-generated
+migrations already include the right `implements` clause. Migrations from
+older versions of cakephp/migrations keep working without the interface
+through a `method_exists()` fallback; see
+[Upgrading to Capability Interfaces](/upgrades/upgrading-to-capability-interfaces)
+for the adoption path.
+
+## 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 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.
+
+The database adapter is available inside `shouldExecute()`, so the decision can
+be based on the current state of the database (for example the server version or
+whether a table already exists):
+
+```php
+hasTable('some_table');
+ }
+
+ public function change(): void
+ {
+ // ...
+ }
+}
+```
+
+## Working With Tables
+
+The Table object enables you to 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');
+ }
+
+ public function down(): void
+ {
+ }
+}
+```
+
+You can then manipulate this table using the methods provided by the Table
+object.
+
+## Next Steps
+
+- [Columns and Table Operations](columns-and-table-operations)
+- [Indexes and Constraints](indexes-and-constraints)
+- [Schema Introspection and Platform Limitations](schema-introspection-and-platform-limitations)
diff --git a/docs/en/guides/writing-migrations/schema-introspection-and-platform-limitations.md b/docs/en/guides/writing-migrations/schema-introspection-and-platform-limitations.md
new file mode 100644
index 000000000..d733a9c9d
--- /dev/null
+++ b/docs/en/guides/writing-migrations/schema-introspection-and-platform-limitations.md
@@ -0,0 +1,105 @@
+# Schema Introspection and Platform Limitations
+
+## 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, create a table object and call `getColumns()`:
+
+```php
+table('users')->getColumns();
+ }
+}
+```
+
+### Get a Column by Name
+
+To retrieve one table column, call `getColumn()`:
+
+```php
+table('users')->getColumn('email');
+ }
+}
+```
+
+### Check Whether a Column Exists
+
+Use `hasColumn()` to determine whether a table already has a given column:
+
+```php
+table('user');
+ if ($table->hasColumn('username')) {
+ // 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 are not currently implemented
+- Table comments are not supported
+- `insertOrSkip()` is not supported; use `insertOrUpdate()` instead
+
+### SQLite
+
+SQLite limitations include:
+
+- named foreign keys are not supported
+- table comments are stored as metadata, not in the database itself
+- altering check constraints requires recreating the table
+- table partitioning is not supported
+
+### PostgreSQL
+
+PostgreSQL does not support MySQL's `KEY` partitioning type. Use `HASH`
+partitioning instead for similar distribution behavior.
+
+### MySQL/MariaDB
+
+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.
+
+Some geometry column features may not work correctly on MariaDB due to
+differences in GIS implementation compared to MySQL.
diff --git a/docs/en/index.md b/docs/en/index.md
new file mode 100644
index 000000000..7c5c13c0d
--- /dev/null
+++ b/docs/en/index.md
@@ -0,0 +1,216 @@
+# 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](upgrades/upgrading-from-4-x) 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, run the following from your application's
+root directory:
+
+```bash
+php composer.phar require cakephp/migrations "@stable"
+
+# Or if composer is installed globally
+composer require cakephp/migrations "@stable"
+```
+
+To use the plugin, load it in your application's `config/bootstrap.php` file:
+
+```bash
+bin/cake plugin load Migrations
+```
+
+Or load the plugin in `src/Application.php`:
+
+```php
+$this->addPlugin('Migrations');
+```
+
+Additionally, configure the default database connection in `config/app.php` as
+explained in the [Database Configuration section](https://book.cakephp.org/5/en/orm/database-basics.html#database-configuration).
+
+## 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. Use the CLI commands to apply and
+> roll back migrations.
+
+```bash
+bin/cake migrations migrate
+```
+
+## Guide Map
+
+Use the focused guides below instead of a single long reference page:
+
+- [Installation and Overview](getting-started/installation-and-overview)
+- [Creating Migrations](getting-started/creating-migrations)
+- [Snapshots and Diffs](getting-started/snapshots-and-diffs)
+- [Running and Managing Migrations](getting-started/running-and-managing-migrations)
+- [Integration and Deployment](advanced/integration-and-deployment)
+- [Writing Migrations](guides/writing-migrations)
+- [Using the Query Builder](guides/using-the-query-builder)
+- [Executing Queries](guides/executing-queries)
+- [Database Seeding](guides/seeding)
+- [Upgrading to the builtin backend](upgrades/upgrading-to-builtin-backend)
+
+## Upgrading from 4.x
+
+If you are upgrading from Migrations 4.x, see the
+[Upgrading from 4.x to 5.x](upgrades/upgrading-from-4-x) guide.
+
+## Creating Migrations
+
+Migration naming conventions, bake patterns, column syntax, and generated
+examples are covered in [Creating Migrations](getting-started/creating-migrations).
+
+## Generating migration snapshots from an existing database
+
+For snapshot generation, diff workflows, and dump files, see
+[Snapshots and Diffs](getting-started/snapshots-and-diffs).
+
+## Generating a diff
+
+The `bake migration_diff` workflow is also covered in
+[Snapshots and Diffs](getting-started/snapshots-and-diffs).
+
+## Applying Migrations
+
+For `migrate`, `rollback`, `status`, `mark_migrated`, and related command
+options, see
+[Running and Managing Migrations](getting-started/running-and-managing-migrations).
+
+## Reverting Migrations
+
+Rollback usage is documented in
+[Running and Managing Migrations](getting-started/running-and-managing-migrations).
+
+## View Migrations Status
+
+Status commands, cleanup, and JSON output are documented in
+[Running and Managing Migrations](getting-started/running-and-managing-migrations).
+
+## Marking a migration as migrated
+
+See [Running and Managing Migrations](getting-started/running-and-managing-migrations)
+for `mark_migrated` examples and caveats.
+
+## Seeding your database
+
+Seed classes are documented in [Database Seeding](guides/seeding).
+
+## Generating a dump file
+
+Dump generation is documented in
+[Snapshots and Diffs](getting-started/snapshots-and-diffs).
+
+## Using Migrations for Tests
+
+Test bootstrapping with `Migrations\TestSuite\Migrator` is covered in
+[Integration and Deployment](advanced/integration-and-deployment).
+
+## Using Migrations In Plugins
+
+Plugin-scoped migration workflows are covered in
+[Integration and Deployment](advanced/integration-and-deployment).
+
+## Running Migrations in a non-shell environment
+
+Programmatic execution through `Migrations\Migrations` is covered in
+[Integration and Deployment](advanced/integration-and-deployment).
+
+## Feature Flags
+
+Compatibility feature flags are covered in
+[Integration and Deployment](advanced/integration-and-deployment).
+
+## Skipping the `schema.lock` file generation
+
+The `--no-lock` workflow is covered in
+[Integration and Deployment](advanced/integration-and-deployment).
+
+## Deployment
+
+Deployment guidance and schema cache refresh steps are covered in
+[Integration and Deployment](advanced/integration-and-deployment).
+
+## Alert of missing migrations
+
+Local development alerts for pending migrations are covered in
+[Integration and Deployment](advanced/integration-and-deployment).
+
+## IDE autocomplete support
+
+IDE-related tooling notes are covered in
+[Integration and Deployment](advanced/integration-and-deployment).
diff --git a/docs/en/index.rst b/docs/en/index.rst
deleted file mode 100644
index c0155fecd..000000000
--- a/docs/en/index.rst
+++ /dev/null
@@ -1,955 +0,0 @@
-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 :doc:`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):
-
-.. code-block:: 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
-`__ to
-load and unload plugins from your **config/bootstrap.php**:
-
-.. code-block:: bash
-
- bin/cake plugin load Migrations
-
-Or you can load the plugin by editing your **src/Application.php** file and
-adding the following statement::
-
- $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
-`__.
-
-Upgrading from 4.x
-==================
-
-If you are upgrading from Migrations 4.x, please see the :doc:`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::
-
- 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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 :ref:`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:
-
-.. code-block:: bash
-
- bin/cake bake migration CreateProducts name:string description:text created modified
-
-The command line above will generate a migration file that resembles::
-
- 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:
-
-.. code-block:: 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:
-
-.. code-block:: bash
-
- bin/cake migrations create MyCustomMigration
-
-
-See :doc:`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:
-
-.. code-block:: bash
-
- bin/cake bake migration AddPriceToProducts price:decimal[5,2]
-
-Executing the command line above will generate::
-
- 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:
-
-.. code-block:: bash
-
- bin/cake bake migration AddNameIndexToProducts name:string:index
-
-will generate::
-
- 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:
-
-.. code-block:: bash
-
- bin/cake bake migration AddActiveToUsers active:boolean:default[true]
-
-will generate::
-
- table('users');
- $table->addColumn('active', 'boolean', [
- 'default' => true,
- 'null' => false,
- ]);
- $table->update();
- }
- }
-
-You can combine default values with other options like nullable and indexes:
-
-.. code-block:: 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":
-
-.. code-block:: bash
-
- bin/cake bake migration AlterPriceOnProducts name:float
-
-will generate::
-
- 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":
-
-.. code-block:: bash
-
- bin/cake bake migration RemovePriceFromProducts price
-
-creates the file::
-
- 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: bash
-
- bin/cake migrations status
-
-You can also output the results as a JSON formatted string using the
-``--format`` option (or ``-f`` for short):
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: bash
-
- bin/cake migrations mark_migrated
-
-You can also mark all migrations up to a specific version as migrated using
-the ``--target`` option:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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 :doc:`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:
-
-.. code-block:: 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::
-
- // 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::
-
- // 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::
-
- $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:
-
-.. code-block:: 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::
-
- 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::
-
- 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::
-
- 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::
-
- 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:
-
-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``)::
-
- '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:
-
-.. code-block:: 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
-`__ that you
-can use to perform this operation:
-
-.. code-block:: 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::
-
- 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
-`__ 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.rst b/docs/en/seeding.rst
deleted file mode 100644
index 2d4838823..000000000
--- a/docs/en/seeding.rst
+++ /dev/null
@@ -1,616 +0,0 @@
-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:
-
-.. code-block:: bash
-
- $ bin/cake bake seed MyNewSeed
-
-By default, it generates a traditional seed class with a named class:
-
-.. code-block:: 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:
-
-.. code-block:: bash
-
- $ bin/cake bake seed MyNewSeed --style anonymous
-
-This generates a seed file using an anonymous class:
-
-.. code-block:: php
-
- [
- 'style' => 'anonymous', // or 'traditional'
- ],
-
-Seed Options
-------------
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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.
-
-.. _custom-seed-migration-templates:
-
-Customizing Seed and Migration templates
-----------------------------------------
-
-Because migrations uses `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:
-
-.. code-block:: bash
-
- bin/cake seeds run Users --force
-
-You can check which seeds have been executed using the status command:
-
-.. code-block:: bash
-
- bin/cake seeds status
-
-To reset all seeds' execution state (allowing them to run again without ``--force``):
-
-.. code-block:: 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``:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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:
-
-.. code-block:: 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/upgrades/upgrading-from-4-x.md b/docs/en/upgrades/upgrading-from-4-x.md
new file mode 100644
index 000000000..0786baa8b
--- /dev/null
+++ b/docs/en/upgrades/upgrading-from-4-x.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](../guides/seeding) for more details.
+
+### Check Constraints
+
+Support for database check constraints via `addCheckConstraint()`.
+See [Writing Migrations](../guides/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/upgrades/upgrading-to-builtin-backend.md b/docs/en/upgrades/upgrading-to-builtin-backend.md
new file mode 100644
index 000000000..575446651
--- /dev/null
+++ b/docs/en/upgrades/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/upgrades/upgrading-to-capability-interfaces.md b/docs/en/upgrades/upgrading-to-capability-interfaces.md
new file mode 100644
index 000000000..14481aebc
--- /dev/null
+++ b/docs/en/upgrades/upgrading-to-capability-interfaces.md
@@ -0,0 +1,213 @@
+# Upgrading to Capability Interfaces
+
+Starting with 5.2.0, cakephp/migrations ships two capability interfaces that let migrations declare their style explicitly:
+
+- `Migrations\ReversibleMigrationInterface` — for migrations that define a single reversible `change()` method.
+- `Migrations\DirectionalMigrationInterface` — for migrations that define separate `up()` and `down()` methods.
+
+A migration implements **one** of the two interfaces, never both.
+
+## Why this exists
+
+Until now, `Environment` dispatched migrations through `method_exists()` checks against `change()`, `up()`, and `down()`. This works at runtime but has a few drawbacks:
+
+- A typo such as `function chnage()` silently no-ops at run time. The runtime cannot tell whether the migration is reversible or directional, so it just does nothing.
+- IDEs and static analyzers cannot resolve `change()` / `up()` / `down()` on a generic `MigrationInterface`, so refactoring tools and PHPStan narrowing do not work.
+- Custom runners that wrap `MigrationInterface` have to use reflection to figure out the migration style.
+
+The capability interfaces are a way out of this without breaking existing code:
+
+- `Environment` now dispatches via `instanceof` first and falls back to `method_exists()` for migrations that have not adopted the interfaces yet.
+- The interfaces declare their method contracts as PHPDoc `@method` tags (not as real abstract methods), which means adding `implements` to an existing migration is a **zero-friction** change — your method signature is not validated against an abstract.
+
+::: tip 5.x is a soft window
+The PHPDoc-only contract is intentional. The 6.x release is expected to promote the `@method` tags to real abstract method declarations, at which point a missing or mistyped `change()` / `up()` / `down()` becomes a static error. The 5.next release gives you a runway to adopt the interfaces without breakage; the 6.x release tightens the contract.
+:::
+
+## What changed for app developers
+
+If you do nothing, your existing migrations keep working. `Environment` retains a `method_exists()` fallback throughout the 5.x cycle.
+
+Adopting the interfaces now is recommended because:
+
+- New bakes already emit the right `implements` clause.
+- Static analysis and IDE autocomplete start working on your migrations.
+- Your app is upgrade-ready when 6.x lands.
+
+## Per-app upgrade — manual edits
+
+### Reversible migration (defines `change()`)
+
+Before:
+
+```php
+table('products')
+ ->addColumn('name', 'string')
+ ->create();
+ }
+}
+```
+
+After:
+
+```php
+table('products')
+ ->addColumn('name', 'string')
+ ->create();
+ }
+}
+```
+
+### Directional migration (defines `up()` and `down()`)
+
+Before:
+
+```php
+execute('UPDATE orders SET total = ...');
+ }
+
+ public function down(): void
+ {
+ $this->execute('UPDATE orders SET total = NULL');
+ }
+}
+```
+
+After:
+
+```php
+execute('UPDATE orders SET total = ...');
+ }
+
+ public function down(): void
+ {
+ $this->execute('UPDATE orders SET total = NULL');
+ }
+}
+```
+
+### Anonymous migrations
+
+Anonymous migrations get the same treatment:
+
+```php
+return new class extends BaseMigration implements ReversibleMigrationInterface
+{
+ public function change(): void
+ {
+ }
+};
+```
+
+## Automated upgrade with rector
+
+cakephp/migrations ships a rector rule that adds the right `implements` clause to every migration in your `config/Migrations/` folder.
+
+Add the following to your `rector.php`:
+
+```php
+use Migrations\Rector\AddMigrationCapabilityInterfaceRector;
+use Rector\Config\RectorConfig;
+
+return RectorConfig::configure()
+ ->withPaths([
+ __DIR__ . '/config/Migrations',
+ ])
+ ->withRules([
+ AddMigrationCapabilityInterfaceRector::class,
+ ]);
+```
+
+Then run rector:
+
+```bash
+vendor/bin/rector process --config=rector.php
+```
+
+What the rule does:
+
+- For every class extending `Migrations\BaseMigration` (directly or transitively):
+ - If the class defines `change()`, add `implements ReversibleMigrationInterface`.
+ - If the class defines both `up()` and `down()`, add `implements DirectionalMigrationInterface`.
+- One-way migrations that define only `up()` or only `down()` are left untouched. They keep working through the `method_exists()` fallback; adding `DirectionalMigrationInterface` would make `Environment` call the missing direction unconditionally and turn a rollback no-op into a fatal error.
+- Classes that already implement either capability interface are skipped.
+- Classes that define both `change()` and `up()`/`down()` are skipped — these are user errors that need a deliberate decision.
+
+### Optional: combine with built-in rector rules
+
+If you also want to normalize visibility and return types on your migration methods (the shape 6.x will expect), compose with rector's built-in sets:
+
+```php
+use Migrations\Rector\AddMigrationCapabilityInterfaceRector;
+use Rector\Config\RectorConfig;
+use Rector\Set\ValueObject\SetList;
+
+return RectorConfig::configure()
+ ->withPaths([
+ __DIR__ . '/config/Migrations',
+ ])
+ ->withRules([
+ AddMigrationCapabilityInterfaceRector::class,
+ ])
+ ->withSets([
+ SetList::TYPE_DECLARATION,
+ ]);
+```
+
+::: warning Only point rector at your migrations folder
+Migration paths use scoped rector configs by default; pointing rector at `src/` or `tests/` will apply unrelated transformations. Keep the path list narrow.
+:::
+
+## Manual work that remains after rector
+
+- **Migrations not on `BaseMigration`.** Anything still on a legacy Phinx `AbstractMigration` fork or a custom base that does not extend `BaseMigration` is skipped. Add the `implements` clause by hand.
+- **Dynamically generated migration classes** (eval'd test fixtures, factories). Rector cannot see them. Add the `implements` clause at the generation site.
+- **Custom base classes that themselves declare `change()` / `up()` / `down()`.** Rector adds the interface to the base class once. If the base lives in a third-party plugin you do not control, either implement the capability interface on the leaf class or PR the plugin upstream.
+- **Bake-generated migrations from older versions.** Bake templates emit the `implements` clause out of the box from 5.next; older bakes do not. Rector cleans those up in one pass.
+
+## Forward direction
+
+The 6.x release is expected to:
+
+- Promote the PHPDoc `@method` declarations on the capability interfaces to real abstract method declarations.
+- Remove the `method_exists()` fallback in `Environment`.
+
+Running rector now means the 6.x bump is a no-op for your migration files.
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.rst b/docs/en/upgrading.rst
deleted file mode 100644
index 23ab2e113..000000000
--- a/docs/en/upgrading.rst
+++ /dev/null
@@ -1,140 +0,0 @@
-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 :doc:`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:
-
-.. code-block:: 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:
-
-.. code-block:: 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``::
-
- 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``::
-
- // 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 :doc:`seeding` for more details.
-
-Check Constraints
------------------
-
-Support for database check constraints via ``addCheckConstraint()``.
-See :doc:`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.
-
-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
-https://github.com/cakephp/migrations/issues
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.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/examples/ViewsAndTriggersExample.php b/docs/examples/ViewsAndTriggersExample.php
new file mode 100644
index 000000000..1c381735c
--- /dev/null
+++ b/docs/examples/ViewsAndTriggersExample.php
@@ -0,0 +1,149 @@
+table('users');
+ $users->addColumn('username', 'string', ['limit' => 100])
+ ->addColumn('email', 'string', ['limit' => 255])
+ ->addColumn('status', 'string', ['limit' => 20, 'default' => 'active'])
+ ->addColumn('created', 'datetime')
+ ->create();
+
+ // Create a posts table
+ $posts = $this->table('posts');
+ $posts->addColumn('user_id', 'integer')
+ ->addColumn('title', 'string', ['limit' => 255])
+ ->addColumn('body', 'text')
+ ->addColumn('published', 'boolean', ['default' => false])
+ ->addColumn('created', 'datetime')
+ ->addForeignKey('user_id', 'users', 'id', ['delete' => 'CASCADE'])
+ ->create();
+
+ // Create a view showing active users with their post counts
+ // Note: Views are created through a dummy table object
+ $this->createView(
+ 'active_users_with_posts',
+ 'SELECT u.id, u.username, u.email, COUNT(p.id) as post_count
+ FROM users u
+ LEFT JOIN posts p ON u.id = p.user_id
+ WHERE u.status = \'active\'
+ GROUP BY u.id, u.username, u.email'
+ );
+
+ // Create a materialized view (PostgreSQL only)
+ // On other databases, this will create a regular view
+ $this->createView(
+ 'published_posts_summary',
+ 'SELECT user_id, COUNT(*) as published_count
+ FROM posts
+ WHERE published = 1
+ GROUP BY user_id',
+ ['materialized' => true]
+ );
+
+ // Create an audit log table for triggers
+ $auditLog = $this->table('audit_log');
+ $auditLog->addColumn('table_name', 'string', ['limit' => 100])
+ ->addColumn('action', 'string', ['limit' => 20])
+ ->addColumn('record_id', 'integer')
+ ->addColumn('created', 'datetime')
+ ->create();
+
+ // Create a trigger to log user insertions
+ // Note: The trigger definition syntax varies by database
+
+ // For MySQL:
+ $this->createTrigger(
+ 'users',
+ 'log_user_insert',
+ 'INSERT',
+ "INSERT INTO audit_log (table_name, action, record_id, created)
+ VALUES ('users', 'INSERT', NEW.id, NOW())",
+ ['timing' => 'AFTER']
+ );
+
+ // For PostgreSQL, you would need to create a function first:
+ // $this->execute("
+ // CREATE OR REPLACE FUNCTION log_user_insert_func()
+ // RETURNS TRIGGER AS $$
+ // BEGIN
+ // INSERT INTO audit_log (table_name, action, record_id, created)
+ // VALUES ('users', 'INSERT', NEW.id, NOW());
+ // RETURN NEW;
+ // END;
+ // $$ LANGUAGE plpgsql;
+ // ");
+ //
+ // $this->createTrigger(
+ // 'users',
+ // 'log_user_insert',
+ // 'INSERT',
+ // 'log_user_insert_func()', // Function name for PostgreSQL
+ // ['timing' => 'AFTER']
+ // );
+
+ // Create a trigger for updates with multiple events
+ $this->createTrigger(
+ 'posts',
+ 'log_post_changes',
+ ['UPDATE', 'DELETE'],
+ "INSERT INTO audit_log (table_name, action, record_id, created)
+ VALUES ('posts', 'CHANGE', OLD.id, NOW())",
+ ['timing' => 'BEFORE']
+ );
+ }
+
+ /**
+ * Migrate Up.
+ *
+ * If you need more control, you can use up() and down() methods instead.
+ */
+ public function up(): void
+ {
+ // Example of creating a view in up() method
+ $this->createView(
+ 'simple_user_list',
+ 'SELECT id, username FROM users'
+ );
+ }
+
+ /**
+ * Migrate Down.
+ */
+ public function down(): void
+ {
+ // Drop views
+ $this->dropView('simple_user_list');
+ $this->dropView('active_users_with_posts');
+ $this->dropView('published_posts_summary', ['materialized' => true]);
+
+ // Drop triggers
+ $this->dropTrigger('users', 'log_user_insert');
+ $this->dropTrigger('posts', 'log_post_changes');
+
+ // Drop tables
+ $this->table('audit_log')->drop()->save();
+ $this->table('posts')->drop()->save();
+ $this->table('users')->drop()->save();
+ }
+}
diff --git a/docs/package-lock.json b/docs/package-lock.json
new file mode 100644
index 000000000..e5011a901
--- /dev/null
+++ b/docs/package-lock.json
@@ -0,0 +1,1856 @@
+{
+ "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.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@cakephp/docs-skeleton": {
+ "version": "1.0.0",
+ "resolved": "git+ssh://git@github.com/cakephp/docs-skeleton.git#9501c6d47a80645604fceb31328368eeb6a21bc6",
+ "bin": {
+ "cakedocs": "bin/cakedocs.js"
+ },
+ "peerDependencies": {
+ "vitepress": "^2.0.0-alpha.15"
+ }
+ },
+ "node_modules/@docsearch/css": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.7.0.tgz",
+ "integrity": "sha512-Sk5xkdRFeE7PeWjG9l4AfTwdvMfr9wHiwNNCpHXT4v4SNyNMKdHGvEILc31BgaVFGDDNbv5u/a73tofRiwbEZw==",
+ "license": "MIT"
+ },
+ "node_modules/@docsearch/js": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.7.0.tgz",
+ "integrity": "sha512-x5lCqu1tetgsJFkjQ6VSocbHldsRkGEgwg5N98Vx21sq/V5wcmj4u226PY9k+TEpIgQ772zlYbPLTPicWyGnpA==",
+ "license": "MIT"
+ },
+ "node_modules/@docsearch/sidepanel-js": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.7.0.tgz",
+ "integrity": "sha512-A8r34jCU8kcIk2viECEn2msA28ojUF1BLi/3v5OWWc5G2N3jOuuumBXoeYjfr8dA0UxgFSy5R2bt12dnFJQSyA==",
+ "license": "MIT"
+ },
+ "node_modules/@iconify-json/simple-icons": {
+ "version": "1.2.93",
+ "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.93.tgz",
+ "integrity": "sha512-/XhANjfGYOuqvSR3TmUnkQkINvQ4GVjVuukvymRbxtVFBvIq/yiXJqCDycKcQPT401OYT9H2vIY6ihAlz1QIAw==",
+ "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/@oxc-project/types": {
+ "version": "0.144.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz",
+ "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",
+ "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz",
+ "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz",
+ "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz",
+ "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz",
+ "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz",
+ "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz",
+ "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz",
+ "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz",
+ "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz",
+ "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz",
+ "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz",
+ "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz",
+ "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz",
+ "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "license": "MIT"
+ },
+ "node_modules/@shikijs/core": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz",
+ "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/primitive": "4.4.3",
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5",
+ "hast-util-to-html": "^9.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/engine-javascript": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz",
+ "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "oniguruma-to-es": "^4.3.6"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/engine-oniguruma": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz",
+ "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/langs": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz",
+ "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/primitive": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz",
+ "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/themes": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz",
+ "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/transformers": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-4.4.3.tgz",
+ "integrity": "sha512-oJSARV6NaWd+rnNJbtnpAdj3Zg0ZVyzsnMgb3vi3HA+35y8lBWUCpOnWsmyiXZIikY+x1BDqrQUgmxfzWh7Jvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/core": "4.4.3",
+ "@shikijs/types": "4.4.3"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/types": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz",
+ "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "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/hast": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
+ "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
+ "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
+ "license": "ISC"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "6.0.8",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
+ "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==",
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "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.41",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz",
+ "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@vue/shared": "3.5.41",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz",
+ "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.41",
+ "@vue/shared": "3.5.41"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz",
+ "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@vue/compiler-core": "3.5.41",
+ "@vue/compiler-dom": "3.5.41",
+ "@vue/compiler-ssr": "3.5.41",
+ "@vue/shared": "3.5.41",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.19",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz",
+ "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.41",
+ "@vue/shared": "3.5.41"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz",
+ "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-kit": "^8.2.1"
+ }
+ },
+ "node_modules/@vue/devtools-kit": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz",
+ "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-shared": "^8.2.1",
+ "birpc": "^2.6.1",
+ "hookable": "^5.5.3",
+ "perfect-debounce": "^2.0.0"
+ }
+ },
+ "node_modules/@vue/devtools-shared": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz",
+ "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz",
+ "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.41"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz",
+ "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.41",
+ "@vue/shared": "3.5.41"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz",
+ "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.41",
+ "@vue/runtime-core": "3.5.41",
+ "@vue/shared": "3.5.41",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz",
+ "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.41",
+ "@vue/runtime-dom": "3.5.41",
+ "@vue/shared": "3.5.41"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz",
+ "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==",
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "14.4.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz",
+ "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "14.4.0",
+ "@vueuse/shared": "14.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/integrations": {
+ "version": "14.4.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.4.0.tgz",
+ "integrity": "sha512-oJz9qTgczvA7L1nXQFRU7h8tQbOCoiceqvMMhT9XYMyOGTqLJ2rEa09PON+nD2t48sZUfeOmg4eaWJXV4sZb/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@vueuse/core": "14.4.0",
+ "@vueuse/shared": "14.4.0"
+ },
+ "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.4.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz",
+ "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "14.4.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz",
+ "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==",
+ "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/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/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.2.2",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.2.2.tgz",
+ "integrity": "sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug==",
+ "license": "MIT",
+ "dependencies": {
+ "tabbable": "^6.5.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/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "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.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "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.2",
+ "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
+ "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
+ "license": "MIT"
+ },
+ "node_modules/oniguruma-to-es": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
+ "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
+ "license": "MIT",
+ "dependencies": {
+ "oniguruma-parser": "^0.12.2",
+ "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.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.26",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+ "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.17",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
+ "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
+ "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/rolldown": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz",
+ "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.144.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.2.4",
+ "@rolldown/binding-darwin-arm64": "1.2.4",
+ "@rolldown/binding-darwin-x64": "1.2.4",
+ "@rolldown/binding-freebsd-x64": "1.2.4",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.4",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.4",
+ "@rolldown/binding-linux-arm64-musl": "1.2.4",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.4",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.4",
+ "@rolldown/binding-linux-x64-gnu": "1.2.4",
+ "@rolldown/binding-linux-x64-musl": "1.2.4",
+ "@rolldown/binding-openharmony-arm64": "1.2.4",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.4",
+ "@rolldown/binding-win32-x64-msvc": "1.2.4"
+ }
+ },
+ "node_modules/shiki": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz",
+ "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/core": "4.4.3",
+ "@shikijs/engine-javascript": "4.4.3",
+ "@shikijs/engine-oniguruma": "4.4.3",
+ "@shikijs/langs": "4.4.3",
+ "@shikijs/themes": "4.4.3",
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "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.5.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz",
+ "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "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": "8.2.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
+ "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.25",
+ "rolldown": "~1.2.1",
+ "tinyglobby": "^0.2.17"
+ },
+ "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",
+ "@vitejs/devtools": "^0.4.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.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
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "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.19",
+ "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.19.tgz",
+ "integrity": "sha512-WnBsb0Bwr43kXKyiis+lld/7ri3hnMbthS8N3hpFtjjwsdLO4IRmiAE08D7aud4q6oMDf9uwRowxzNqRFe/amw==",
+ "license": "MIT",
+ "dependencies": {
+ "@docsearch/css": "^4.7.0",
+ "@docsearch/js": "^4.7.0",
+ "@docsearch/sidepanel-js": "^4.7.0",
+ "@iconify-json/simple-icons": "^1.2.92",
+ "@shikijs/core": "^4.4.1",
+ "@shikijs/transformers": "^4.4.1",
+ "@shikijs/types": "^4.4.1",
+ "@types/markdown-it": "^14.1.2",
+ "@vitejs/plugin-vue": "^6.0.8",
+ "@vue/devtools-api": "^8.2.1",
+ "@vue/shared": "^3.5.40",
+ "@vueuse/core": "^14.4.0",
+ "@vueuse/integrations": "^14.4.0",
+ "focus-trap": "^8.2.2",
+ "mark.js": "8.11.1",
+ "minisearch": "^7.2.0",
+ "shiki": "^4.4.1",
+ "vite": "^8.2.0",
+ "vue": "^3.5.40"
+ },
+ "bin": {
+ "vitepress": "bin/vitepress.js"
+ },
+ "peerDependencies": {
+ "markdown-it-mathjax3": "^4",
+ "postcss": "^8"
+ },
+ "peerDependenciesMeta": {
+ "markdown-it-mathjax3": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz",
+ "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.41",
+ "@vue/compiler-sfc": "3.5.41",
+ "@vue/runtime-dom": "3.5.41",
+ "@vue/server-renderer": "3.5.41",
+ "@vue/shared": "3.5.41"
+ },
+ "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..51e11cb59
--- /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": "module",
+ "dependencies": {
+ "@cakephp/docs-skeleton": "github:cakephp/docs-skeleton#node-package",
+ "vitepress": "^2.0.0-alpha.16"
+ }
+}
diff --git a/phpcs.xml b/phpcs.xml
index 7a7a8a87c..e89659f62 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -1,6 +1,9 @@
-
+
+
+
+
src/
tests/
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 2cda39ed2..0d7ad0789 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -1,35 +1,5 @@
parameters:
ignoreErrors:
- -
- message: '#^PHPDoc tag @var with type string is not subtype of native type non\-falsy\-string\|true\.$#'
- identifier: varTag.nativeType
- count: 1
- path: src/Command/BakeSeedCommand.php
-
- -
- message: '#^Strict comparison using \!\=\= between string and false will always evaluate to true\.$#'
- identifier: notIdentical.alwaysTrue
- count: 1
- path: src/Command/BakeSeedCommand.php
-
- -
- message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(int\|string\)\: mixed\)\|null, Closure\(string\)\: string given\.$#'
- identifier: argument.type
- count: 1
- path: src/Db/Adapter/AbstractAdapter.php
-
- -
- message: '#^Unsafe usage of new static\(\)\.$#'
- identifier: new.static
- count: 1
- path: src/Db/Adapter/AdapterFactory.php
-
- -
- message: '#^Offset ''id'' on non\-empty\-array\ in isset\(\) always exists and is not nullable\.$#'
- identifier: isset.offset
- count: 2
- path: src/Db/Adapter/MysqlAdapter.php
-
-
message: '#^Strict comparison using \!\=\= between Cake\\Database\\StatementInterface and null will always evaluate to true\.$#'
identifier: notIdentical.alwaysTrue
@@ -41,39 +11,3 @@ parameters:
identifier: method.notFound
count: 1
path: src/Db/Table/Index.php
-
- -
- message: '#^Call to an undefined method Migrations\\MigrationInterface\:\:down\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: src/Migration/Environment.php
-
- -
- message: '#^Call to an undefined method Migrations\\MigrationInterface\:\:up\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: src/Migration/Environment.php
-
- -
- message: '#^Call to function method_exists\(\) with Migrations\\MigrationInterface and ''useTransactions'' will always evaluate to true\.$#'
- identifier: function.alreadyNarrowedType
- count: 1
- path: src/Migration/Environment.php
-
- -
- message: '#^Parameter \#1 \.\.\.\$arg1 of function max expects non\-empty\-array, array given\.$#'
- identifier: argument.type
- count: 1
- path: src/Migration/Manager.php
-
- -
- message: '#^Offset 0 on non\-empty\-list\ in isset\(\) always exists and is not nullable\.$#'
- identifier: isset.offset
- count: 2
- path: src/Util/TableFinder.php
-
- -
- message: '#^Possibly invalid array key type Cake\\Database\\Schema\\TableSchemaInterface\|string\.$#'
- identifier: offsetAccess.invalidOffset
- count: 2
- path: src/View/Helper/MigrationHelper.php
diff --git a/phpstan.neon b/phpstan.neon
index d3803bdec..c95484c60 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -2,9 +2,15 @@ includes:
- phpstan-baseline.neon
parameters:
- level: 7
+ level: 8
paths:
- src/
+ excludePaths:
+ # rector/rector is installed on-demand via the rector-setup composer
+ # script, so PHPStan cannot resolve AbstractRector or the Symplify
+ # value objects during the main analysis run. The rule is autoloaded
+ # for downstream apps and type-checked by rector itself when invoked.
+ - src/Rector/
bootstrapFiles:
- tests/bootstrap.php
ignoreErrors:
diff --git a/rector.php b/rector.php
new file mode 100644
index 000000000..a93e7edcf
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,54 @@
+withPaths([
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ])
+
+ ->withCache(
+ cacheClass: FileCacheStorage::class,
+ cacheDirectory: $cacheDir,
+ )
+
+ ->withPhpSets()
+ ->withAttributesSets()
+
+ ->withSets([
+ SetList::CODE_QUALITY,
+ SetList::CODING_STYLE,
+ SetList::DEAD_CODE,
+ SetList::EARLY_RETURN,
+ SetList::INSTANCEOF,
+ SetList::TYPE_DECLARATION,
+ ])
+
+ ->withSkip([
+ ClassPropertyAssignToConstructorPromotionRector::class,
+ CatchExceptionNameMatchingTypeRector::class,
+ ClosureToArrowFunctionRector::class,
+ RemoveUselessReturnTagRector::class,
+ CompactToVariablesRector::class,
+ ReturnTypeFromStrictFluentReturnRector::class,
+ SplitDoubleAssignRector::class,
+ NewlineAfterStatementRector::class,
+ ExplicitBoolCompareRector::class,
+ TypedPropertyFromCreateMockAssignRector::class,
+ ]);
diff --git a/src/BaseMigration.php b/src/BaseMigration.php
index 2969933aa..6d61260b1 100644
--- a/src/BaseMigration.php
+++ b/src/BaseMigration.php
@@ -30,24 +30,18 @@ class BaseMigration implements MigrationInterface
{
/**
* The Adapter instance
- *
- * @var \Migrations\Db\Adapter\AdapterInterface
*/
protected ?AdapterInterface $adapter = null;
/**
* The ConsoleIo instance
- *
- * @var \Cake\Console\ConsoleIo
*/
protected ?ConsoleIo $io = null;
/**
* The config instance.
- *
- * @var \Migrations\Config\ConfigInterface
*/
- protected ?ConfigInterface $config;
+ protected ?ConfigInterface $config = null;
/**
* List of all the table objects created by this migration
@@ -58,15 +52,11 @@ class BaseMigration implements MigrationInterface
/**
* Is migrating up prop
- *
- * @var bool
*/
protected bool $isMigratingUp = true;
/**
* The version number.
- *
- * @var int
*/
protected int $version;
@@ -77,8 +67,6 @@ class BaseMigration implements MigrationInterface
* This option is global for all tables created in the migration file.
* If you set it to false, you have to manually add the primary keys for your
* tables using the Migrations\Table::addPrimaryKey() method
- *
- * @var bool
*/
public bool $autoId = true;
@@ -110,7 +98,7 @@ public function setAdapter(AdapterInterface $adapter)
*/
public function getAdapter(): AdapterInterface
{
- if (!$this->adapter) {
+ if (!$this->adapter instanceof AdapterInterface) {
throw new RuntimeException('Adapter not set.');
}
@@ -458,18 +446,13 @@ public function index(string|array $columns): Index
*/
public function preFlightCheck(): void
{
- if (method_exists($this, MigrationInterface::CHANGE)) {
- if (
- method_exists($this, MigrationInterface::UP) ||
- method_exists($this, MigrationInterface::DOWN)
- ) {
- $io = $this->getIo();
- if ($io) {
- $io->out(
- 'warning Migration contains both change() and up()/down() methods.' .
- ' Ignoring up() and down().',
- );
- }
+ if (method_exists($this, MigrationInterface::CHANGE) && (method_exists($this, MigrationInterface::UP) || method_exists($this, MigrationInterface::DOWN))) {
+ $io = $this->getIo();
+ if ($io instanceof ConsoleIo) {
+ $io->out(
+ 'warning Migration contains both change() and up()/down() methods.' .
+ ' Ignoring up() and down().',
+ );
}
}
}
@@ -498,6 +481,66 @@ public function shouldExecute(): bool
return true;
}
+ /**
+ * Creates a view.
+ *
+ * This is a convenience method that creates a dummy table to associate the view with.
+ * Views are not directly associated with tables, but the Table class is used to
+ * manage the migration actions.
+ *
+ * @param string $viewName View name
+ * @param string $definition SQL SELECT statement for the view
+ * @param array $options View options
+ * @return void
+ */
+ public function createView(string $viewName, string $definition, array $options = []): void
+ {
+ $table = $this->table($viewName);
+ $table->createView($viewName, $definition, $options)->save();
+ }
+
+ /**
+ * Drops a view.
+ *
+ * @param string $viewName View name
+ * @param array $options View options
+ * @return void
+ */
+ public function dropView(string $viewName, array $options = []): void
+ {
+ $table = $this->table($viewName);
+ $table->dropView($viewName, $options)->save();
+ }
+
+ /**
+ * Creates a trigger on a table.
+ *
+ * @param string $tableName Table name
+ * @param string $triggerName Trigger name
+ * @param string|array $event Event(s) that fire the trigger (INSERT, UPDATE, DELETE)
+ * @param string $definition Trigger body/definition
+ * @param array $options Trigger options
+ * @return void
+ */
+ public function createTrigger(string $tableName, string $triggerName, string|array $event, string $definition, array $options = []): void
+ {
+ $table = $this->table($tableName);
+ $table->createTrigger($triggerName, $event, $definition, $options)->save();
+ }
+
+ /**
+ * Drops a trigger from a table.
+ *
+ * @param string $tableName Table name
+ * @param string $triggerName Trigger name
+ * @return void
+ */
+ public function dropTrigger(string $tableName, string $triggerName): void
+ {
+ $table = $this->table($tableName);
+ $table->dropTrigger($triggerName)->save();
+ }
+
/**
* Makes sure the version int is within range for valid datetime.
* This is required to have a meaningful order in the overview.
diff --git a/src/BaseSeed.php b/src/BaseSeed.php
index 146abc4e1..8cde6fb73 100644
--- a/src/BaseSeed.php
+++ b/src/BaseSeed.php
@@ -25,31 +25,18 @@ class BaseSeed implements SeedInterface
{
/**
* The Adapter instance
- *
- * @var \Migrations\Db\Adapter\AdapterInterface
*/
protected ?AdapterInterface $adapter = null;
/**
* The ConsoleIo instance
- *
- * @var \Cake\Console\ConsoleIo
*/
protected ?ConsoleIo $io = null;
/**
* The config instance.
- *
- * @var \Migrations\Config\ConfigInterface
- */
- protected ?ConfigInterface $config;
-
- /**
- * No-op constructor.
*/
- public function __construct()
- {
- }
+ protected ?ConfigInterface $config = null;
/**
* {@inheritDoc}
@@ -81,7 +68,7 @@ public function setAdapter(AdapterInterface $adapter)
*/
public function getAdapter(): AdapterInterface
{
- if (!$this->adapter) {
+ if (!$this->adapter instanceof AdapterInterface) {
throw new RuntimeException('Adapter not set.');
}
@@ -130,10 +117,8 @@ public function setConfig(ConfigInterface $config)
public function getName(): string
{
$name = static::class;
- if (str_starts_with($name, 'Migrations\BaseSeed@anonymous')) {
- if (preg_match('#[/\\\\]([a-zA-Z0-9_]+)\.php:#', $name, $matches)) {
- $name = $matches[1];
- }
+ if (str_starts_with($name, 'Migrations\BaseSeed@anonymous') && preg_match('#[/\\\\](\w+)\.php:#', $name, $matches)) {
+ return $matches[1];
}
return $name;
@@ -238,7 +223,7 @@ public function isIdempotent(): bool
public function call(string $seeder, array $options = []): void
{
$io = $this->getIo();
- if ($io === null) {
+ if (!$io instanceof ConsoleIo) {
throw new RuntimeException('ConsoleIo is required for calling other seeders.');
}
$io->out('');
@@ -278,8 +263,8 @@ protected function runCall(string $seeder, array $options = []): void
$options += [
'connection' => $connection,
- 'plugin' => $pluginName ?? $config['plugin'],
- 'source' => $config['source'],
+ 'plugin' => $pluginName ?? ($config instanceof ConfigInterface ? $config['plugin'] : null),
+ 'source' => $config instanceof ConfigInterface ? $config['source'] : null,
];
$factory = new ManagerFactory([
'connection' => $options['connection'],
@@ -287,7 +272,7 @@ protected function runCall(string $seeder, array $options = []): void
'source' => $options['source'],
]);
$io = $this->getIo();
- if ($io === null) {
+ if (!$io instanceof ConsoleIo) {
throw new RuntimeException('ConsoleIo is required for calling other seeders.');
}
$manager = $factory->createManager($io);
diff --git a/src/Command/BakeMigrationCommand.php b/src/Command/BakeMigrationCommand.php
index c727b03ee..ce9af3cb2 100644
--- a/src/Command/BakeMigrationCommand.php
+++ b/src/Command/BakeMigrationCommand.php
@@ -29,9 +29,6 @@
*/
class BakeMigrationCommand extends BakeSimpleMigrationCommand
{
- /**
- * @var string
- */
protected string $_name;
/**
@@ -45,7 +42,7 @@ public static function defaultName(): string
/**
* @inheritDoc
*/
- public function bake(string $name, Arguments $args, ConsoleIo $io): void
+ protected function bake(string $name, Arguments $args, ConsoleIo $io): void
{
EventManager::instance()->on('Bake.initialize', function (Event $event): void {
/** @var \Bake\View\BakeView $view */
@@ -95,7 +92,7 @@ public function templateData(Arguments $arguments): array
$action = $this->detectAction($className);
if (!$action && count($fields)) {
- $this->io->abort('When applying fields the migration name should start with one of the following prefixes: `Create`, `Drop`, `Add`, `Remove`, `Alter`. See: https://book.cakephp.org/migrations/5/en/index.html#migrations-file-name');
+ $this->io->abort('When applying fields the migration name should start with one of the following prefixes: `Create`, `Drop`, `Add`, `Remove`, `Alter`. See: https://book.cakephp.org/migrations/5/getting-started/creating-migrations.html#migration-file-names');
}
if (!$action) {
@@ -248,7 +245,7 @@ public function getOptionParser(): ConsoleOptionParser
/**
* @inheritDoc
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = parent::buildOptionParser($parser);
diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php
index d0490dafe..e03c8b1eb 100644
--- a/src/Command/BakeMigrationDiffCommand.php
+++ b/src/Command/BakeMigrationDiffCommand.php
@@ -33,6 +33,7 @@
use Cake\Event\Event;
use Cake\Event\EventManager;
use Error;
+use Migrations\Db\Adapter\UnifiedMigrationsTableStorage;
use Migrations\Migration\ManagerFactory;
use Migrations\Util\TableFinder;
use Migrations\Util\UtilTrait;
@@ -49,29 +50,21 @@ class BakeMigrationDiffCommand extends BakeSimpleMigrationCommand
/**
* Array of migrations that have already been migrated
- *
- * @var array
*/
protected array $migratedItems = [];
/**
* Path to the migration files
- *
- * @var string
*/
protected string $migrationsPath;
/**
* Migration files that are stored in the self::migrationsPath
- *
- * @var array
*/
protected array $migrationsFiles = [];
/**
* Name of the phinx log table
- *
- * @var string
*/
protected string $phinxTable;
@@ -109,6 +102,14 @@ class BakeMigrationDiffCommand extends BakeSimpleMigrationCommand
*/
protected array $templateData = [];
+ /**
+ * @inheritDoc
+ */
+ public static function getDescription(): string
+ {
+ return 'Create a migration containing the difference between the current schema and migration state.';
+ }
+
/**
* @inheritDoc
*/
@@ -120,7 +121,7 @@ public static function defaultName(): string
/**
* @inheritDoc
*/
- public function bake(string $name, Arguments $args, ConsoleIo $io): void
+ protected function bake(string $name, Arguments $args, ConsoleIo $io): void
{
$this->setup($args);
@@ -169,13 +170,18 @@ protected function setup(Arguments $args): void
$migratedItems = [];
if ($tableExists) {
- $query = $connection->selectQuery();
- /** @var array $migratedItems */
- $migratedItems = $query
+ $query = $connection->selectQuery()
->select(['version'])
->from($this->phinxTable)
- ->orderBy(['version DESC'])
- ->execute()->fetchAll('assoc');
+ ->orderBy(['version DESC']);
+
+ // In unified table mode, filter by the current plugin context
+ if ($this->phinxTable === UnifiedMigrationsTableStorage::TABLE_NAME) {
+ $query->where(['plugin IS' => $this->plugin]);
+ }
+
+ /** @var array $migratedItems */
+ $migratedItems = $query->execute()->fetchAll('assoc');
}
$this->migratedItems = $migratedItems;
@@ -265,7 +271,7 @@ protected function getColumns(): void
foreach ($addedColumns as $columnName) {
$column = $this->safeGetColumn($currentSchema, $columnName);
/** @var int $key */
- $key = array_search($columnName, $currentColumns);
+ $key = array_search($columnName, $currentColumns, true);
if ($key > 0) {
$column['after'] = $currentColumns[$key - 1];
}
@@ -279,7 +285,18 @@ protected function getColumns(): void
// changes in columns meta-data
foreach ($currentColumns as $columnName) {
$column = $this->safeGetColumn($currentSchema, $columnName);
+ if ($column === null) {
+ continue;
+ }
+ if (!in_array($columnName, $oldColumns, true)) {
+ continue;
+ }
+
$oldColumn = $this->safeGetColumn($this->dumpSchema[$table], $columnName);
+ if ($oldColumn === null) {
+ continue;
+ }
+
unset(
$column['collate'],
$column['fixed'],
@@ -287,10 +304,7 @@ protected function getColumns(): void
$oldColumn['fixed'],
);
- if (
- in_array($columnName, $oldColumns, true) &&
- $column !== $oldColumn
- ) {
+ if ($column !== $oldColumn) {
$changedAttributes = array_diff_assoc($column, $oldColumn);
foreach (['type', 'length', 'null', 'default'] as $attribute) {
@@ -353,16 +367,14 @@ protected function getColumns(): void
$this->templateData[$table]['columns']['remove'] = [];
}
$removedColumns = array_diff($oldColumns, $currentColumns);
- if ($removedColumns) {
- foreach ($removedColumns as $columnName) {
- $column = $this->safeGetColumn($this->dumpSchema[$table], $columnName);
- /** @var int $key */
- $key = array_search($columnName, $oldColumns);
- if ($key > 0) {
- $column['after'] = $oldColumns[$key - 1];
- }
- $this->templateData[$table]['columns']['remove'][$columnName] = $column;
+ foreach ($removedColumns as $columnName) {
+ $column = $this->safeGetColumn($this->dumpSchema[$table], $columnName);
+ /** @var int $key */
+ $key = array_search($columnName, $oldColumns, true);
+ if ($key > 0) {
+ $column['after'] = $oldColumns[$key - 1];
}
+ $this->templateData[$table]['columns']['remove'][$columnName] = $column;
}
}
}
@@ -385,9 +397,10 @@ protected function getConstraints(): void
// brand new constraints
$addedConstraints = array_diff($currentConstraints, $oldConstraints);
foreach ($addedConstraints as $constraintName) {
- $this->templateData[$table]['constraints']['add'][$constraintName] =
- $currentSchema->getConstraint($constraintName);
$constraint = $currentSchema->getConstraint($constraintName);
+ if ($constraint === null) {
+ continue;
+ }
if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
$this->templateData[$table]['constraints']['add'][$constraintName] = $constraint;
} else {
@@ -399,13 +412,18 @@ protected function getConstraints(): void
// if present in both, check if they are the same : if not, remove the old one and add the new one
foreach ($currentConstraints as $constraintName) {
$constraint = $currentSchema->getConstraint($constraintName);
+ if ($constraint === null) {
+ continue;
+ }
+ $oldConstraint = $this->dumpSchema[$table]->getConstraint($constraintName);
if (
in_array($constraintName, $oldConstraints, true) &&
- $constraint !== $this->dumpSchema[$table]->getConstraint($constraintName)
+ $constraint !== $oldConstraint
) {
- $this->templateData[$table]['constraints']['remove'][$constraintName] =
- $this->dumpSchema[$table]->getConstraint($constraintName);
+ if ($oldConstraint !== null) {
+ $this->templateData[$table]['constraints']['remove'][$constraintName] = $oldConstraint;
+ }
$this->templateData[$table]['constraints']['add'][$constraintName] =
$constraint;
}
@@ -415,6 +433,9 @@ protected function getConstraints(): void
$removedConstraints = array_diff($oldConstraints, $currentConstraints);
foreach ($removedConstraints as $constraintName) {
$constraint = $this->dumpSchema[$table]->getConstraint($constraintName);
+ if ($constraint === null) {
+ continue;
+ }
if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
$this->templateData[$table]['constraints']['remove'][$constraintName] = $constraint;
} else {
@@ -469,10 +490,8 @@ protected function getIndexes(): void
$removedIndexes = array_diff($oldIndexes, $currentIndexes);
$parts = [];
- if ($removedIndexes) {
- foreach ($removedIndexes as $index) {
- $parts[$index] = $this->dumpSchema[$table]->getIndex($index);
- }
+ foreach ($removedIndexes as $index) {
+ $parts[$index] = $this->dumpSchema[$table]->getIndex($index);
}
$this->templateData[$table]['indexes']['remove'] = array_merge(
$this->templateData[$table]['indexes']['remove'],
@@ -484,22 +503,56 @@ protected function getIndexes(): void
/**
* Checks that the migrations history is in sync with the migrations files
*
+ * Compares the last migrated version against the last migration file,
+ * but only considers migrations that belong to the current context (app or plugin).
+ * This avoids false positives when migrations from other plugins have been
+ * run more recently than the last app migration.
+ *
+ * @see https://github.com/cakephp/migrations/issues/1060
* @return bool Whether migrations history is sync or not
*/
protected function checkSync(): bool
{
+ // No files and no migrations - nothing to check
if (!$this->migrationsFiles && !$this->migratedItems) {
return true;
}
- if ($this->migratedItems) {
- $lastVersion = $this->migratedItems[0]['version'];
- $lastFile = end($this->migrationsFiles);
+ // No files in this context - nothing to sync
+ // This allows baking a snapshot for a new plugin even when
+ // the unified migrations table has records from other contexts
+ if (!$this->migrationsFiles) {
+ return true;
+ }
- return $lastFile && str_contains($lastFile, (string)$lastVersion);
+ // Have files - need to verify they're synced
+ // Extract version numbers from current context's migration files
+ $fileVersions = [];
+ foreach ($this->migrationsFiles as $file) {
+ $filename = basename((string)$file);
+ if (preg_match('/^(\d+)_/', $filename, $matches)) {
+ $fileVersions[] = $matches[1];
+ }
+ }
+
+ // Filter migrated versions to only those that have corresponding files in this context
+ $contextMigratedVersions = [];
+ foreach ($this->migratedItems as $item) {
+ if (in_array((string)$item['version'], $fileVersions, true)) {
+ $contextMigratedVersions[] = (string)$item['version'];
+ }
+ }
+
+ if ($contextMigratedVersions === []) {
+ // No migrations from this context have been run yet
+ return false;
}
- return false;
+ // Get the most recent migrated version within this context
+ $lastVersion = max($contextMigratedVersions);
+ $lastFile = end($this->migrationsFiles);
+
+ return $lastFile && str_contains((string)$lastFile, $lastVersion);
}
/**
@@ -515,6 +568,7 @@ protected function bakeSnapshot(string $name, Arguments $args, ConsoleIo $io): ?
{
$io->out('Your migrations history is empty and you do not have any migrations files.');
$io->out('Falling back to baking a snapshot...');
+
$newArgs = [];
$newArgs[] = $name;
@@ -604,10 +658,13 @@ protected function getCurrentSchema(): array
}
foreach ($tablesToDescribe as $table) {
- if (preg_match('/^.*phinxlog$/', $table) === 1) {
+ if (preg_match('/^.*phinxlog$/', (string)$table) === 1) {
+ continue;
+ }
+ if ($table === 'cake_migrations') {
continue;
}
- if ($table === 'cake_migrations' || $table === 'cake_seeds') {
+ if ($table === 'cake_seeds') {
continue;
}
diff --git a/src/Command/BakeMigrationSnapshotCommand.php b/src/Command/BakeMigrationSnapshotCommand.php
index 259b62bfd..375235255 100644
--- a/src/Command/BakeMigrationSnapshotCommand.php
+++ b/src/Command/BakeMigrationSnapshotCommand.php
@@ -35,10 +35,15 @@ class BakeMigrationSnapshotCommand extends BakeSimpleMigrationCommand
use SnapshotTrait;
use UtilTrait;
+ protected string $_name;
+
/**
- * @var string
+ * @inheritDoc
*/
- protected string $_name;
+ public static function getDescription(): string
+ {
+ return 'Create a migration snapshot of the current schema.';
+ }
/**
* @inheritDoc
@@ -51,7 +56,7 @@ public static function defaultName(): string
/**
* @inheritDoc
*/
- public function bake(string $name, Arguments $args, ConsoleIo $io): void
+ protected function bake(string $name, Arguments $args, ConsoleIo $io): void
{
$collection = $this->getCollection($this->connection);
diff --git a/src/Command/BakeSeedCommand.php b/src/Command/BakeSeedCommand.php
index 8cedabf45..3eed89180 100644
--- a/src/Command/BakeSeedCommand.php
+++ b/src/Command/BakeSeedCommand.php
@@ -32,22 +32,23 @@ class BakeSeedCommand extends SimpleBakeCommand
{
/**
* path to Migration directory
- *
- * @var string
*/
public string $pathFragment = 'config/Seeds/';
- /**
- * @var string
- */
protected string $_name;
/**
* Arguments
- *
- * @var \Cake\Console\Arguments|null
*/
- protected ?Arguments $args = null;
+ protected Arguments $args;
+
+ /**
+ * @inheritDoc
+ */
+ public static function getDescription(): string
+ {
+ return 'Create a migration seed.';
+ }
/**
* @inheritDoc
@@ -91,7 +92,7 @@ public function getPath(Arguments $args): string
*/
public function template(): string
{
- $style = $this->args?->getOption('style') ?? Configure::read('Migrations.style', 'traditional');
+ $style = $this->args->getOption('style') ?? Configure::read('Migrations.style', 'traditional');
if ($style === 'anonymous') {
return 'Migrations.Seed/seed-anonymous';
}
@@ -123,8 +124,7 @@ public function templateData(Arguments $arguments): array
if ($arguments->getOption('data')) {
$limit = (int)$arguments->getOption('limit');
- /** @var string $fields */
- $fields = $arguments->getOption('fields') ?: '*';
+ $fields = (string)$arguments->getOption('fields') ?: '*';
if ($fields !== '*') {
$fields = explode(',', $fields);
}
@@ -160,7 +160,7 @@ public function templateData(Arguments $arguments): array
/**
* @inheritDoc
*/
- public function bake(string $name, Arguments $args, ConsoleIo $io): void
+ protected function bake(string $name, Arguments $args, ConsoleIo $io): void
{
$this->args = $args;
/** @var array $options */
@@ -180,7 +180,7 @@ public function bake(string $name, Arguments $args, ConsoleIo $io): void
* @param \Cake\Console\ConsoleOptionParser $parser Option parser to update.
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = parent::buildOptionParser($parser);
@@ -221,6 +221,7 @@ protected function prettifyArray(array $array, int $tabCount = 3, string $indent
$lines = explode("\n", $content);
$inString = false;
+ $removeKeys = [];
foreach ($lines as $k => &$line) {
if ($k === 0) {
@@ -244,7 +245,7 @@ protected function prettifyArray(array $array, int $tabCount = 3, string $indent
$tabCount--;
} elseif (preg_match("/^\d+\s\=\>\s$/", $line)) {
// Mark '0 =>' kind of lines to remove
- $line = false;
+ $removeKeys[] = $k;
continue;
}
@@ -257,7 +258,7 @@ protected function prettifyArray(array $array, int $tabCount = 3, string $indent
if ($line[$j] === '\\') {
// skip character right after an escape \
$j++;
- } elseif ($line[$j] === '\'') {
+ } elseif ($line[$j] === "'") {
// check string open/end
$inString = !$inString;
}
@@ -271,10 +272,9 @@ protected function prettifyArray(array $array, int $tabCount = 3, string $indent
}
unset($line);
- // Remove marked lines
- $lines = array_filter($lines, function ($line) {
- return $line !== false;
- });
+ foreach ($removeKeys as $key) {
+ unset($lines[$key]);
+ }
return implode("\n", $lines);
}
diff --git a/src/Command/BakeSimpleMigrationCommand.php b/src/Command/BakeSimpleMigrationCommand.php
index 622b61fcc..b85f0f7e3 100644
--- a/src/Command/BakeSimpleMigrationCommand.php
+++ b/src/Command/BakeSimpleMigrationCommand.php
@@ -44,24 +44,18 @@ abstract class BakeSimpleMigrationCommand extends SimpleBakeCommand
/**
* path to Migration directory
- *
- * @var string
*/
public string $pathFragment = 'config';
/**
* Console IO
- *
- * @var \Cake\Console\ConsoleIo|null
*/
- protected ?ConsoleIo $io = null;
+ protected ConsoleIo $io;
/**
* Arguments
- *
- * @var \Cake\Console\Arguments|null
*/
- protected ?Arguments $args = null;
+ protected Arguments $args;
/**
* @inheritDoc
@@ -154,7 +148,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
/**
* @inheritDoc
*/
- public function bake(string $name, Arguments $args, ConsoleIo $io): void
+ protected function bake(string $name, Arguments $args, ConsoleIo $io): void
{
$this->io = $io;
$this->args = $args;
@@ -189,6 +183,7 @@ public function bake(string $name, Arguments $args, ConsoleIo $io): void
$renderer = new TemplateRenderer($this->theme);
$renderer->set('name', $name);
$renderer->set($this->templateData($args));
+
$contents = $renderer->generate($this->template());
$path = $this->getPath($args);
@@ -241,7 +236,7 @@ protected function getMigrationName(?string $name = null): string
* @param \Cake\Console\ConsoleOptionParser $parser Option parser to update.
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->_setCommonOptions($parser);
@@ -271,7 +266,6 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
/**
* If reserved PHP keyword.
*
- * @param string $name
* @return bool
*/
protected function isReservedKeyword(string $name): bool
diff --git a/src/Command/DumpCommand.php b/src/Command/DumpCommand.php
index ff779e598..10bf3982d 100644
--- a/src/Command/DumpCommand.php
+++ b/src/Command/DumpCommand.php
@@ -32,6 +32,14 @@ class DumpCommand extends Command
{
protected string $connection;
+ /**
+ * @inheritDoc
+ */
+ public static function getDescription(): string
+ {
+ return 'Create a migration state dump file.';
+ }
+
/**
* The default name added to the application command list
*
@@ -45,7 +53,6 @@ public static function defaultName(): string
/**
* Extract options for the dump command from another migrations option parser.
*
- * @param \Cake\Console\Arguments $args
* @return array
*/
public static function extractArgs(Arguments $args): array
@@ -74,7 +81,7 @@ public static function extractArgs(Arguments $args): array
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription([
'Dumps the current schema of the database to be used while baking a diff',
@@ -133,21 +140,19 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
$tables = $finder->getTablesToBake($collection, $options);
$dump = [];
- if ($tables) {
- foreach ($tables as $table) {
- $schema = $collection->describe($table);
- $dump[$table] = $schema;
- }
+ foreach ($tables as $table) {
+ $schema = $collection->describe($table);
+ $dump[$table] = $schema;
}
$filePath = $path . DS . 'schema-dump-' . $connectionName . '.lock';
- $io->verbose("Writing dump file `{$filePath}`...");
+ $io->verbose(sprintf('Writing dump file `%s`...', $filePath));
if (file_put_contents($filePath, serialize($dump))) {
- $io->verbose("Dump file `{$filePath}` was successfully written");
+ $io->verbose(sprintf('Dump file `%s` was successfully written', $filePath));
return self::CODE_SUCCESS;
}
- $io->err("An error occurred while writing dump file `{$filePath}`");
+ $io->err(sprintf('An error occurred while writing dump file `%s`', $filePath));
return self::CODE_ERROR;
}
diff --git a/src/Command/EntryCommand.php b/src/Command/EntryCommand.php
index 3651d928d..0577b2dbc 100644
--- a/src/Command/EntryCommand.php
+++ b/src/Command/EntryCommand.php
@@ -31,8 +31,6 @@ class EntryCommand extends Command implements CommandCollectionAwareInterface
{
/**
* The command collection to get help on.
- *
- * @var \Cake\Console\CommandCollection
*/
protected CommandCollection $commands;
@@ -109,7 +107,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
if ($args->hasArgumentAt(0)) {
$name = $args->getArgumentAt(0);
$io->err(
- "Could not find migrations command named `$name`."
+ sprintf('Could not find migrations command named `%s`.', $name)
. ' Run `migrations --help` to get a list of commands.',
);
@@ -135,7 +133,7 @@ public function getHelp(): HelpCommand
// Remove `migrations`
array_shift($parts);
- if (count($parts) === 0) {
+ if ($parts === []) {
continue;
}
$commands[$command] = $class;
diff --git a/src/Command/MarkMigratedCommand.php b/src/Command/MarkMigratedCommand.php
index bb7cb6f44..cbfe55014 100644
--- a/src/Command/MarkMigratedCommand.php
+++ b/src/Command/MarkMigratedCommand.php
@@ -26,6 +26,14 @@
*/
class MarkMigratedCommand extends Command
{
+ /**
+ * @inheritDoc
+ */
+ public static function getDescription(): string
+ {
+ return 'Mark one or more migrations as run.';
+ }
+
/**
* The default name added to the application command list
*
@@ -42,7 +50,7 @@ public static function defaultName(): string
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription([
'Mark a migration as applied',
@@ -95,9 +103,12 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
*/
protected function invalidOnlyOrExclude(Arguments $args): bool
{
- return ($args->getOption('exclude') && $args->getOption('only')) ||
- ($args->getOption('exclude') || $args->getOption('only')) &&
- $args->getOption('target') === null;
+ if ($args->getOption('exclude') && $args->getOption('only')) {
+ return true;
+ }
+
+ return ($args->getOption('exclude') || $args->getOption('only')) &&
+ $args->getOption('target') === null;
}
/**
@@ -135,7 +146,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
}
$output = $manager->markVersionsAsMigrated($path, $versions);
- array_map(fn($line) => $io->out($line), $output);
+ array_map(fn(string $line): ?int => $io->out($line), $output);
return self::CODE_SUCCESS;
}
diff --git a/src/Command/MigrateCommand.php b/src/Command/MigrateCommand.php
index abc73655f..702943337 100644
--- a/src/Command/MigrateCommand.php
+++ b/src/Command/MigrateCommand.php
@@ -29,10 +29,15 @@
*/
class MigrateCommand extends Command
{
+ use EventDispatcherTrait;
+
/**
- * @use \Cake\Event\EventDispatcherTrait<\Migrations\Command\MigrateCommand>
+ * @inheritDoc
*/
- use EventDispatcherTrait;
+ public static function getDescription(): string
+ {
+ return 'Run un-applied migrations.';
+ }
/**
* The default name added to the application command list
@@ -50,7 +55,7 @@ public static function defaultName(): string
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription([
'Apply migrations to a SQL datasource',
@@ -152,7 +157,7 @@ protected function executeMigrations(Arguments $args, ConsoleIo $io): ?int
if ($config->isDryRun()) {
$io->info('DRY-RUN mode enabled');
}
- $io->verbose('using connection ' . (string)$args->getOption('connection'));
+ $io->verbose('using connection ' . $args->getOption('connection'));
$io->verbose('using paths ' . $config->getMigrationPath());
$io->verbose('ordering by ' . $versionOrder . ' time');
diff --git a/src/Command/ResetCommand.php b/src/Command/ResetCommand.php
new file mode 100644
index 000000000..f5a8d021c
--- /dev/null
+++ b/src/Command/ResetCommand.php
@@ -0,0 +1,287 @@
+setDescription([
+ 'Drop all tables and re-run all migrations.',
+ '',
+ 'This is a destructive operation!',
+ 'All data in the database will be lost.',
+ '',
+ 'migrations reset',
+ 'migrations reset -c secondary',
+ 'migrations reset --dry-run',
+ ])->addOption('plugin', [
+ 'short' => 'p',
+ 'help' => 'The plugin to run migrations for',
+ ])->addOption('connection', [
+ 'short' => 'c',
+ 'help' => 'The datasource connection to use',
+ 'default' => 'default',
+ ])->addOption('source', [
+ 'short' => 's',
+ 'default' => ConfigInterface::DEFAULT_MIGRATION_FOLDER,
+ 'help' => 'The folder where your migrations are',
+ ])->addOption('dry-run', [
+ 'short' => 'x',
+ 'help' => 'Preview what tables would be dropped without making changes',
+ 'boolean' => true,
+ ])->addOption('no-lock', [
+ 'help' => 'If present, no lock file will be generated after migrating',
+ 'boolean' => true,
+ ]);
+
+ return $parser;
+ }
+
+ /**
+ * Execute the command.
+ *
+ * @param \Cake\Console\Arguments $args The command arguments.
+ * @param \Cake\Console\ConsoleIo $io The console io
+ * @return int|null The exit code or null for success
+ */
+ public function execute(Arguments $args, ConsoleIo $io): ?int
+ {
+ $event = $this->dispatchEvent('Migration.beforeReset');
+ if ($event->isStopped()) {
+ return $event->getResult() ? self::CODE_SUCCESS : self::CODE_ERROR;
+ }
+
+ $connectionName = (string)$args->getOption('connection');
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get($connectionName);
+ $dryRun = (bool)$args->getOption('dry-run');
+
+ if ($dryRun) {
+ $io->out('DRY-RUN mode enabled - no changes will be made');
+ $io->out('');
+ }
+
+ // Get tables to drop
+ $tablesToDrop = $this->getTablesToDrop($connection);
+
+ if ($tablesToDrop === []) {
+ $io->out('No tables to drop.');
+ $io->out('');
+ $io->out('Running migrations...');
+
+ return $this->runMigrationsAndDispatch($args, $io);
+ }
+
+ // Show what will be dropped
+ $io->out('The following tables will be dropped:');
+ foreach ($tablesToDrop as $table) {
+ $io->out(' - ' . $table);
+ }
+ $io->out('');
+
+ // Ask for confirmation (unless dry-run)
+ if (!$dryRun) {
+ $continue = $io->askChoice(
+ 'This will permanently delete all data. Do you want to continue?',
+ ['y', 'n'],
+ 'n',
+ );
+ if ($continue !== 'y') {
+ $io->warning('Reset operation aborted.');
+
+ return self::CODE_SUCCESS;
+ }
+ }
+
+ // Drop tables
+ $io->out('');
+ if (!$dryRun) {
+ $factory = new ManagerFactory([
+ 'plugin' => $args->getOption('plugin'),
+ 'source' => $args->getOption('source'),
+ 'connection' => $args->getOption('connection'),
+ ]);
+ $manager = $factory->createManager($io);
+ $adapter = $manager->getEnvironment()->getAdapter();
+
+ $this->dropTables($adapter, $tablesToDrop, $io);
+ } else {
+ $io->info('DRY-RUN: Would drop ' . count($tablesToDrop) . ' table(s).');
+ }
+
+ $io->out('');
+
+ // Re-run migrations
+ if (!$dryRun) {
+ return $this->runMigrationsAndDispatch($args, $io);
+ }
+
+ $io->info('DRY-RUN: Would re-run all migrations.');
+
+ return self::CODE_SUCCESS;
+ }
+
+ /**
+ * Get list of tables to drop.
+ *
+ * @param \Cake\Database\Connection $connection Database connection
+ * @return array List of table names
+ */
+ protected function getTablesToDrop(Connection $connection): array
+ {
+ $schema = $connection->getDriver()->schemaDialect();
+
+ return $schema->listTables();
+ }
+
+ /**
+ * Drop tables with foreign key handling.
+ *
+ * @param \Migrations\Db\Adapter\AdapterInterface $adapter The adapter
+ * @param array $tables Tables to drop
+ * @param \Cake\Console\ConsoleIo $io Console IO
+ * @return void
+ */
+ protected function dropTables(AdapterInterface $adapter, array $tables, ConsoleIo $io): void
+ {
+ if (!$adapter instanceof DirectActionInterface) {
+ throw new RuntimeException('The adapter must implement DirectActionInterface');
+ }
+
+ $adapter->disableForeignKeyConstraints();
+
+ try {
+ foreach ($tables as $table) {
+ $io->verbose('Dropping table: ' . $table);
+ $adapter->dropTable($table);
+ }
+ } finally {
+ $adapter->enableForeignKeyConstraints();
+ }
+
+ $io->success('Dropped ' . count($tables) . ' table(s).');
+ }
+
+ /**
+ * Run migrations and dispatch afterReset event.
+ *
+ * @param \Cake\Console\Arguments $args The command arguments.
+ * @param \Cake\Console\ConsoleIo $io The console io
+ * @return int|null The exit code
+ */
+ protected function runMigrationsAndDispatch(Arguments $args, ConsoleIo $io): ?int
+ {
+ $result = $this->runMigrations($args, $io);
+ $this->dispatchEvent('Migration.afterReset');
+
+ return $result;
+ }
+
+ /**
+ * Run migrations.
+ *
+ * @param \Cake\Console\Arguments $args The command arguments.
+ * @param \Cake\Console\ConsoleIo $io The console io
+ * @return int|null The exit code
+ */
+ protected function runMigrations(Arguments $args, ConsoleIo $io): ?int
+ {
+ $factory = new ManagerFactory([
+ 'plugin' => $args->getOption('plugin'),
+ 'source' => $args->getOption('source'),
+ 'connection' => $args->getOption('connection'),
+ 'dry-run' => (bool)$args->getOption('dry-run'),
+ ]);
+
+ $manager = $factory->createManager($io);
+ $config = $manager->getConfig();
+
+ $io->verbose('using connection ' . $args->getOption('connection'));
+ $io->verbose('using paths ' . $config->getMigrationPath());
+
+ try {
+ $start = microtime(true);
+ $manager->migrate(null, false);
+ $end = microtime(true);
+ } catch (Throwable $e) {
+ $io->err('' . $e->getMessage() . '');
+ $io->verbose($e->getTraceAsString());
+
+ return self::CODE_ERROR;
+ }
+
+ $io->comment('All Done. Took ' . sprintf('%.4fs', $end - $start));
+ $io->out('');
+
+ $exitCode = self::CODE_SUCCESS;
+
+ // Run dump command to generate lock file
+ if (!$args->getOption('no-lock') && !$args->getOption('dry-run')) {
+ $io->verbose('');
+ $io->verbose('Dumping the current schema of the database to be used while baking a diff');
+ $io->verbose('');
+
+ $newArgs = DumpCommand::extractArgs($args);
+ $exitCode = $this->executeCommand(DumpCommand::class, $newArgs, $io);
+ }
+
+ return $exitCode;
+ }
+}
diff --git a/src/Command/RollbackCommand.php b/src/Command/RollbackCommand.php
index d3647f542..bf45b0a7b 100644
--- a/src/Command/RollbackCommand.php
+++ b/src/Command/RollbackCommand.php
@@ -30,10 +30,15 @@
*/
class RollbackCommand extends Command
{
+ use EventDispatcherTrait;
+
/**
- * @use \Cake\Event\EventDispatcherTrait<\Migrations\Command\MigrateCommand>
+ * @inheritDoc
*/
- use EventDispatcherTrait;
+ public static function getDescription(): string
+ {
+ return 'Rollback reversible migrations.';
+ }
/**
* The default name added to the application command list
@@ -51,7 +56,7 @@ public static function defaultName(): string
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription([
'Rollback migrations to a specific migration',
@@ -154,7 +159,7 @@ protected function executeMigrations(Arguments $args, ConsoleIo $io): ?int
$config = $manager->getConfig();
$versionOrder = $config->getVersionOrder();
- $io->verbose('using connection ' . (string)$args->getOption('connection'));
+ $io->verbose('using connection ' . $args->getOption('connection'));
$io->verbose('using paths ' . $config->getMigrationPath());
$io->verbose('ordering by ' . $versionOrder . ' time');
@@ -230,7 +235,6 @@ protected function getTargetFromDate(string|bool $date): string
4 => '0101000000',
];
- /** @var string $date */
$dateLength = strlen($date);
if (!isset($dateStrlenToAppend[$dateLength])) {
throw new InvalidArgumentException('Invalid date. Format is YYYY[MM[DD[HH[II[SS]]]]].');
diff --git a/src/Command/SeedCommand.php b/src/Command/SeedCommand.php
index 9ccb761b1..a764d0643 100644
--- a/src/Command/SeedCommand.php
+++ b/src/Command/SeedCommand.php
@@ -28,10 +28,15 @@
*/
class SeedCommand extends Command
{
+ use EventDispatcherTrait;
+
/**
- * @use \Cake\Event\EventDispatcherTrait<\Migrations\Command\MigrateCommand>
+ * @inheritDoc
*/
- use EventDispatcherTrait;
+ public static function getDescription(): string
+ {
+ return 'Run migration seeds.';
+ }
/**
* The default name added to the application command list
@@ -49,7 +54,7 @@ public static function defaultName(): string
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$description = [
'Seed the database with data',
@@ -166,7 +171,7 @@ protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int
if ($fake) {
$io->warning('performing fake seeding');
}
- $io->verbose('using connection ' . (string)$args->getOption('connection'));
+ $io->verbose('using connection ' . $args->getOption('connection'));
$io->verbose('using paths ' . $config->getMigrationPath());
$io->verbose('ordering by ' . $versionOrder . ' time');
diff --git a/src/Command/SeedResetCommand.php b/src/Command/SeedResetCommand.php
index f11964d03..34d5ae3fe 100644
--- a/src/Command/SeedResetCommand.php
+++ b/src/Command/SeedResetCommand.php
@@ -26,6 +26,14 @@
*/
class SeedResetCommand extends Command
{
+ /**
+ * @inheritDoc
+ */
+ public static function getDescription(): string
+ {
+ return 'Reset the seed execution state.';
+ }
+
/**
* The default name added to the application command list
*
@@ -42,7 +50,7 @@ public static function defaultName(): string
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription([
'The reset command removes seed execution records from the log',
@@ -98,7 +106,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
$io->info('DRY-RUN mode enabled');
}
- $io->verbose('using connection ' . (string)$args->getOption('connection'));
+ $io->verbose('using connection ' . $args->getOption('connection'));
$io->verbose('using paths ' . $config->getSeedPath());
$seeds = $manager->getSeeds();
@@ -109,13 +117,13 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
$seedsToReset = $seeds;
if ($seedOption) {
- $requestedSeeds = array_map('trim', explode(',', (string)$seedOption));
+ $requestedSeeds = array_map(trim(...), explode(',', (string)$seedOption));
$seedsToReset = [];
foreach ($requestedSeeds as $requestedSeed) {
$normalizedName = $manager->normalizeSeedName($requestedSeed, $seeds);
if ($normalizedName === null) {
- $io->error("Seed `{$requestedSeed}` does not exist.");
+ $io->error(sprintf('Seed `%s` does not exist.', $requestedSeed));
return self::CODE_ERROR;
}
@@ -123,7 +131,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
}
}
- if (empty($seedsToReset)) {
+ if ($seedsToReset === []) {
$io->warning('No seeds to reset.');
return self::CODE_SUCCESS;
@@ -155,18 +163,18 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
if (!$config->isDryRun()) {
$adapter->removeSeedFromLog($seed);
}
- $io->info("Reset: {$seedName} seed");
+ $io->info(sprintf('Reset: %s seed', $seedName));
$count++;
} else {
- $io->verbose("Skipped (not executed): {$seedName} seed");
+ $io->verbose(sprintf('Skipped (not executed): %s seed', $seedName));
}
}
$io->out('');
if ($config->isDryRun()) {
- $io->success("DRY-RUN: Would reset {$count} seed(s).");
+ $io->success(sprintf('DRY-RUN: Would reset %d seed(s).', $count));
} else {
- $io->success("Reset {$count} seed(s).");
+ $io->success(sprintf('Reset %d seed(s).', $count));
}
return self::CODE_SUCCESS;
diff --git a/src/Command/SeedStatusCommand.php b/src/Command/SeedStatusCommand.php
index 49a49d66f..3a73bbd85 100644
--- a/src/Command/SeedStatusCommand.php
+++ b/src/Command/SeedStatusCommand.php
@@ -17,7 +17,6 @@
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
-use Cake\Core\Configure;
use Migrations\Config\ConfigInterface;
use Migrations\Migration\ManagerFactory;
use Migrations\Util\Util;
@@ -43,7 +42,7 @@ public static function defaultName(): string
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription([
'The status command prints a list of all seeds, along with their execution status',
@@ -91,7 +90,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
$manager = $factory->createManager($io);
$config = $manager->getConfig();
- $io->verbose('using connection ' . (string)$args->getOption('connection'));
+ $io->verbose('using connection ' . $args->getOption('connection'));
$io->verbose('using paths ' . $config->getSeedPath());
$seeds = $manager->getSeeds();
@@ -106,24 +105,14 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
// Build status list
$statuses = [];
- $appNamespace = Configure::read('App.namespace', 'App');
foreach ($seeds as $seed) {
- $plugin = null;
- $className = get_class($seed);
-
- if (str_contains($className, '\\')) {
- $parts = explode('\\', $className);
- if (count($parts) > 1 && $parts[0] !== $appNamespace) {
- $plugin = $parts[0];
- }
- }
-
+ $plugin = Util::getSeedPlugin($seed);
$seedName = $seed->getName();
$executed = false;
$executedAt = null;
foreach ($seedLog as $entry) {
- if ($entry['seed_name'] === $seedName && $entry['plugin'] === $plugin) {
+ if ($entry['seed_name'] === $seedName && Util::matchesSeedPlugin($entry['plugin'], $plugin)) {
$executed = true;
$executedAt = $entry['executed_at'];
break;
@@ -163,8 +152,8 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
$io->out('Current seed execution status:');
$io->out('');
- $maxNameLength = max(array_map(fn($s) => strlen($s['seedName']), $statuses));
- $maxPluginLength = max(array_map(fn($s) => strlen($s['plugin'] ?? ''), $statuses));
+ $maxNameLength = max(array_map(fn(array $s): int => strlen($s['seedName']), $statuses));
+ $maxPluginLength = max(array_map(fn(array $s): int => strlen($s['plugin'] ?? ''), $statuses));
foreach ($statuses as $status) {
$seedName = str_pad($status['seedName'], $maxNameLength);
@@ -174,10 +163,10 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
if ($status['status'] === 'executed') {
$statusText = 'executed';
$date = $status['executedAt'] ? ' (' . $status['executedAt'] . ')' : '';
- $io->out(" {$statusText} {$plugin} {$seedName}{$date}{$idempotent}");
+ $io->out(sprintf(' %s %s %s%s%s', $statusText, $plugin, $seedName, $date, $idempotent));
} else {
$statusText = 'pending ';
- $io->out(" {$statusText} {$plugin} {$seedName}{$idempotent}");
+ $io->out(sprintf(' %s %s %s%s', $statusText, $plugin, $seedName, $idempotent));
}
}
diff --git a/src/Command/SeedsEntryCommand.php b/src/Command/SeedsEntryCommand.php
index e353b7db5..cc0ea6f57 100644
--- a/src/Command/SeedsEntryCommand.php
+++ b/src/Command/SeedsEntryCommand.php
@@ -31,8 +31,6 @@ class SeedsEntryCommand extends Command implements CommandCollectionAwareInterfa
{
/**
* The command collection to get help on.
- *
- * @var \Cake\Console\CommandCollection
*/
protected CommandCollection $commands;
@@ -109,7 +107,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
if ($args->hasArgumentAt(0)) {
$name = $args->getArgumentAt(0);
$io->err(
- "Could not find seeds command named `$name`."
+ sprintf('Could not find seeds command named `%s`.', $name)
. ' Run `seeds --help` to get a list of commands.',
);
@@ -135,7 +133,7 @@ public function getHelp(): HelpCommand
// Remove `seeds`
array_shift($parts);
- if (count($parts) === 0) {
+ if ($parts === []) {
continue;
}
$commands[$command] = $class;
diff --git a/src/Command/StatusCommand.php b/src/Command/StatusCommand.php
index 7d344c604..4161d64f4 100644
--- a/src/Command/StatusCommand.php
+++ b/src/Command/StatusCommand.php
@@ -17,6 +17,7 @@
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
+use Cake\Core\Plugin;
use Migrations\Config\ConfigInterface;
use Migrations\Db\Adapter\UnifiedMigrationsTableStorage;
use Migrations\Migration\ManagerFactory;
@@ -41,6 +42,14 @@ class StatusCommand extends Command
*/
public const CODE_STATUS_DOWN = 3;
+ /**
+ * @inheritDoc
+ */
+ public static function getDescription(): string
+ {
+ return 'Display the state of migrations.';
+ }
+
/**
* The default name added to the application command list
*
@@ -57,18 +66,27 @@ public static function defaultName(): string
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription([
'The status command prints a list of all migrations, along with their current status',
'',
'migrations status -c secondary',
'migrations status -c secondary -f json',
+ 'migrations status --all',
+ 'Print a summary for the app and every loaded plugin that has migrations.',
+ 'Add -v to also print the per-section migration tables.',
'migrations status --cleanup',
'Remove *MISSING* migrations from the migration tracking table',
])->addOption('plugin', [
'short' => 'p',
'help' => 'The plugin to run migrations for',
+ ])->addOption('all', [
+ 'help' => 'Print a status summary for the app and every loaded plugin that has migrations. '
+ . 'Use -v to also print the per-section migration tables. '
+ . 'Cannot be combined with --plugin or --cleanup.',
+ 'boolean' => true,
+ 'default' => false,
])->addOption('connection', [
'short' => 'c',
'help' => 'The datasource connection to use',
@@ -103,6 +121,22 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
/** @var string|null $format */
$format = $args->getOption('format');
$clean = $args->getOption('cleanup');
+ $all = (bool)$args->getOption('all');
+
+ if ($all) {
+ if ($args->getOption('plugin')) {
+ $io->err('The --all option cannot be combined with --plugin.');
+
+ return Command::CODE_ERROR;
+ }
+ if ($clean) {
+ $io->err('The --all option cannot be combined with --cleanup.');
+
+ return Command::CODE_ERROR;
+ }
+
+ return $this->executeAll($args, $io, $format);
+ }
$factory = new ManagerFactory([
'plugin' => $args->getOption('plugin'),
@@ -143,10 +177,175 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
return Command::CODE_SUCCESS;
}
+ /**
+ * Execute the status command for the app and every loaded plugin
+ * that ships migrations.
+ *
+ * @param \Cake\Console\Arguments $args The command arguments.
+ * @param \Cake\Console\ConsoleIo $io The console io.
+ * @param string|null $format Output format.
+ * @return int The exit code: CODE_STATUS_MISSING (2) when there are missing entries,
+ * CODE_STATUS_DOWN (3) when there are pending down migrations, CODE_SUCCESS otherwise.
+ */
+ protected function executeAll(Arguments $args, ConsoleIo $io, ?string $format): int
+ {
+ $sections = ['app' => null];
+ foreach (Plugin::loaded() as $pluginName) {
+ $migrationsPath = Plugin::path($pluginName) . 'config' . DS
+ . $args->getOption('source') . DS;
+ if (!is_dir($migrationsPath)) {
+ continue;
+ }
+ $sections[$pluginName] = $pluginName;
+ }
+
+ $verbose = (bool)$args->getOption('verbose');
+ $jsonResults = [];
+ $summary = [];
+ $exitCode = Command::CODE_SUCCESS;
+
+ foreach ($sections as $label => $plugin) {
+ $factory = new ManagerFactory([
+ 'plugin' => $plugin,
+ 'source' => $args->getOption('source'),
+ 'connection' => $args->getOption('connection'),
+ 'dry-run' => $args->getOption('dry-run'),
+ ]);
+ $manager = $factory->createManager($io);
+ $migrations = $manager->printStatus($format);
+
+ $sectionExit = $this->statusExitCode($migrations);
+ // Precedence: MISSING > DOWN > SUCCESS — once we see MISSING anywhere, keep it.
+ if ($sectionExit === self::CODE_STATUS_MISSING) {
+ $exitCode = self::CODE_STATUS_MISSING;
+ } elseif ($sectionExit === self::CODE_STATUS_DOWN && $exitCode === Command::CODE_SUCCESS) {
+ $exitCode = self::CODE_STATUS_DOWN;
+ }
+
+ $summary[$label] = $this->countActions($migrations);
+
+ if ($format === 'json') {
+ $jsonResults[$label] = $migrations;
+ continue;
+ }
+
+ if (!$verbose) {
+ continue;
+ }
+
+ $heading = $label === 'app' ? 'APP' : $label;
+ $io->out('');
+ $io->out('==================================================');
+ $io->out(sprintf('%s', $heading));
+ $io->out('==================================================');
+ $this->display($migrations, $io, $manager->getSchemaTableName());
+ }
+
+ if ($format === 'json') {
+ $flags = 0;
+ if ($verbose) {
+ $flags = JSON_PRETTY_PRINT;
+ }
+ $io->out((string)json_encode($jsonResults, $flags));
+
+ return $exitCode;
+ }
+
+ $this->displaySummary($io, $summary);
+
+ return $exitCode;
+ }
+
+ /**
+ * Count actionable items (down + missing) in a section's migrations array.
+ *
+ * @param array $migrations The result of {@see Manager::printStatus()}.
+ * @return array{down: int, missing: int}
+ */
+ protected function countActions(array $migrations): array
+ {
+ $down = 0;
+ $missing = 0;
+ foreach ($migrations as $migration) {
+ if (!empty($migration['missing'])) {
+ $missing++;
+ continue;
+ }
+ if (($migration['status'] ?? null) === 'down') {
+ $down++;
+ }
+ }
+
+ return ['down' => $down, 'missing' => $missing];
+ }
+
+ /**
+ * Render the trailing summary block listing sections that need action.
+ *
+ * @param \Cake\Console\ConsoleIo $io The console io.
+ * @param array $summary
+ * @return void
+ */
+ protected function displaySummary(ConsoleIo $io, array $summary): void
+ {
+ $needsAction = array_filter(
+ $summary,
+ fn(array $counts): bool => $counts['down'] > 0 || $counts['missing'] > 0,
+ );
+
+ $io->out('');
+ if (!$needsAction) {
+ $io->out(sprintf(
+ 'Summary: all %d sections are up to date.',
+ count($summary),
+ ));
+
+ return;
+ }
+
+ $io->out(sprintf(
+ 'Summary: %d of %d sections require action:',
+ count($needsAction),
+ count($summary),
+ ));
+ foreach ($needsAction as $label => $counts) {
+ $heading = $label === 'app' ? 'APP' : $label;
+ $parts = [];
+ if ($counts['down'] > 0) {
+ $parts[] = sprintf('%d pending', $counts['down']);
+ }
+ if ($counts['missing'] > 0) {
+ $parts[] = sprintf('%d missing', $counts['missing']);
+ }
+ $io->out(sprintf(' - %s: %s', $heading, implode(', ', $parts)));
+ }
+ }
+
+ /**
+ * Compute the appropriate status exit code for a single section's migrations array.
+ *
+ * @param array $migrations The result of {@see Manager::printStatus()}.
+ * @return int CODE_STATUS_MISSING when missing entries exist, CODE_STATUS_DOWN when
+ * any migration is pending (down), otherwise CODE_SUCCESS.
+ */
+ protected function statusExitCode(array $migrations): int
+ {
+ $hasDown = false;
+ foreach ($migrations as $migration) {
+ if (!empty($migration['missing'])) {
+ return self::CODE_STATUS_MISSING;
+ }
+ if (($migration['status'] ?? null) === 'down') {
+ $hasDown = true;
+ }
+ }
+
+ return $hasDown ? self::CODE_STATUS_DOWN : Command::CODE_SUCCESS;
+ }
+
/**
* Print migration status to stdout.
*
- * @param array $migrations
* @param \Cake\Console\ConsoleIo $io The console io
* @param string $tableName The migration tracking table name
* @return void
diff --git a/src/Command/UpgradeCommand.php b/src/Command/UpgradeCommand.php
index 5897c4589..76c947429 100644
--- a/src/Command/UpgradeCommand.php
+++ b/src/Command/UpgradeCommand.php
@@ -17,6 +17,7 @@
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
+use Cake\Core\Plugin;
use Cake\Database\Connection;
use Cake\Database\Exception\QueryException;
use Cake\Datasource\ConnectionManager;
@@ -34,6 +35,14 @@
*/
class UpgradeCommand extends Command
{
+ /**
+ * @inheritDoc
+ */
+ public static function getDescription(): string
+ {
+ return 'Upgrade migration storage to use cake_migrations.';
+ }
+
/**
* The default name added to the application command list
*
@@ -50,7 +59,7 @@ public static function defaultName(): string
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription([
'Upgrades migration tracking from legacy phinxlog tables to unified cake_migrations table.',
@@ -113,20 +122,20 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
$io->out(sprintf('Found %d phinxlog table(s):', count($legacyTables)));
foreach ($legacyTables as $table => $plugin) {
- $pluginLabel = $plugin === null ? '(app)' : "({$plugin})";
- $io->out(" - {$table} {$pluginLabel}");
+ $pluginLabel = $plugin === null ? '(app)' : sprintf('(%s)', $plugin);
+ $io->out(sprintf(' - %s %s', $table, $pluginLabel));
}
$io->out('');
// Create unified table if needed
$unifiedTableName = UnifiedMigrationsTableStorage::TABLE_NAME;
if (!$this->tableExists($connection, $unifiedTableName)) {
- $io->out("Creating unified table {$unifiedTableName}...");
+ $io->out(sprintf('Creating unified table %s...', $unifiedTableName));
if (!$dryRun) {
$this->createUnifiedTable($connection, $io);
}
} else {
- $io->out("Unified table {$unifiedTableName} already exists.");
+ $io->out(sprintf('Unified table %s already exists.', $unifiedTableName));
}
$io->out('');
@@ -143,10 +152,10 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
if (!$dryRun) {
// Clean up legacy tables
$io->out('');
- foreach ($legacyTables as $tableName => $plugin) {
+ foreach (array_keys($legacyTables) as $tableName) {
if ($dropTables) {
- $io->out("Dropping legacy table {$tableName}...");
- $connection->execute("DROP TABLE {$connection->getDriver()->quoteIdentifier($tableName)}");
+ $io->out(sprintf('Dropping legacy table %s...', $tableName));
+ $connection->execute('DROP TABLE ' . $connection->getDriver()->quoteIdentifier($tableName));
} else {
$io->out('Retaining legacy table. You should drop these tables once you have verified your upgrade.');
}
@@ -156,10 +165,13 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
$io->success('Upgrade complete!');
$io->out('');
$io->out('Next steps:');
- $io->out(' 1. Set \'Migrations\' => [\'legacyTables\' => false] in your config');
- $io->out(' 2. Test your application');
- if (!$dropTables) {
- $io->out(' 3. Optionally drop the empty phinxlog tables (re-run `bin/cake migrations upgrade --drop-tables`)');
+ if ($dropTables) {
+ $io->out(" 1. Set 'Migrations' => ['legacyTables' => false] in your config");
+ $io->out(' 2. Test your application');
+ } else {
+ $io->out(' 1. Test your application');
+ $io->out(' 2. Drop the phinxlog tables (re-run `bin/cake migrations upgrade --drop-tables`)');
+ $io->out(" 3. Set 'Migrations' => ['legacyTables' => false] in your config");
}
} else {
$io->out('');
@@ -181,13 +193,24 @@ protected function findLegacyTables(Connection $connection): array
$tables = $schema->listTables();
$legacyTables = [];
+ // Build a map of expected table prefixes to plugin names for loaded plugins
+ // This allows matching plugins with special characters like CakeDC/Users
+ $pluginPrefixMap = $this->buildPluginPrefixMap();
+
foreach ($tables as $table) {
if ($table === 'phinxlog') {
$legacyTables[$table] = null;
} elseif (str_ends_with($table, '_phinxlog')) {
// Extract plugin name from table name
$prefix = substr($table, 0, -9); // Remove '_phinxlog'
- $plugin = Inflector::camelize($prefix);
+
+ // Try to match against loaded plugins first
+ if (isset($pluginPrefixMap[$prefix])) {
+ $plugin = $pluginPrefixMap[$prefix];
+ } else {
+ // Fall back to camelizing the prefix
+ $plugin = Inflector::camelize($prefix);
+ }
$legacyTables[$table] = $plugin;
}
}
@@ -195,6 +218,26 @@ protected function findLegacyTables(Connection $connection): array
return $legacyTables;
}
+ /**
+ * Build a map of table prefixes to plugin names for all loaded plugins.
+ *
+ * This handles plugins with special characters like CakeDC/Users where
+ * the table prefix is cake_d_c_users but the plugin name is CakeDC/Users.
+ *
+ * @return array Map of table prefix => plugin name
+ */
+ protected function buildPluginPrefixMap(): array
+ {
+ $map = [];
+ foreach (Plugin::loaded() as $plugin) {
+ $prefix = Inflector::underscore($plugin);
+ $prefix = str_replace(['\\', '/', '.'], '_', $prefix);
+ $map[$prefix] = $plugin;
+ }
+
+ return $map;
+ }
+
/**
* Check if a table exists.
*
@@ -264,7 +307,7 @@ protected function migrateTable(
$rows = $query->execute()->fetchAll('assoc');
$count = count($rows);
- $io->out("Migrating {$count} record(s) from {$tableName} ({$pluginLabel})...");
+ $io->out(sprintf('Migrating %d record(s) from %s (%s)...', $count, $tableName, $pluginLabel));
if ($dryRun || $count === 0) {
return $count;
@@ -285,7 +328,7 @@ protected function migrateTable(
'breakpoint' => (int)($row['breakpoint'] ?? 0),
]);
$insertQuery->execute();
- } catch (QueryException $e) {
+ } catch (QueryException) {
$io->out('Already migrated ' . $row['migration_name'] . '.');
}
}
diff --git a/src/Config/Config.php b/src/Config/Config.php
index 03361772d..829f1358b 100644
--- a/src/Config/Config.php
+++ b/src/Config/Config.php
@@ -28,9 +28,6 @@ class Config implements ConfigInterface
*/
public const VERSION_ORDER_EXECUTION_TIME = 'execution';
- /**
- * @var array
- */
protected array $values = [];
/**
@@ -114,7 +111,7 @@ public function isVersionOrderCreationTime(): bool
{
$versionOrder = $this->getVersionOrder();
- return $versionOrder == self::VERSION_ORDER_CREATION_TIME;
+ return $versionOrder === self::VERSION_ORDER_CREATION_TIME;
}
/**
diff --git a/src/Config/ConfigInterface.php b/src/Config/ConfigInterface.php
index 57e4ffc7f..e4cb37cc9 100644
--- a/src/Config/ConfigInterface.php
+++ b/src/Config/ConfigInterface.php
@@ -18,6 +18,7 @@
interface ConfigInterface extends ArrayAccess
{
public const DEFAULT_MIGRATION_FOLDER = 'Migrations';
+
public const DEFAULT_SEED_FOLDER = 'Seeds';
/**
diff --git a/src/Db/Action/Action.php b/src/Db/Action/Action.php
index 4718b2682..73fddf752 100644
--- a/src/Db/Action/Action.php
+++ b/src/Db/Action/Action.php
@@ -12,9 +12,6 @@
abstract class Action
{
- /**
- * @var \Migrations\Db\Table\TableMetadata
- */
protected TableMetadata $table;
/**
diff --git a/src/Db/Action/AddCheckConstraint.php b/src/Db/Action/AddCheckConstraint.php
new file mode 100644
index 000000000..ca558208e
--- /dev/null
+++ b/src/Db/Action/AddCheckConstraint.php
@@ -0,0 +1,63 @@
+checkConstraint = $checkConstraint;
+ }
+
+ /**
+ * Creates a new AddCheckConstraint object after building the check constraint with
+ * the passed attributes
+ *
+ * @param \Migrations\Db\Table\TableMetadata $table The table object to add the check constraint to
+ * @param string $expression The check constraint expression (e.g., "age >= 18")
+ * @param array $options Options for the check constraint (e.g., 'name')
+ * @return self
+ */
+ public static function build(
+ TableMetadata $table,
+ string $expression,
+ array $options = [],
+ ): self {
+ $name = $options['name'] ?? '';
+
+ $checkConstraint = new CheckConstraint($name, $expression);
+
+ return new AddCheckConstraint($table, $checkConstraint);
+ }
+
+ /**
+ * Returns the check constraint to be added
+ *
+ * @return \Migrations\Db\Table\CheckConstraint
+ */
+ public function getCheckConstraint(): CheckConstraint
+ {
+ return $this->checkConstraint;
+ }
+}
diff --git a/src/Db/Action/AddColumn.php b/src/Db/Action/AddColumn.php
index c4948740a..5029d727b 100644
--- a/src/Db/Action/AddColumn.php
+++ b/src/Db/Action/AddColumn.php
@@ -15,8 +15,6 @@ class AddColumn extends Action
{
/**
* The column to add
- *
- * @var \Migrations\Db\Table\Column
*/
protected Column $column;
diff --git a/src/Db/Action/AddForeignKey.php b/src/Db/Action/AddForeignKey.php
index a1cfa00c5..f7a97cabd 100644
--- a/src/Db/Action/AddForeignKey.php
+++ b/src/Db/Action/AddForeignKey.php
@@ -15,8 +15,6 @@ class AddForeignKey extends Action
{
/**
* The foreign key to add
- *
- * @var \Migrations\Db\Table\ForeignKey
*/
protected ForeignKey $foreignKey;
diff --git a/src/Db/Action/AddIndex.php b/src/Db/Action/AddIndex.php
index f818ed4c6..85d38a207 100644
--- a/src/Db/Action/AddIndex.php
+++ b/src/Db/Action/AddIndex.php
@@ -15,8 +15,6 @@ class AddIndex extends Action
{
/**
* The index to add to the table
- *
- * @var \Migrations\Db\Table\Index
*/
protected Index $index;
diff --git a/src/Db/Action/AddPartition.php b/src/Db/Action/AddPartition.php
index aeb0a0bdc..a87d6feaf 100644
--- a/src/Db/Action/AddPartition.php
+++ b/src/Db/Action/AddPartition.php
@@ -16,9 +16,6 @@
*/
class AddPartition extends Action
{
- /**
- * @var \Migrations\Db\Table\PartitionDefinition
- */
protected PartitionDefinition $partition;
/**
diff --git a/src/Db/Action/ChangeColumn.php b/src/Db/Action/ChangeColumn.php
index 267e30aa9..a3770fb45 100644
--- a/src/Db/Action/ChangeColumn.php
+++ b/src/Db/Action/ChangeColumn.php
@@ -15,15 +15,11 @@ class ChangeColumn extends Action
{
/**
* The column definition
- *
- * @var \Migrations\Db\Table\Column
*/
protected Column $column;
/**
* The name of the column to be changed
- *
- * @var string
*/
protected string $columnName;
@@ -41,7 +37,7 @@ public function __construct(TableMetadata $table, string $columnName, Column $co
$this->column = $column;
// if the name was omitted use the existing column name
- if ($column->getName() === null || strlen((string)$column->getName()) === 0) {
+ if ($column->getName() === '') {
$column->setName($columnName);
}
}
diff --git a/src/Db/Action/ChangeComment.php b/src/Db/Action/ChangeComment.php
index 0fb773c90..d3b2d4f4b 100644
--- a/src/Db/Action/ChangeComment.php
+++ b/src/Db/Action/ChangeComment.php
@@ -14,8 +14,6 @@ class ChangeComment extends Action
{
/**
* The new comment for the table
- *
- * @var string|null
*/
protected ?string $newComment = null;
diff --git a/src/Db/Action/CreateTrigger.php b/src/Db/Action/CreateTrigger.php
new file mode 100644
index 000000000..fdb592f84
--- /dev/null
+++ b/src/Db/Action/CreateTrigger.php
@@ -0,0 +1,38 @@
+trigger;
+ }
+}
diff --git a/src/Db/Action/CreateView.php b/src/Db/Action/CreateView.php
new file mode 100644
index 000000000..51ed0d5d4
--- /dev/null
+++ b/src/Db/Action/CreateView.php
@@ -0,0 +1,38 @@
+view;
+ }
+}
diff --git a/src/Db/Action/DropCheckConstraint.php b/src/Db/Action/DropCheckConstraint.php
new file mode 100644
index 000000000..85355dd74
--- /dev/null
+++ b/src/Db/Action/DropCheckConstraint.php
@@ -0,0 +1,41 @@
+constraintName = $constraintName;
+ }
+
+ /**
+ * Returns the name of the check constraint to drop
+ *
+ * @return string
+ */
+ public function getConstraintName(): string
+ {
+ return $this->constraintName;
+ }
+}
diff --git a/src/Db/Action/DropForeignKey.php b/src/Db/Action/DropForeignKey.php
index c311f1bc8..5e9cfde9d 100644
--- a/src/Db/Action/DropForeignKey.php
+++ b/src/Db/Action/DropForeignKey.php
@@ -15,8 +15,6 @@ class DropForeignKey extends Action
{
/**
* The foreign key to remove
- *
- * @var \Migrations\Db\Table\ForeignKey
*/
protected ForeignKey $foreignKey;
diff --git a/src/Db/Action/DropIndex.php b/src/Db/Action/DropIndex.php
index 4c9bbf014..9299dd36d 100644
--- a/src/Db/Action/DropIndex.php
+++ b/src/Db/Action/DropIndex.php
@@ -15,8 +15,6 @@ class DropIndex extends Action
{
/**
* The index to drop
- *
- * @var \Migrations\Db\Table\Index
*/
protected Index $index;
diff --git a/src/Db/Action/DropPartition.php b/src/Db/Action/DropPartition.php
index 3647ff47d..e6b9c2cdb 100644
--- a/src/Db/Action/DropPartition.php
+++ b/src/Db/Action/DropPartition.php
@@ -15,9 +15,6 @@
*/
class DropPartition extends Action
{
- /**
- * @var string
- */
protected string $partitionName;
/**
diff --git a/src/Db/Action/DropTrigger.php b/src/Db/Action/DropTrigger.php
new file mode 100644
index 000000000..2077c9dd1
--- /dev/null
+++ b/src/Db/Action/DropTrigger.php
@@ -0,0 +1,37 @@
+triggerName;
+ }
+}
diff --git a/src/Db/Action/DropView.php b/src/Db/Action/DropView.php
new file mode 100644
index 000000000..eba4324e2
--- /dev/null
+++ b/src/Db/Action/DropView.php
@@ -0,0 +1,49 @@
+viewName;
+ }
+
+ /**
+ * Gets whether this is a materialized view
+ *
+ * @return bool
+ */
+ public function getMaterialized(): bool
+ {
+ return $this->materialized;
+ }
+}
diff --git a/src/Db/Action/RemoveColumn.php b/src/Db/Action/RemoveColumn.php
index 5aaa4a253..a73c5512e 100644
--- a/src/Db/Action/RemoveColumn.php
+++ b/src/Db/Action/RemoveColumn.php
@@ -15,8 +15,6 @@ class RemoveColumn extends Action
{
/**
* The column to be removed
- *
- * @var \Migrations\Db\Table\Column
*/
protected Column $column;
diff --git a/src/Db/Action/RenameColumn.php b/src/Db/Action/RenameColumn.php
index 2565d753d..1092a5f7d 100644
--- a/src/Db/Action/RenameColumn.php
+++ b/src/Db/Action/RenameColumn.php
@@ -15,15 +15,11 @@ class RenameColumn extends Action
{
/**
* The column to be renamed
- *
- * @var \Migrations\Db\Table\Column
*/
protected Column $column;
/**
* The new name for the column
- *
- * @var string
*/
protected string $newName;
diff --git a/src/Db/Action/RenameTable.php b/src/Db/Action/RenameTable.php
index 1f0b11e63..7b58ac653 100644
--- a/src/Db/Action/RenameTable.php
+++ b/src/Db/Action/RenameTable.php
@@ -14,8 +14,6 @@ class RenameTable extends Action
{
/**
* The new name for the table
- *
- * @var string
*/
protected string $newName;
diff --git a/src/Db/Action/SetPartitioning.php b/src/Db/Action/SetPartitioning.php
index 0e24e048a..b56e32c6b 100644
--- a/src/Db/Action/SetPartitioning.php
+++ b/src/Db/Action/SetPartitioning.php
@@ -16,9 +16,6 @@
*/
class SetPartitioning extends Action
{
- /**
- * @var \Migrations\Db\Table\Partition
- */
protected Partition $partition;
/**
diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php
index 556a60787..02034482c 100644
--- a/src/Db/Adapter/AbstractAdapter.php
+++ b/src/Db/Adapter/AbstractAdapter.php
@@ -23,6 +23,7 @@
use Exception;
use InvalidArgumentException;
use Migrations\Config\Config;
+use Migrations\Db\Action\AddCheckConstraint;
use Migrations\Db\Action\AddColumn;
use Migrations\Db\Action\AddForeignKey;
use Migrations\Db\Action\AddIndex;
@@ -30,10 +31,15 @@
use Migrations\Db\Action\ChangeColumn;
use Migrations\Db\Action\ChangeComment;
use Migrations\Db\Action\ChangePrimaryKey;
+use Migrations\Db\Action\CreateTrigger;
+use Migrations\Db\Action\CreateView;
+use Migrations\Db\Action\DropCheckConstraint;
use Migrations\Db\Action\DropForeignKey;
use Migrations\Db\Action\DropIndex;
use Migrations\Db\Action\DropPartition;
use Migrations\Db\Action\DropTable;
+use Migrations\Db\Action\DropTrigger;
+use Migrations\Db\Action\DropView;
use Migrations\Db\Action\RemoveColumn;
use Migrations\Db\Action\RenameColumn;
use Migrations\Db\Action\RenameTable;
@@ -48,8 +54,11 @@
use Migrations\Db\Table\Index;
use Migrations\Db\Table\Partition;
use Migrations\Db\Table\TableMetadata;
+use Migrations\Db\Table\Trigger;
+use Migrations\Db\Table\View;
use Migrations\MigrationInterface;
use Migrations\SeedInterface;
+use Migrations\Util\Util;
use PDOException;
use RuntimeException;
use function Cake\Core\deprecationWarning;
@@ -64,9 +73,6 @@ abstract class AbstractAdapter implements AdapterInterface, DirectActionInterfac
*/
protected array $options = [];
- /**
- * @var \Cake\Console\ConsoleIo
- */
protected ConsoleIo $io;
/**
@@ -74,24 +80,12 @@ abstract class AbstractAdapter implements AdapterInterface, DirectActionInterfac
*/
protected array $createdTables = [];
- /**
- * @var string
- */
protected string $schemaTableName = 'phinxlog';
- /**
- * @var string
- */
protected string $seedSchemaTableName = 'cake_seeds';
- /**
- * @var array
- */
protected array $dataDomain = [];
- /**
- * @var \Cake\Database\Connection|null
- */
protected ?Connection $connection = null;
/**
@@ -103,7 +97,7 @@ abstract class AbstractAdapter implements AdapterInterface, DirectActionInterfac
public function __construct(array $options, ?ConsoleIo $io = null)
{
$this->setOptions($options);
- if ($io !== null) {
+ if ($io instanceof ConsoleIo) {
$this->setIo($io);
}
}
@@ -197,10 +191,13 @@ protected function getSchemaDialect(): SchemaDialect
*/
public function getConnection(): Connection
{
- if ($this->connection === null) {
+ if (!$this->connection instanceof Connection) {
$this->connection = $this->getOption('connection');
$this->connect();
}
+ if (!$this->connection instanceof Connection) {
+ throw new RuntimeException('Unable to establish database connection. Ensure a connection is configured.');
+ }
return $this->connection;
}
@@ -294,9 +291,9 @@ protected function verboseLog(string $message): void
{
$io = $this->getIo();
if (
- $io === null || (
+ !$io instanceof ConsoleIo || (
!$this->isDryRunEnabled() &&
- $io->level() != ConsoleIo::VERBOSE
+ $io->level() !== ConsoleIo::VERBOSE
)
) {
return;
@@ -465,7 +462,7 @@ public function isDryRunEnabled(): bool
protected function addCreatedTable(string $tableName): void
{
$tableName = $this->quoteTableName($tableName);
- if (substr_compare($tableName, 'phinxlog', -strlen('phinxlog')) !== 0) {
+ if (!str_ends_with($tableName, 'phinxlog')) {
$this->createdTables[] = $tableName;
}
}
@@ -694,22 +691,18 @@ protected function generateInsertSql(
$upsertClause = $this->getUpsertClause($mode, $updateColumns, $conflictColumns);
if ($this->isDryRunEnabled()) {
- $sql .= ' VALUES (' . implode(', ', array_map($this->quoteValue(...), $row)) . ')' . $upsertClause . ';';
-
- return $sql;
- } else {
- $values = [];
- foreach ($row as $value) {
- $placeholder = '?';
- if ($value instanceof Literal) {
- $placeholder = (string)$value;
- }
- $values[] = $placeholder;
+ return $sql . (' VALUES (' . implode(', ', array_map($this->quoteValue(...), $row)) . ')' . $upsertClause . ';');
+ }
+ $values = [];
+ foreach ($row as $value) {
+ $placeholder = '?';
+ if ($value instanceof Literal) {
+ $placeholder = (string)$value;
}
- $sql .= ' VALUES (' . implode(',', $values) . ')' . $upsertClause;
-
- return $sql;
+ $values[] = $placeholder;
}
+
+ return $sql . (' VALUES (' . implode(',', $values) . ')' . $upsertClause);
}
/**
@@ -748,7 +741,7 @@ protected function getUpsertClause(?InsertMode $mode, ?array $updateColumns, ?ar
if ($conflictColumns !== null && $conflictColumns !== []) {
trigger_error(
'The $conflictColumns parameter is ignored by MySQL. ' .
- 'MySQL\'s ON DUPLICATE KEY UPDATE applies to all unique constraints on the table.',
+ "MySQL's ON DUPLICATE KEY UPDATE applies to all unique constraints on the table.",
E_USER_WARNING,
);
}
@@ -849,7 +842,7 @@ public function bulkinsert(
* Generates the SQL for a bulk insert.
*
* @param \Migrations\Db\Table\TableMetadata $table The table to insert into
- * @param array $rows The rows to insert
+ * @param array> $rows The rows to insert
* @param \Migrations\Db\InsertMode|null $mode Insert mode
* @param array|null $updateColumns Columns to update on upsert conflict
* @param array|null $conflictColumns Columns that define uniqueness for upsert (unused in MySQL)
@@ -867,38 +860,35 @@ protected function generateBulkInsertSql(
$this->getInsertPrefix($mode),
$this->quoteTableName($table->getName()),
);
- $current = current($rows);
- $keys = array_keys($current);
+ $current = (array)current($rows);
+ $keys = array_map(strval(...), array_keys($current));
$sql .= '(' . implode(', ', array_map($this->quoteColumnName(...), $keys)) . ') VALUES ';
$upsertClause = $this->getUpsertClause($mode, $updateColumns, $conflictColumns);
if ($this->isDryRunEnabled()) {
- $values = array_map(function ($row) {
+ $values = array_map(function (array $row): string {
return '(' . implode(', ', array_map($this->quoteValue(...), $row)) . ')';
}, $rows);
- $sql .= implode(', ', $values) . $upsertClause . ';';
- return $sql;
- } else {
- $queries = [];
- foreach ($rows as $row) {
- $values = [];
- foreach ($row as $v) {
- $placeholder = '?';
- if ($v instanceof Literal) {
- $placeholder = (string)$v;
- }
- $values[] = $placeholder;
+ return $sql . (implode(', ', $values) . $upsertClause . ';');
+ }
+ $queries = [];
+ foreach ($rows as $row) {
+ $values = [];
+ foreach ($row as $v) {
+ $placeholder = '?';
+ if ($v instanceof Literal) {
+ $placeholder = (string)$v;
}
- $query = '(' . implode(', ', $values) . ')';
- $queries[] = $query;
+ $values[] = $placeholder;
}
- $sql .= implode(',', $queries) . $upsertClause;
-
- return $sql;
+ $query = '(' . implode(', ', $values) . ')';
+ $queries[] = $query;
}
+
+ return $sql . implode(',', $queries) . $upsertClause;
}
/**
@@ -969,7 +959,7 @@ protected function isUsingUnifiedTable(): bool
// Autodetect mode (config is null or not set)
// Check if the main legacy phinxlog table exists
- if ($this->connection !== null) {
+ if ($this->connection instanceof Connection) {
$dialect = $this->connection->getDriver()->schemaDialect();
if ($dialect->hasTable('phinxlog')) {
return false;
@@ -987,16 +977,11 @@ protected function isUsingUnifiedTable(): bool
*/
public function getVersionLog(): array
{
- switch ($this->options['version_order']) {
- case Config::VERSION_ORDER_CREATION_TIME:
- $orderBy = ['version' => 'ASC'];
- break;
- case Config::VERSION_ORDER_EXECUTION_TIME:
- $orderBy = ['start_time' => 'ASC', 'version' => 'ASC'];
- break;
- default:
- throw new RuntimeException('Invalid version_order configuration option');
- }
+ $orderBy = match ($this->options['version_order']) {
+ Config::VERSION_ORDER_CREATION_TIME => ['version' => 'ASC'],
+ Config::VERSION_ORDER_EXECUTION_TIME => ['start_time' => 'ASC', 'version' => 'ASC'],
+ default => throw new RuntimeException('Invalid version_order configuration option'),
+ };
$query = $this->migrationsTable()->getVersions($orderBy);
// This will throw an exception if doing a --dry-run without any migrations as phinxlog
@@ -1112,17 +1097,7 @@ public function getSeedLog(): array
*/
public function seedExecuted(SeedInterface $seed, string $executedTime): AdapterInterface
{
- $plugin = null;
- $className = get_class($seed);
-
- if (str_contains($className, '\\')) {
- $parts = explode('\\', $className);
- $appNamespace = Configure::read('App.namespace', 'App');
- if (count($parts) > 1 && $parts[0] !== $appNamespace) {
- $plugin = $parts[0];
- }
- }
-
+ $plugin = $this->resolveSeedPlugin($seed);
$seedName = substr($seed->getName(), 0, 100);
$query = $this->getInsertBuilder();
@@ -1143,31 +1118,51 @@ public function seedExecuted(SeedInterface $seed, string $executedTime): Adapter
*/
public function removeSeedFromLog(SeedInterface $seed): AdapterInterface
{
- $plugin = null;
- $className = get_class($seed);
+ $plugin = $this->resolveSeedPlugin($seed);
+ $seedName = $seed->getName();
- if (str_contains($className, '\\')) {
- $parts = explode('\\', $className);
- $appNamespace = Configure::read('App.namespace', 'App');
- if (count($parts) > 1 && $parts[0] !== $appNamespace) {
- $plugin = $parts[0];
- }
+ $conditions = ['seed_name' => $seedName];
+ if ($plugin !== null) {
+ // Also remove entries logged before plugin attribution was fixed.
+ $conditions['OR'] = [
+ 'plugin' => $plugin,
+ 'plugin IS' => null,
+ ];
+ } else {
+ $conditions['plugin IS'] = null;
}
- $seedName = $seed->getName();
-
$query = $this->getDeleteBuilder();
$query->delete()
->from($this->getSeedSchemaTableName())
- ->where([
- 'seed_name' => $seedName,
- 'plugin IS' => $plugin,
- ]);
+ ->where($conditions);
$this->executeQuery($query);
return $this;
}
+ /**
+ * Resolve the plugin a seed belongs to.
+ *
+ * Seed classes are not namespaced, so the plugin cannot be derived from the class
+ * name. The plugin of the current run is used instead, matching how migrations are
+ * tracked.
+ *
+ * @param \Migrations\SeedInterface $seed The seed to resolve the plugin for.
+ * @return string|null The plugin name or null for application seeds.
+ */
+ protected function resolveSeedPlugin(SeedInterface $seed): ?string
+ {
+ $plugin = Util::getSeedPlugin($seed);
+ if ($plugin !== null) {
+ return $plugin;
+ }
+
+ $option = $this->getOption('plugin');
+
+ return $option ? (string)$option : null;
+ }
+
/**
* {@inheritDoc}
*
@@ -1278,7 +1273,7 @@ protected function getDefaultValueDefinition(mixed $default, ?string $columnType
$default = $this->castToBool((bool)$default);
}
- return isset($default) ? " DEFAULT $default" : '';
+ return isset($default) ? ' DEFAULT ' . $default : '';
}
/**
@@ -1291,7 +1286,7 @@ protected function getDefaultValueDefinition(mixed $default, ?string $columnType
protected function executeAlterSteps(string $tableName, AlterInstructions $instructions): void
{
$alter = sprintf('ALTER TABLE %s %%s', $this->quoteTableName($tableName));
- $instructions->execute($alter, [$this, 'execute']);
+ $instructions->execute($alter, $this->execute(...));
}
/**
@@ -1635,6 +1630,41 @@ public function changeComment(TableMetadata $table, $newComment): void
*/
abstract protected function getChangeCommentInstructions(TableMetadata $table, ?string $newComment): AlterInstructions;
+ /**
+ * Returns the instructions to create a view.
+ *
+ * @param \Migrations\Db\Table\View $view The view to create
+ * @return \Migrations\Db\AlterInstructions
+ */
+ abstract protected function getCreateViewInstructions(View $view): AlterInstructions;
+
+ /**
+ * Returns the instructions to drop a view.
+ *
+ * @param string $viewName The name of the view to drop
+ * @param bool $materialized Whether this is a materialized view (PostgreSQL only)
+ * @return \Migrations\Db\AlterInstructions
+ */
+ abstract protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions;
+
+ /**
+ * Returns the instructions to create a trigger.
+ *
+ * @param string $tableName The name of the table for the trigger
+ * @param \Migrations\Db\Table\Trigger $trigger The trigger to create
+ * @return \Migrations\Db\AlterInstructions
+ */
+ abstract protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions;
+
+ /**
+ * Returns the instructions to drop a trigger.
+ *
+ * @param string $tableName The name of the table for the trigger
+ * @param string $triggerName The name of the trigger to drop
+ * @return \Migrations\Db\AlterInstructions
+ */
+ abstract protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions;
+
/**
* {@inheritDoc}
*
@@ -1687,17 +1717,19 @@ public function executeActions(TableMetadata $table, array $actions): void
case $action instanceof DropForeignKey && $action->getForeignKey()->getName():
/** @var \Migrations\Db\Action\DropForeignKey $action */
+ $fkName = (string)$action->getForeignKey()->getName();
$instructions->merge($this->getDropForeignKeyInstructions(
$table->getName(),
- (string)$action->getForeignKey()->getName(),
+ $fkName,
));
break;
case $action instanceof DropIndex && $action->getIndex()->getName():
/** @var \Migrations\Db\Action\DropIndex $action */
+ $indexName = (string)$action->getIndex()->getName();
$instructions->merge($this->getDropIndexByNameInstructions(
$table->getName(),
- (string)$action->getIndex()->getName(),
+ $indexName,
));
break;
@@ -1710,7 +1742,6 @@ public function executeActions(TableMetadata $table, array $actions): void
break;
case $action instanceof DropTable:
- /** @var \Migrations\Db\Action\DropTable $action */
$instructions->merge($this->getDropTableInstructions(
$table->getName(),
));
@@ -1720,7 +1751,7 @@ public function executeActions(TableMetadata $table, array $actions): void
/** @var \Migrations\Db\Action\RemoveColumn $action */
$instructions->merge($this->getDropColumnInstructions(
$table->getName(),
- (string)$action->getColumn()->getName(),
+ $action->getColumn()->getName(),
));
break;
@@ -1728,7 +1759,7 @@ public function executeActions(TableMetadata $table, array $actions): void
/** @var \Migrations\Db\Action\RenameColumn $action */
$instructions->merge($this->getRenameColumnInstructions(
$table->getName(),
- (string)$action->getColumn()->getName(),
+ $action->getColumn()->getName(),
$action->getNewName(),
));
break;
@@ -1775,9 +1806,54 @@ public function executeActions(TableMetadata $table, array $actions): void
));
break;
+ case $action instanceof CreateView:
+ /** @var \Migrations\Db\Action\CreateView $action */
+ $instructions->merge($this->getCreateViewInstructions($action->getView()));
+ break;
+
+ case $action instanceof DropView:
+ /** @var \Migrations\Db\Action\DropView $action */
+ $instructions->merge($this->getDropViewInstructions(
+ $action->getViewName(),
+ $action->getMaterialized(),
+ ));
+ break;
+
+ case $action instanceof CreateTrigger:
+ /** @var \Migrations\Db\Action\CreateTrigger $action */
+ $instructions->merge($this->getCreateTriggerInstructions(
+ $table->getName(),
+ $action->getTrigger(),
+ ));
+ break;
+
+ case $action instanceof DropTrigger:
+ /** @var \Migrations\Db\Action\DropTrigger $action */
+ $instructions->merge($this->getDropTriggerInstructions(
+ $table->getName(),
+ $action->getTriggerName(),
+ ));
+ break;
+
+ case $action instanceof AddCheckConstraint:
+ /** @var \Migrations\Db\Action\AddCheckConstraint $action */
+ $instructions->merge($this->getAddCheckConstraintInstructions(
+ $table,
+ $action->getCheckConstraint(),
+ ));
+ break;
+
+ case $action instanceof DropCheckConstraint:
+ /** @var \Migrations\Db\Action\DropCheckConstraint $action */
+ $instructions->merge($this->getDropCheckConstraintInstructions(
+ $table->getName(),
+ $action->getConstraintName(),
+ ));
+ break;
+
default:
throw new InvalidArgumentException(
- sprintf("Don't know how to execute action `%s`", get_class($action)),
+ sprintf("Don't know how to execute action `%s`", $action::class),
);
}
}
diff --git a/src/Db/Adapter/AdapterFactory.php b/src/Db/Adapter/AdapterFactory.php
index 4c7691f2b..43c5e573e 100644
--- a/src/Db/Adapter/AdapterFactory.php
+++ b/src/Db/Adapter/AdapterFactory.php
@@ -23,6 +23,13 @@ class AdapterFactory
*/
protected static ?AdapterFactory $instance = null;
+ /**
+ * Constructor.
+ */
+ final public function __construct()
+ {
+ }
+
/**
* Get the factory singleton instance.
*
@@ -30,7 +37,7 @@ class AdapterFactory
*/
public static function instance(): static
{
- if (!static::$instance) {
+ if (!static::$instance instanceof AdapterFactory) {
static::$instance = new static();
}
@@ -72,7 +79,7 @@ public static function instance(): static
public function registerAdapter(string $name, Closure|string $class)
{
if (
- !($class instanceof Closure || is_subclass_of($class, AdapterInterface::class))
+ !$class instanceof Closure && !is_subclass_of($class, AdapterInterface::class)
) {
throw new RuntimeException(sprintf(
'Adapter class `%s` must implement `Migrations\\Db\\Adapter\\AdapterInterface`',
diff --git a/src/Db/Adapter/AdapterInterface.php b/src/Db/Adapter/AdapterInterface.php
index 74d43406a..48cad38c3 100644
--- a/src/Db/Adapter/AdapterInterface.php
+++ b/src/Db/Adapter/AdapterInterface.php
@@ -29,29 +29,50 @@
interface AdapterInterface
{
public const TYPE_STRING = TableSchemaInterface::TYPE_STRING;
+
public const TYPE_CHAR = TableSchemaInterface::TYPE_CHAR;
+
public const TYPE_TEXT = TableSchemaInterface::TYPE_TEXT;
+
public const TYPE_INTEGER = TableSchemaInterface::TYPE_INTEGER;
+
public const TYPE_TINYINTEGER = TableSchemaInterface::TYPE_TINYINTEGER;
+
public const TYPE_SMALLINTEGER = TableSchemaInterface::TYPE_SMALLINTEGER;
+
public const TYPE_BIGINTEGER = TableSchemaInterface::TYPE_BIGINTEGER;
+
public const TYPE_FLOAT = TableSchemaInterface::TYPE_FLOAT;
+
public const TYPE_DECIMAL = TableSchemaInterface::TYPE_DECIMAL;
+
public const TYPE_DATETIME = TableSchemaInterface::TYPE_DATETIME;
+
public const TYPE_TIMESTAMP = TableSchemaInterface::TYPE_TIMESTAMP;
+
public const TYPE_TIME = TableSchemaInterface::TYPE_TIME;
+
public const TYPE_DATE = TableSchemaInterface::TYPE_DATE;
+
public const TYPE_BINARY = TableSchemaInterface::TYPE_BINARY;
+
public const TYPE_BINARY_UUID = TableSchemaInterface::TYPE_BINARY_UUID;
+
public const TYPE_BOOLEAN = TableSchemaInterface::TYPE_BOOLEAN;
+
public const TYPE_JSON = TableSchemaInterface::TYPE_JSON;
+
public const TYPE_UUID = TableSchemaInterface::TYPE_UUID;
+
public const TYPE_NATIVE_UUID = TableSchemaInterface::TYPE_NATIVE_UUID;
// Geospatial database types
public const TYPE_GEOMETRY = TableSchemaInterface::TYPE_GEOMETRY;
+
public const TYPE_POINT = TableSchemaInterface::TYPE_POINT;
+
public const TYPE_LINESTRING = TableSchemaInterface::TYPE_LINESTRING;
+
public const TYPE_POLYGON = TableSchemaInterface::TYPE_POLYGON;
public const TYPES_GEOSPATIAL = [
@@ -64,88 +85,114 @@ interface AdapterInterface
// only for mysql so far
public const TYPE_YEAR = TableSchemaInterface::TYPE_YEAR;
+ public const TYPE_BIT = TableSchemaInterface::TYPE_BIT;
+
// only for postgresql so far
public const TYPE_CIDR = TableSchemaInterface::TYPE_CIDR;
+
public const TYPE_INET = TableSchemaInterface::TYPE_INET;
+
public const TYPE_MACADDR = TableSchemaInterface::TYPE_MACADDR;
+
+ public const TYPE_CITEXT = TableSchemaInterface::TYPE_CITEXT;
+
public const TYPE_INTERVAL = TableSchemaInterface::TYPE_INTERVAL;
/**
* @deprecated 5.0.0 Use TYPE_STRING instead.
*/
public const PHINX_TYPE_STRING = self::TYPE_STRING;
+
/**
* @deprecated 5.0.0 Use TYPE_CHAR instead.
*/
public const PHINX_TYPE_CHAR = self::TYPE_CHAR;
+
/**
* @deprecated 5.0.0 Use TYPE_TEXT instead.
*/
public const PHINX_TYPE_TEXT = self::TYPE_TEXT;
+
/**
* @deprecated 5.0.0 Use TYPE_INTEGER instead.
*/
public const PHINX_TYPE_INTEGER = self::TYPE_INTEGER;
+
/**
* @deprecated 5.0.0 Use TYPE_TINYINTEGER instead.
*/
public const PHINX_TYPE_TINY_INTEGER = self::TYPE_TINYINTEGER;
+
/**
* @deprecated 5.0.0 Use TYPE_SMALLINTEGER instead.
*/
public const PHINX_TYPE_SMALL_INTEGER = self::TYPE_SMALLINTEGER;
+
/**
* @deprecated 5.0.0 Use TYPE_BIGINTEGER instead.
*/
public const PHINX_TYPE_BIG_INTEGER = self::TYPE_BIGINTEGER;
+
/**
* @deprecated 5.0.0 Use TYPE_FLOAT instead.
*/
public const PHINX_TYPE_FLOAT = self::TYPE_FLOAT;
+
/**
* @deprecated 5.0.0 Use TYPE_DECIMAL instead.
*/
public const PHINX_TYPE_DECIMAL = self::TYPE_DECIMAL;
+
/**
* @deprecated 5.0.0 Use TYPE_DATETIME instead.
*/
public const PHINX_TYPE_DATETIME = self::TYPE_DATETIME;
+
/**
* @deprecated 5.0.0 Use TYPE_TIMESTAMP instead.
*/
public const PHINX_TYPE_TIMESTAMP = self::TYPE_TIMESTAMP;
+
/**
* @deprecated 5.0.0 Use TYPE_TIME instead.
*/
public const PHINX_TYPE_TIME = self::TYPE_TIME;
+
/**
* @deprecated 5.0.0 Use TYPE_DATE instead.
*/
public const PHINX_TYPE_DATE = self::TYPE_DATE;
+
/**
* @deprecated 5.0.0 Use TYPE_BINARY instead.
*/
public const PHINX_TYPE_BINARY = self::TYPE_BINARY;
+
/**
* @deprecated 5.0.0 Use TYPE_BINARY_UUID instead.
*/
public const PHINX_TYPE_BINARYUUID = self::TYPE_BINARY_UUID;
+
/**
* @deprecated 5.0.0 Use TYPE_BOOLEAN instead.
*/
public const PHINX_TYPE_BOOLEAN = self::TYPE_BOOLEAN;
+
/**
* @deprecated 5.0.0 Use TYPE_JSON instead.
*/
public const PHINX_TYPE_JSON = self::TYPE_JSON;
+
/**
* @deprecated 5.0.0 Use TableSchemaInterface::TYPE_JSON instead.
*/
public const PHINX_TYPE_JSONB = 'jsonb';
+
/**
* @deprecated 5.0.0 Use TYPE_UUID instead.
*/
public const PHINX_TYPE_UUID = self::TYPE_UUID;
+
/**
* @deprecated 5.0.0 Use TYPE_NATIVE_UUID instead.
*/
@@ -155,14 +202,17 @@ interface AdapterInterface
* @deprecated 5.0.0 Use TYPE_GEOMETRY instead.
*/
public const PHINX_TYPE_GEOMETRY = self::TYPE_GEOMETRY;
+
/**
* @deprecated 5.0.0 Use TYPE_POINT instead.
*/
public const PHINX_TYPE_POINT = self::TYPE_POINT;
+
/**
* @deprecated 5.0.0 Use TYPE_LINESTRING instead.
*/
public const PHINX_TYPE_LINESTRING = self::TYPE_LINESTRING;
+
/**
* @deprecated 5.0.0 Use TYPE_POLYGON instead.
*/
@@ -187,14 +237,17 @@ interface AdapterInterface
* @deprecated 5.0.0 Use TYPE_CIDR instead.
*/
public const PHINX_TYPE_CIDR = self::TYPE_CIDR;
+
/**
* @deprecated 5.0.0 Use TYPE_INET instead.
*/
public const PHINX_TYPE_INET = self::TYPE_INET;
+
/**
* @deprecated 5.0.0 Use TYPE_MACADDR instead.
*/
public const PHINX_TYPE_MACADDR = self::TYPE_MACADDR;
+
/**
* @deprecated 5.0.0 Use TYPE_INTERVAL instead.
*/
@@ -552,6 +605,26 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
*/
public function truncateTable(string $tableName): void;
+ /**
+ * Disable foreign key constraint checking.
+ *
+ * This is useful when dropping tables or performing bulk operations
+ * that would otherwise fail due to foreign key constraints.
+ *
+ * @return void
+ */
+ public function disableForeignKeyConstraints(): void;
+
+ /**
+ * Enable foreign key constraint checking.
+ *
+ * This should be called after disableForeignKeyConstraints() to
+ * restore normal constraint checking behavior.
+ *
+ * @return void
+ */
+ public function enableForeignKeyConstraints(): void;
+
/**
* Returns table columns
*
diff --git a/src/Db/Adapter/AdapterWrapper.php b/src/Db/Adapter/AdapterWrapper.php
index 597e9926d..64c0a4814 100644
--- a/src/Db/Adapter/AdapterWrapper.php
+++ b/src/Db/Adapter/AdapterWrapper.php
@@ -30,9 +30,6 @@
*/
abstract class AdapterWrapper implements WrapperInterface
{
- /**
- * @var \Migrations\Db\Adapter\AdapterInterface
- */
protected AdapterInterface $adapter;
/**
@@ -590,4 +587,20 @@ public function getSchemaTableName(): string
{
return $this->getAdapter()->getSchemaTableName();
}
+
+ /**
+ * @inheritDoc
+ */
+ public function disableForeignKeyConstraints(): void
+ {
+ $this->getAdapter()->disableForeignKeyConstraints();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enableForeignKeyConstraints(): void
+ {
+ $this->getAdapter()->enableForeignKeyConstraints();
+ }
}
diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php
index c07cfe6f7..b3f0002bf 100644
--- a/src/Db/Adapter/MysqlAdapter.php
+++ b/src/Db/Adapter/MysqlAdapter.php
@@ -23,12 +23,19 @@
use Migrations\Db\Table\Partition;
use Migrations\Db\Table\PartitionDefinition;
use Migrations\Db\Table\TableMetadata;
+use Migrations\Db\Table\Trigger;
+use Migrations\Db\Table\View;
/**
* MySQL Adapter.
*/
class MysqlAdapter extends AbstractAdapter
{
+ /**
+ * Maximum length for identifiers (table names, column names, constraint names, etc.)
+ */
+ protected const IDENTIFIER_MAX_LENGTH = 64;
+
/**
* @var string[]
*/
@@ -48,26 +55,32 @@ class MysqlAdapter extends AbstractAdapter
* @deprecated 5.0.0 Enum column support will be removed in a future release.
*/
public const PHINX_TYPE_ENUM = 'enum';
+
/**
* @deprecated 5.0.0 Set column support will be removed in a future release.
*/
public const PHINX_TYPE_SET = 'set';
+
/**
* @deprecated 5.0.0 Use binary type with with no limit instead.
*/
public const PHINX_TYPE_BLOB = 'blob';
+
/**
* @deprecated 5.0.0 Use binary type with with limit BLOB_SMALL instead.
*/
public const PHINX_TYPE_TINYBLOB = 'tinyblob';
+
/**
* @deprecated 5.0.0 Use binary type with with limit BLOB_MEDIUM instead.
*/
public const PHINX_TYPE_MEDIUMBLOB = 'mediumblob';
+
/**
* @deprecated 5.0.0 Use binary type with with limit BLOB_LONG instead.
*/
public const PHINX_TYPE_LONGBLOB = 'longblob';
+
/**
* @deprecated 5.0.0 Use binary type instead.
*/
@@ -79,29 +92,45 @@ class MysqlAdapter extends AbstractAdapter
// as its actual value is its regular value is larger than PHP_INT_MAX. We do this
// to keep consistent the type hints for Column::$limit being integers.
public const TEXT_TINY = 255;
+
public const TEXT_SMALL = 255; /* deprecated, alias of TEXT_TINY */
/** @deprecated Use length of null instead **/
public const TEXT_REGULAR = 65535;
+
public const TEXT_MEDIUM = 16777215;
+
public const TEXT_LONG = 2147483647;
// According to https://dev.mysql.com/doc/refman/5.0/en/blob.html BLOB sizes are the same as TEXT
public const BLOB_TINY = TableSchema::LENGTH_TINY;
- public const BLOB_SMALL = TableSchema::LENGTH_TINY; /* deprecated, alias of BLOB_TINY */
+
+ public const BLOB_SMALL = TableSchema::LENGTH_TINY;
+
+ /* deprecated, alias of BLOB_TINY */
public const BLOB_REGULAR = 65535;
+
public const BLOB_MEDIUM = TableSchema::LENGTH_MEDIUM;
+
public const BLOB_LONG = TableSchema::LENGTH_LONG;
public const INT_TINY = 255;
+
public const INT_SMALL = 65535;
+
public const INT_MEDIUM = 16777215;
+
public const INT_REGULAR = 1073741823;
+
public const INT_BIG = 2147483647;
public const INT_DISPLAY_TINY = 4;
+
public const INT_DISPLAY_SMALL = 6;
+
public const INT_DISPLAY_MEDIUM = 8;
+
public const INT_DISPLAY_REGULAR = 11;
+
public const INT_DISPLAY_BIG = 20;
public const BIT = 64;
@@ -149,8 +178,11 @@ class MysqlAdapter extends AbstractAdapter
* @see https://mariadb.com/kb/en/alter-table/#algorithm
*/
public const ALGORITHM_DEFAULT = 'DEFAULT';
+
public const ALGORITHM_INSTANT = 'INSTANT';
+
public const ALGORITHM_INPLACE = 'INPLACE';
+
public const ALGORITHM_COPY = 'COPY';
/**
@@ -177,8 +209,11 @@ class MysqlAdapter extends AbstractAdapter
* @see https://mariadb.com/kb/en/alter-table/#lock
*/
public const LOCK_DEFAULT = 'DEFAULT';
+
public const LOCK_NONE = 'NONE';
+
public const LOCK_SHARED = 'SHARED';
+
public const LOCK_EXCLUSIVE = 'EXCLUSIVE';
/**
@@ -213,7 +248,7 @@ public function hasTable(string $tableName): bool
return true;
}
- if (strpos($tableName, '.') !== false) {
+ if (str_contains($tableName, '.')) {
[$schema, $table] = explode('.', $tableName);
$exists = $this->hasTableWithSchema($schema, $table);
// Only break here on success, because it is possible for table names to contain a dot.
@@ -265,11 +300,11 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
);
// Add the default primary key
- if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
+ if (!isset($options['id']) || $options['id'] === true) {
$options['id'] = 'id';
}
- if (isset($options['id']) && is_string($options['id'])) {
+ if (is_string($options['id'])) {
$useUnsigned = (bool)Configure::read('Migrations.unsigned_primary_keys');
// Handle id => "field_name" to support AUTO_INCREMENT
$column = new Column();
@@ -301,7 +336,7 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
// process table collation
if (isset($options['collation'])) {
- $charset = explode('_', $options['collation']);
+ $charset = explode('_', (string)$options['collation']);
$optionsStr .= sprintf(' CHARACTER SET %s', $charset[0]);
$optionsStr .= sprintf(' COLLATE %s', $options['collation']);
}
@@ -349,7 +384,7 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
// add partitioning
$partition = $table->getPartition();
- if ($partition !== null) {
+ if ($partition instanceof Partition) {
$sql .= ' ' . $this->getPartitionSqlDefinition($partition);
}
@@ -369,8 +404,10 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
protected function mapColumnData(array $data): array
{
if ($data['type'] == self::TYPE_TEXT && $data['length'] !== null) {
+ // Accept both migrations TEXT_LONG and CakePHP LENGTH_LONG for backward compatibility
+ // with migrations generated before the fix (LENGTH_TINY/MEDIUM are already equal to TEXT_TINY/MEDIUM)
$data['length'] = match ($data['length']) {
- self::TEXT_LONG => TableSchema::LENGTH_LONG,
+ self::TEXT_LONG, TableSchema::LENGTH_LONG => TableSchema::LENGTH_LONG,
self::TEXT_MEDIUM => TableSchema::LENGTH_MEDIUM,
self::TEXT_REGULAR => null,
self::TEXT_TINY => TableSchema::LENGTH_TINY,
@@ -443,7 +480,7 @@ protected function columnDefinitionSql(SchemaDialect $dialect, Column $column):
$sql = $this->quoteColumnName($columnData['name']) . ' ' . $columnData['type'];
$values = $column->getValues();
if ($values) {
- $sql .= '(' . implode(', ', array_map(function ($value) {
+ $sql .= '(' . implode(', ', array_map(function ($value): string {
// Special case NULL to trigger errors as it isn't allowed
// in enum values.
return $value === null ? 'NULL' : $this->quoteString($value);
@@ -505,7 +542,7 @@ protected function getChangeCommentInstructions(TableMetadata $table, ?string $n
$instructions = new AlterInstructions();
// passing 'null' is to remove table comment
- $newComment = $newComment ?? '';
+ $newComment ??= '';
$sql = sprintf(' COMMENT=%s ', $this->quoteString($newComment));
$instructions->addAlter($sql);
@@ -551,6 +588,22 @@ public function truncateTable(string $tableName): void
$this->execute($sql);
}
+ /**
+ * @inheritDoc
+ */
+ public function disableForeignKeyConstraints(): void
+ {
+ $this->execute('SET FOREIGN_KEY_CHECKS = 0');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enableForeignKeyConstraints(): void
+ {
+ $this->execute('SET FOREIGN_KEY_CHECKS = 1');
+ }
+
/**
* Convert from cakephp/database conventions to migrations\column
*
@@ -625,7 +678,7 @@ public function getColumns(string $tableName): array
$rawTypes = [];
$rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName)));
foreach ($rows as $row) {
- $rawTypes[$row['Field']] = strtolower($row['Type']);
+ $rawTypes[$row['Field']] = strtolower((string)$row['Type']);
}
$columns = [];
@@ -651,6 +704,9 @@ public function getColumns(string $tableName): array
if ($record['onUpdate'] ?? false) {
$column->setUpdate($record['onUpdate']);
}
+ if ($record['fixed'] ?? false) {
+ $column->setFixed(true);
+ }
$columns[] = $column;
}
@@ -724,7 +780,7 @@ protected function getRenameColumnInstructions(string $tableName, string $column
$targetColumn = null;
foreach ($columns as $column) {
- if (strcasecmp($column->getName(), $columnName) === 0) {
+ if (strcasecmp((string)$column->getName(), $columnName) === 0) {
$targetColumn = $column;
break;
}
@@ -741,7 +797,7 @@ protected function getRenameColumnInstructions(string $tableName, string $column
$rows = $this->fetchAll(sprintf('SHOW FULL COLUMNS FROM %s', $this->quoteTableName($tableName)));
foreach ($rows as $row) {
- if (strcasecmp($row['Field'], $columnName) === 0) {
+ if (strcasecmp((string)$row['Field'], $columnName) === 0) {
$null = $row['Null'] === 'NO' ? 'NOT NULL' : 'NULL';
$comment = isset($row['Comment']) && $row['Comment'] !== ''
? ' COMMENT ' . $this->getConnection()->getDriver()->schemaValue($row['Comment'])
@@ -749,8 +805,8 @@ protected function getRenameColumnInstructions(string $tableName, string $column
// create the extra string by also filtering out the DEFAULT_GENERATED option (MySQL 8 fix)
$extras = array_filter(
- explode(' ', strtoupper($row['Extra'])),
- static function ($value) {
+ explode(' ', strtoupper((string)$row['Extra'])),
+ static function (string $value): bool {
return $value !== 'DEFAULT_GENERATED';
},
);
@@ -823,9 +879,8 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa
protected function getIndexes(string $tableName): array
{
$dialect = $this->getSchemaDialect();
- $indexes = $dialect->describeIndexes($tableName);
- return $indexes;
+ return $dialect->describeIndexes($tableName);
}
/**
@@ -844,6 +899,20 @@ protected function getAddIndexInstructions(TableMetadata $table, Index $index):
$this->getIndexSqlDefinition($index),
);
+ // FULLTEXT indexes use post-steps (raw SQL) which executeAlterSteps
+ // does not append algorithm/lock to, so we inline the clause here.
+ // Setting on instructions as well ensures validation still runs.
+ if ($index->getAlgorithm() !== null || $index->getLock() !== null) {
+ if ($index->getAlgorithm() !== null) {
+ $alter .= ', ALGORITHM=' . strtoupper($index->getAlgorithm());
+ $instructions->setAlgorithm($index->getAlgorithm());
+ }
+ if ($index->getLock() !== null) {
+ $alter .= ', LOCK=' . strtoupper($index->getLock());
+ $instructions->setLock($index->getLock());
+ }
+ }
+
$instructions->addPostStep($alter);
} else {
$alter = sprintf(
@@ -852,6 +921,13 @@ protected function getAddIndexInstructions(TableMetadata $table, Index $index):
);
$instructions->addAlter($alter);
+
+ if ($index->getAlgorithm() !== null) {
+ $instructions->setAlgorithm($index->getAlgorithm());
+ }
+ if ($index->getLock() !== null) {
+ $instructions->setLock($index->getLock());
+ }
}
return $instructions;
@@ -869,7 +945,7 @@ protected function getDropIndexByColumnsInstructions(string $tableName, $columns
}
$indexes = $this->getIndexes($tableName);
- $columns = array_map('strtolower', $columns);
+ $columns = array_map(strtolower(...), $columns);
foreach ($indexes as $index) {
if ($columns == $index['columns']) {
@@ -923,11 +999,10 @@ public function hasPrimaryKey(string $tableName, string|array $columns, ?string
if ($constraint) {
return $primaryKey['name'] === $constraint;
- } else {
- $missingColumns = array_diff((array)$columns, (array)$primaryKey['columns']);
-
- return empty($missingColumns);
}
+ $missingColumns = array_diff((array)$columns, (array)$primaryKey['columns']);
+
+ return $missingColumns === [];
}
/**
@@ -962,9 +1037,8 @@ public function getPrimaryKey(string $tableName): array
protected function getForeignKeys(string $tableName): array
{
$dialect = $this->getSchemaDialect();
- $foreignKeys = $dialect->describeForeignKeys($tableName);
- return $foreignKeys;
+ return $dialect->describeForeignKeys($tableName);
}
/**
@@ -974,7 +1048,7 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey
{
$alter = sprintf(
'ADD %s',
- $this->getForeignKeySqlDefinition($foreignKey),
+ $this->getForeignKeySqlDefinition($foreignKey, $table->getName()),
);
return new AlterInstructions([$alter]);
@@ -987,7 +1061,7 @@ protected function getDropForeignKeyInstructions(string $tableName, string $cons
{
$alter = sprintf(
'DROP FOREIGN KEY %s',
- $constraint,
+ $this->quoteColumnName($constraint),
);
return new AlterInstructions([$alter]);
@@ -1002,12 +1076,12 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr
{
$instructions = new AlterInstructions();
- $columns = array_map('mb_strtolower', $columns);
+ $columns = array_map(mb_strtolower(...), $columns);
$matches = [];
$foreignKeys = $this->getForeignKeys($tableName);
foreach ($foreignKeys as $key) {
- if (array_map('mb_strtolower', $key['columns']) === $columns) {
+ if (array_map(mb_strtolower(...), $key['columns']) === $columns) {
$matches[] = $key['name'];
}
}
@@ -1047,7 +1121,7 @@ protected function getCheckConstraints(string $tableName): array
protected function getAddCheckConstraintInstructions(TableMetadata $table, CheckConstraint $checkConstraint): AlterInstructions
{
$constraintName = $checkConstraint->getName();
- if ($constraintName === null) {
+ if ($constraintName === null || $constraintName === '') {
// Auto-generate constraint name if not provided
$constraintName = $table->getName() . '_chk_' . substr(md5($checkConstraint->getExpression()), 0, 8);
}
@@ -1155,7 +1229,7 @@ protected function getIndexSqlDefinition(Index $index): string
$columnNames = (array)$index->getColumns();
$order = $index->getOrder() ?? [];
- $columnNames = array_map(function ($columnName) use ($order) {
+ $columnNames = array_map(function (string $columnName) use ($order): string {
$ret = $this->quoteColumnName($columnName);
if (isset($order[$columnName])) {
$ret .= ' ' . $order[$columnName];
@@ -1189,14 +1263,13 @@ protected function getIndexSqlDefinition(Index $index): string
* Gets the MySQL Foreign Key Definition for an ForeignKey object.
*
* @param \Migrations\Db\Table\ForeignKey $foreignKey Foreign key
+ * @param string $tableName Table name for auto-generating constraint name
* @return string
*/
- protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
+ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
{
- $def = '';
- if ($foreignKey->getName()) {
- $def .= ' CONSTRAINT ' . $this->quoteColumnName((string)$foreignKey->getName());
- }
+ $constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
+ $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
$columnNames = [];
foreach ($foreignKey->getColumns() as $column) {
$columnNames[] = $this->quoteColumnName($column);
@@ -1206,7 +1279,11 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
foreach ($foreignKey->getReferencedColumns() as $column) {
$refColumnNames[] = $this->quoteColumnName($column);
}
- $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()) . ' (' . implode(',', $refColumnNames) . ')';
+ $referencedTable = $foreignKey->getReferencedTable();
+ if ($referencedTable === null) {
+ throw new InvalidArgumentException('Foreign key must have a referenced table.');
+ }
+ $def .= ' REFERENCES ' . $this->quoteTableName($referencedTable) . ' (' . implode(',', $refColumnNames) . ')';
$onDelete = $foreignKey->getOnDelete();
if ($onDelete) {
$def .= ' ON DELETE ' . $onDelete;
@@ -1219,6 +1296,35 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
return $def;
}
+ /**
+ * Generate a unique foreign key constraint name.
+ *
+ * @param string $tableName Table name
+ * @param array $columns Column names
+ * @return string
+ */
+ protected function getUniqueForeignKeyName(string $tableName, array $columns): string
+ {
+ $baseName = $tableName . '_' . implode('_', $columns);
+ $maxLength = static::IDENTIFIER_MAX_LENGTH - 3;
+ if (strlen($baseName) > $maxLength) {
+ $baseName = substr($baseName, 0, $maxLength);
+ }
+ $existingKeys = $this->getForeignKeys($tableName);
+ $existingNames = array_column($existingKeys, 'name');
+
+ if (!in_array($baseName, $existingNames, true)) {
+ return $baseName;
+ }
+
+ $counter = 2;
+ while (in_array($baseName . '_' . $counter, $existingNames, true)) {
+ $counter++;
+ }
+
+ return $baseName . '_' . $counter;
+ }
+
/**
* Returns MySQL column types (inherited and MySQL specified).
*
@@ -1269,7 +1375,7 @@ protected function getPartitionSqlDefinition(Partition $partition): string
if ($columns instanceof Literal) {
$columnsSql = (string)$columns;
} else {
- $columnsSql = implode(', ', array_map(fn($col) => $this->quoteColumnName($col), $columns));
+ $columnsSql = implode(', ', array_map($this->quoteColumnName(...), $columns));
}
$sql = sprintf('PARTITION BY %s (%s)', $type, $columnsSql);
@@ -1319,14 +1425,14 @@ protected function getPartitionDefinitionSql(string $type, PartitionDefinition $
if ($value === 'MAXVALUE' || $value === Partition::TYPE_RANGE . '_MAXVALUE') {
$sql .= 'MAXVALUE';
} elseif (is_array($value)) {
- $sql .= '(' . implode(', ', array_map(fn($v) => $this->quotePartitionValue($v), $value)) . ')';
+ $sql .= '(' . implode(', ', array_map($this->quotePartitionValue(...), $value)) . ')';
} else {
$sql .= '(' . $this->quotePartitionValue($value) . ')';
}
} elseif ($isListType) {
$sql .= ' VALUES IN (';
if (is_array($value)) {
- $sql .= implode(', ', array_map(fn($v) => $this->quotePartitionValue($v), $value));
+ $sql .= implode(', ', array_map($this->quotePartitionValue(...), $value));
} else {
$sql .= $this->quotePartitionValue($value);
}
@@ -1387,7 +1493,7 @@ protected function getSetPartitioningInstructions(TableMetadata $table, Partitio
*/
protected function getAddPartitionsInstructions(TableMetadata $table, array $partitions): AlterInstructions
{
- if (empty($partitions)) {
+ if ($partitions === []) {
return new AlterInstructions();
}
@@ -1413,11 +1519,11 @@ protected function getAddPartitionsInstructions(TableMetadata $table, array $par
*/
protected function getDropPartitionsInstructions(string $tableName, array $partitionNames): AlterInstructions
{
- if (empty($partitionNames)) {
+ if ($partitionNames === []) {
return new AlterInstructions();
}
- $quotedNames = array_map(fn($name) => $this->quoteColumnName($name), $partitionNames);
+ $quotedNames = array_map($this->quoteColumnName(...), $partitionNames);
$sql = 'DROP PARTITION ' . implode(', ', $quotedNames);
return new AlterInstructions([$sql]);
@@ -1448,7 +1554,7 @@ protected function getAddPartitionSql(PartitionDefinition $partition): string
} elseif (is_array($value)) {
// Likely LIST
$sql .= ' VALUES IN (';
- $sql .= implode(', ', array_map(fn($v) => $this->quotePartitionValue($v), $value));
+ $sql .= implode(', ', array_map($this->quotePartitionValue(...), $value));
$sql .= ')';
}
@@ -1468,7 +1574,7 @@ protected function getAddPartitionSql(PartitionDefinition $partition): string
protected function hasNativeUuid(): bool
{
// Prevent infinite connect() loop when MysqlAdapter is used as a stub.
- if ($this->connection === null || !$this->getOption('connection')) {
+ if (!$this->connection instanceof Connection || !$this->getOption('connection')) {
return false;
}
$connection = $this->getConnection();
@@ -1485,7 +1591,7 @@ protected function hasNativeUuid(): bool
protected function isMariaDb(): bool
{
// Prevent infinite connect() loop when MysqlAdapter is used as a stub.
- if ($this->connection === null || !$this->getOption('connection')) {
+ if (!$this->connection instanceof Connection || !$this->getOption('connection')) {
return false;
}
$connection = $this->getConnection();
@@ -1579,4 +1685,65 @@ protected function executeAlterSteps(string $tableName, AlterInstructions $instr
$this->execute($instruction);
}
}
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getCreateViewInstructions(View $view): AlterInstructions
+ {
+ $sql = sprintf(
+ 'CREATE %sVIEW %s AS %s',
+ $view->getReplace() ? 'OR REPLACE ' : '',
+ $this->quoteTableName($view->getName()),
+ $view->getDefinition(),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions
+ {
+ $sql = sprintf(
+ 'DROP VIEW IF EXISTS %s',
+ $this->quoteTableName($viewName),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions
+ {
+ $events = is_array($trigger->getEvent()) ? $trigger->getEvent() : [$trigger->getEvent()];
+ $eventStr = implode(' OR ', $events);
+
+ $sql = sprintf(
+ 'CREATE TRIGGER %s %s %s ON %s FOR EACH ROW %s',
+ $this->quoteColumnName($trigger->getName()),
+ $trigger->getTiming(),
+ $eventStr,
+ $this->quoteTableName($tableName),
+ $trigger->getDefinition(),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions
+ {
+ $sql = sprintf(
+ 'DROP TRIGGER IF EXISTS %s',
+ $this->quoteColumnName($triggerName),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
}
diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php
index 878f81ee9..84c3f1803 100644
--- a/src/Db/Adapter/PostgresAdapter.php
+++ b/src/Db/Adapter/PostgresAdapter.php
@@ -23,12 +23,21 @@
use Migrations\Db\Table\Partition;
use Migrations\Db\Table\PartitionDefinition;
use Migrations\Db\Table\TableMetadata;
+use Migrations\Db\Table\Trigger;
+use Migrations\Db\Table\View;
use RuntimeException;
class PostgresAdapter extends AbstractAdapter
{
+ /**
+ * Maximum length for identifiers (table names, column names, constraint names, etc.)
+ */
+ protected const IDENTIFIER_MAX_LENGTH = 63;
+
public const GENERATED_ALWAYS = 'ALWAYS';
+
public const GENERATED_BY_DEFAULT = 'BY DEFAULT';
+
/**
* Allow insert when a column was created with the GENERATED ALWAYS clause.
* This is required for seeding the database.
@@ -44,12 +53,24 @@ class PostgresAdapter extends AbstractAdapter
self::TYPE_CIDR,
self::TYPE_INET,
self::TYPE_MACADDR,
+ self::TYPE_CITEXT,
self::TYPE_INTERVAL,
self::TYPE_BINARY_UUID,
self::TYPE_NATIVE_UUID,
];
- private const GIN_INDEX_TYPE = 'gin';
+ /**
+ * PostgreSQL index access methods that require USING clause.
+ *
+ * @var array
+ */
+ private const ACCESS_METHOD_TYPES = [
+ Index::GIN,
+ Index::GIST,
+ Index::SPGIST,
+ Index::BRIN,
+ Index::HASH,
+ ];
/**
* Columns with comments
@@ -60,8 +81,6 @@ class PostgresAdapter extends AbstractAdapter
/**
* Use identity columns if available (Postgres >= 10.0)
- *
- * @var bool
*/
protected bool $useIdentity;
@@ -169,7 +188,7 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
if (is_string($options['primary_key'])) { // handle primary_key => 'id'
$sql .= $this->quoteColumnName($options['primary_key']);
} elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
- $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
+ $sql .= implode(',', array_map($this->quoteColumnName(...), $options['primary_key']));
}
$sql .= ')';
} else {
@@ -180,24 +199,20 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
// add partitioning clause
$partition = $table->getPartition();
- if ($partition !== null) {
+ if ($partition instanceof Partition) {
$sql .= ' ' . $this->getPartitionSqlDefinition($partition);
}
$queries[] = $sql;
// process column comments
- if ($this->columnsWithComments) {
- foreach ($this->columnsWithComments as $column) {
- $queries[] = $this->getColumnCommentSqlDefinition($column, $table->getName());
- }
+ foreach ($this->columnsWithComments as $column) {
+ $queries[] = $this->getColumnCommentSqlDefinition($column, $table->getName());
}
// set the indexes
- if ($indexes) {
- foreach ($indexes as $index) {
- $queries[] = $this->getIndexSqlDefinition($index, $table->getName());
- }
+ foreach ($indexes as $index) {
+ $queries[] = $this->getIndexSqlDefinition($index, $table->getName());
}
// process table comments
@@ -210,7 +225,7 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
}
// create partition tables for PostgreSQL declarative partitioning
- if ($partition !== null) {
+ if ($partition instanceof Partition) {
foreach ($partition->getDefinitions() as $definition) {
$queries[] = $this->getPartitionTableSql($table->getName(), $partition, $definition);
}
@@ -275,7 +290,7 @@ protected function getChangePrimaryKeyInstructions(TableMetadata $table, array|s
if (is_string($newColumns)) { // handle primary_key => 'id'
$sql .= $this->quoteColumnName($newColumns);
} else { // handle primary_key => array('tag_id', 'resource_id')
- $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns));
+ $sql .= implode(',', array_map($this->quoteColumnName(...), $newColumns));
}
$sql .= ')';
$instructions->addAlter($sql);
@@ -326,7 +341,7 @@ protected function getRenameTableInstructions(string $tableName, string $newTabl
protected function getDropTableInstructions(string $tableName): AlterInstructions
{
$this->removeCreatedTable($tableName);
- $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
+ $sql = sprintf('DROP TABLE %s CASCADE', $this->quoteTableName($tableName));
return new AlterInstructions([], [$sql]);
}
@@ -344,6 +359,24 @@ public function truncateTable(string $tableName): void
$this->execute($sql);
}
+ /**
+ * @inheritDoc
+ */
+ public function disableForeignKeyConstraints(): void
+ {
+ // PostgreSQL uses CASCADE on DROP TABLE instead of disabling FK checks.
+ // This method is a no-op for PostgreSQL since dropTable already uses CASCADE.
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enableForeignKeyConstraints(): void
+ {
+ // PostgreSQL uses CASCADE on DROP TABLE instead of disabling FK checks.
+ // This method is a no-op for PostgreSQL.
+ }
+
/**
* @inheritDoc
*/
@@ -421,7 +454,7 @@ protected function getRenameColumnInstructions(
];
$result = $this->query($sql, $params)->fetch('assoc');
if (!$result || !(bool)$result['column_exists']) {
- throw new InvalidArgumentException("The specified column does not exist: $columnName");
+ throw new InvalidArgumentException('The specified column does not exist: ' . $columnName);
}
$instructions = new AlterInstructions();
@@ -455,9 +488,9 @@ protected function getChangeColumnInstructions(
$columnSql = $dialect->columnDefinitionSql($this->mapColumnData($newColumn->toArray()));
// Remove the column name from $columnSql
- $columnType = preg_replace('/^"?(?:[^"]+)"?\s+/', '', $columnSql);
+ $columnType = (string)preg_replace('/^"?(?:[^"]+)"?\s+/', '', $columnSql);
// Remove generated clause
- $columnType = preg_replace('/GENERATED (?:ALWAYS|BY DEFAULT) AS IDENTITY/', '', $columnType);
+ $columnType = (string)preg_replace('/GENERATED (?:ALWAYS|BY DEFAULT) AS IDENTITY/', '', $columnType);
$sql = sprintf(
'ALTER COLUMN %s TYPE %s',
@@ -470,23 +503,23 @@ protected function getChangeColumnInstructions(
$quotedColumnName,
);
}
- if (in_array($newColumn->getType(), ['uuid', 'nativeuuid', 'binaryuuid'])) {
+ if (in_array($newColumn->getType(), ['uuid', 'nativeuuid', 'binaryuuid'], true)) {
$sql .= sprintf(
' USING (%s::uuid)',
$quotedColumnName,
);
}
- if (in_array($newColumn->getType(), ['json'])) {
+ if ($newColumn->getType() === 'json') {
$sql .= sprintf(
' USING (%s::jsonb)',
$quotedColumnName,
);
}
// NULL and DEFAULT cannot be set while changing column type
- $sql = preg_replace('/ NOT NULL/', '', $sql);
- $sql = preg_replace('/ DEFAULT NULL/', '', $sql);
+ $sql = (string)preg_replace('/ NOT NULL/', '', $sql);
+ $sql = (string)preg_replace('/ DEFAULT NULL/', '', $sql);
// If it is set, DEFAULT is the last definition
- $sql = preg_replace('/DEFAULT .*/', '', $sql);
+ $sql = (string)preg_replace('/DEFAULT .*/', '', $sql);
if ($newColumn->getType() === 'boolean') {
$sql .= sprintf(
' USING (CASE WHEN %s IS NULL THEN NULL WHEN %s::int=0 THEN FALSE ELSE TRUE END)',
@@ -497,7 +530,7 @@ protected function getChangeColumnInstructions(
$instructions->addAlter($sql);
$column = $this->getColumn($tableName, $columnName);
- assert($column !== null, 'Column must exist');
+ assert($column instanceof Column, 'Column must exist');
if ($this->useIdentity) {
// process identity
@@ -505,11 +538,12 @@ protected function getChangeColumnInstructions(
'ALTER COLUMN %s',
$quotedColumnName,
);
- if ($newColumn->isIdentity() && $newColumn->getGenerated() !== null) {
+ $generated = $newColumn->getGenerated();
+ if ($newColumn->isIdentity() && $generated !== null) {
if ($column->isIdentity()) {
- $sql .= sprintf(' SET GENERATED %s', (string)$newColumn->getGenerated());
+ $sql .= sprintf(' SET GENERATED %s', $generated);
} else {
- $sql .= sprintf(' ADD GENERATED %s AS IDENTITY', (string)$newColumn->getGenerated());
+ $sql .= sprintf(' ADD GENERATED %s AS IDENTITY', $generated);
}
} else {
$sql .= ' DROP IDENTITY IF EXISTS';
@@ -535,7 +569,7 @@ protected function getChangeColumnInstructions(
$instructions->addAlter(sprintf(
'ALTER COLUMN %s SET %s',
$quotedColumnName,
- $this->getDefaultValueDefinition($newColumn->getDefault(), (string)$newColumn->getType()),
+ $this->getDefaultValueDefinition($newColumn->getDefault(), $newColumn->getType()),
));
} elseif (!$newColumn->getIdentity()) {
//drop default
@@ -546,12 +580,13 @@ protected function getChangeColumnInstructions(
}
// rename column
- if ($columnName !== $newColumn->getName()) {
+ $newColumnName = $newColumn->getName();
+ if ($columnName !== $newColumnName) {
$instructions->addPostStep(sprintf(
'ALTER TABLE %s RENAME COLUMN %s TO %s',
$this->quoteTableName($tableName),
$quotedColumnName,
- $this->quoteColumnName((string)$newColumn->getName()),
+ $this->quoteColumnName($newColumnName),
));
}
@@ -602,9 +637,8 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa
protected function getIndexes(string $tableName): array
{
$dialect = $this->getSchemaDialect();
- $indexes = $dialect->describeIndexes($tableName);
- return $indexes;
+ return $dialect->describeIndexes($tableName);
}
/**
@@ -675,14 +709,13 @@ public function hasPrimaryKey(string $tableName, $columns, ?string $constraint =
if ($constraint) {
return $primaryKey['constraint'] === $constraint;
- } else {
- if (is_string($columns)) {
- $columns = [$columns]; // str to array
- }
- $missingColumns = array_diff($columns, $primaryKey['columns']);
-
- return empty($missingColumns);
}
+ if (is_string($columns)) {
+ $columns = [$columns]; // str to array
+ }
+ $missingColumns = array_diff($columns, $primaryKey['columns']);
+
+ return $missingColumns === [];
}
/**
@@ -713,9 +746,8 @@ public function getPrimaryKey(string $tableName): array
protected function getForeignKeys(string $tableName): array
{
$dialect = $this->getSchemaDialect();
- $foreignKeys = $dialect->describeForeignKeys($tableName);
- return $foreignKeys;
+ return $dialect->describeForeignKeys($tableName);
}
/**
@@ -784,9 +816,8 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr
protected function getCheckConstraints(string $tableName): array
{
$dialect = $this->getSchemaDialect();
- $constraints = $dialect->describeCheckConstraints($tableName);
- return $constraints;
+ return $dialect->describeCheckConstraints($tableName);
}
/**
@@ -795,7 +826,7 @@ protected function getCheckConstraints(string $tableName): array
protected function getAddCheckConstraintInstructions(TableMetadata $table, CheckConstraint $checkConstraint): AlterInstructions
{
$constraintName = $checkConstraint->getName();
- if ($constraintName === null) {
+ if ($constraintName === null || $constraintName === '') {
// Auto-generate constraint name if not provided
$parts = $this->getSchemaName($table->getName());
$constraintName = $parts['table'] . '_chk_' . substr(md5($checkConstraint->getExpression()), 0, 8);
@@ -873,6 +904,7 @@ public function dropDatabase($name): void
*/
protected function getColumnCommentSqlDefinition(Column $column, string $tableName): string
{
+ $columnName = $column->getName();
$comment = (string)$column->getComment();
// passing 'null' is to remove column comment
$comment = strcasecmp($comment, 'NULL') !== 0
@@ -882,7 +914,7 @@ protected function getColumnCommentSqlDefinition(Column $column, string $tableNa
return sprintf(
'COMMENT ON COLUMN %s.%s IS %s;',
$this->quoteTableName($tableName),
- $this->quoteColumnName((string)$column->getName()),
+ $this->quoteColumnName($columnName),
$comment,
);
}
@@ -900,13 +932,21 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
$columnNames = (array)$index->getColumns();
$indexName = $index->getName();
- if ($indexName === null || strlen($indexName) === 0) {
+ if ($indexName === null || $indexName === '') {
$indexName = sprintf('%s_%s', $parts['table'], implode('_', $columnNames));
}
$order = $index->getOrder() ?? [];
- $columnNames = array_map(function ($columnName) use ($order) {
+ $opclass = $index->getOpclass() ?? [];
+ $columnNames = array_map(function (string $columnName) use ($order, $opclass): string {
$ret = '"' . $columnName . '"';
+
+ // Add operator class if specified (e.g., gist_trgm_ops)
+ if (isset($opclass[$columnName])) {
+ $ret .= ' ' . $opclass[$columnName];
+ }
+
+ // Add ordering if specified (e.g., ASC NULLS FIRST)
if (isset($order[$columnName])) {
$ret .= ' ' . $order[$columnName];
}
@@ -917,23 +957,25 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
$include = $index->getInclude();
$includedColumns = $include ? sprintf(' INCLUDE ("%s")', implode('","', $include)) : '';
- $createIndexSentence = 'CREATE %sINDEX%s %s ON %s ';
- if ($index->getType() === self::GIN_INDEX_TYPE) {
- $createIndexSentence .= ' USING ' . $index->getType() . '(%s) %s;';
- } else {
- $createIndexSentence .= '(%s)%s%s;';
+ // Build USING clause for access method types (gin, gist, spgist, brin, hash)
+ $indexType = $index->getType();
+ $usingClause = '';
+ if (in_array($indexType, self::ACCESS_METHOD_TYPES, true)) {
+ $usingClause = ' USING ' . $indexType;
}
- $where = (string)$index->getWhere();
- if ($where) {
- $where = ' WHERE ' . $where;
+ $where = '';
+ $whereClause = $index->getWhere();
+ if ($whereClause) {
+ $where = ' WHERE ' . $whereClause;
}
return sprintf(
- $createIndexSentence,
- $index->getType() === Index::UNIQUE ? 'UNIQUE ' : '',
+ 'CREATE %sINDEX%s %s ON %s%s (%s)%s%s;',
+ $indexType === Index::UNIQUE ? 'UNIQUE ' : '',
$index->getConcurrently() ? ' CONCURRENTLY' : '',
- $this->quoteColumnName((string)$indexName),
+ $this->quoteColumnName($indexName),
$this->quoteTableName($tableName),
+ $usingClause,
implode(',', $columnNames),
$includedColumns,
$where,
@@ -949,29 +991,59 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
*/
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
{
- $parts = $this->getSchemaName($tableName);
-
- $constraintName = $foreignKey->getName() ?: (
- $parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey'
- );
+ $constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
$columnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getColumns()));
$refColumnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getReferencedColumns()));
+ $referencedTable = $foreignKey->getReferencedTable();
+ if ($referencedTable === null) {
+ throw new InvalidArgumentException('Foreign key must have a referenced table.');
+ }
$def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName) .
' FOREIGN KEY (' . $columnList . ')' .
- ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()) . ' (' . $refColumnList . ')';
+ ' REFERENCES ' . $this->quoteTableName($referencedTable) . ' (' . $refColumnList . ')';
if ($foreignKey->getOnDelete()) {
- $def .= " ON DELETE {$foreignKey->getOnDelete()}";
+ $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
}
if ($foreignKey->getOnUpdate()) {
- $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
+ $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
}
if ($foreignKey->getDeferrableMode()) {
- $def .= " {$foreignKey->getDeferrableMode()}";
+ $def .= ' ' . $foreignKey->getDeferrableMode();
}
return $def;
}
+ /**
+ * Generate a unique foreign key constraint name.
+ *
+ * @param string $tableName Table name
+ * @param array $columns Column names
+ * @return string
+ */
+ protected function getUniqueForeignKeyName(string $tableName, array $columns): string
+ {
+ $parts = $this->getSchemaName($tableName);
+ $baseName = $parts['table'] . '_' . implode('_', $columns) . '_fkey';
+ $maxLength = static::IDENTIFIER_MAX_LENGTH - 3;
+ if (strlen($baseName) > $maxLength) {
+ $baseName = substr($baseName, 0, $maxLength);
+ }
+ $existingKeys = $this->getForeignKeys($tableName);
+ $existingNames = array_column($existingKeys, 'name');
+
+ if (!in_array($baseName, $existingNames, true)) {
+ return $baseName;
+ }
+
+ $counter = 2;
+ while (in_array($baseName . '_' . $counter, $existingNames, true)) {
+ $counter++;
+ }
+
+ return $baseName . '_' . $counter;
+ }
+
/**
* @inheritDoc
*/
@@ -1107,7 +1179,11 @@ public function getColumnTypes(): array
public function isValidColumnType(Column $column): bool
{
// If not a standard column type, maybe it is array type?
- return parent::isValidColumnType($column) || $this->isArrayType($column->getType());
+ if (parent::isValidColumnType($column)) {
+ return true;
+ }
+
+ return $this->isArrayType($column->getType());
}
/**
@@ -1135,7 +1211,7 @@ protected function getSchemaName(string $tableName): array
{
$schema = $this->getGlobalSchemaName();
$table = $tableName;
- if (strpos($tableName, '.') !== false) {
+ if (str_contains($tableName, '.')) {
[$schema, $table] = explode('.', $tableName);
}
@@ -1260,7 +1336,7 @@ public function bulkinsert(
$conflictClause = $this->getConflictClause($mode, $updateColumns, $conflictColumns);
if ($this->isDryRunEnabled()) {
- $values = array_map(function ($row) {
+ $values = array_map(function ($row): string {
return '(' . implode(', ', array_map($this->quoteValue(...), $row)) . ')';
}, $rows);
$sql .= implode(', ', $values) . $conflictClause . ';';
@@ -1327,7 +1403,7 @@ protected function getConflictClause(
}
$quotedConflictColumns = array_map($this->quoteColumnName(...), $conflictColumns);
$updates = [];
- foreach ($updateColumns as $column) {
+ foreach ($updateColumns ?? [] as $column) {
$quotedColumn = $this->quoteColumnName($column);
$updates[] = $quotedColumn . ' = EXCLUDED.' . $quotedColumn;
}
@@ -1357,7 +1433,7 @@ protected function getPartitionSqlDefinition(Partition $partition): string
if ($columns instanceof Literal) {
$columnsSql = (string)$columns;
} else {
- $columnsSql = implode(', ', array_map(fn($col) => $this->quoteColumnName($col), $columns));
+ $columnsSql = implode(', ', array_map($this->quoteColumnName(...), $columns));
}
return sprintf('PARTITION BY %s (%s)', $type, $columnsSql);
@@ -1388,7 +1464,7 @@ protected function getPartitionTableSql(string $tableName, Partition $partition,
} elseif ($type === Partition::TYPE_LIST) {
$sql .= ' FOR VALUES IN (';
if (is_array($value)) {
- $sql .= implode(', ', array_map(fn($v) => $this->quotePartitionValue($v), $value));
+ $sql .= implode(', ', array_map($this->quotePartitionValue(...), $value));
} else {
$sql .= $this->quotePartitionValue($value);
}
@@ -1507,7 +1583,7 @@ private function getAddPartitionSql(TableMetadata $table, PartitionDefinition $p
} elseif (is_array($value)) {
// LIST partition
$sql .= ' FOR VALUES IN (';
- $sql .= implode(', ', array_map(fn($v) => $this->quotePartitionValue($v), $value));
+ $sql .= implode(', ', array_map($this->quotePartitionValue(...), $value));
$sql .= ')';
} else {
// Simple RANGE (upper bound only)
@@ -1571,4 +1647,79 @@ public function getAdapterType(): string
// compatibility.
return 'pgsql';
}
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getCreateViewInstructions(View $view): AlterInstructions
+ {
+ $viewType = $view->getMaterialized() ? 'MATERIALIZED VIEW' : 'VIEW';
+ $replace = '';
+
+ if ($view->getReplace() && !$view->getMaterialized()) {
+ $replace = 'OR REPLACE ';
+ }
+
+ $sql = sprintf(
+ 'CREATE %s%s %s AS %s',
+ $replace,
+ $viewType,
+ $this->quoteTableName($view->getName()),
+ $view->getDefinition(),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions
+ {
+ $viewType = $materialized ? 'MATERIALIZED VIEW' : 'VIEW';
+ $sql = sprintf(
+ 'DROP %s IF EXISTS %s',
+ $viewType,
+ $this->quoteTableName($viewName),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions
+ {
+ $events = is_array($trigger->getEvent()) ? $trigger->getEvent() : [$trigger->getEvent()];
+ $eventStr = implode(' OR ', $events);
+
+ $forEach = $trigger->getForEach() ? 'FOR EACH ROW' : 'FOR EACH STATEMENT';
+
+ $sql = sprintf(
+ 'CREATE TRIGGER %s %s %s ON %s %s EXECUTE FUNCTION %s',
+ $this->quoteColumnName($trigger->getName()),
+ $trigger->getTiming(),
+ $eventStr,
+ $this->quoteTableName($tableName),
+ $forEach,
+ $trigger->getDefinition(),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions
+ {
+ $sql = sprintf(
+ 'DROP TRIGGER IF EXISTS %s ON %s',
+ $this->quoteColumnName($triggerName),
+ $this->quoteTableName($tableName),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
}
diff --git a/src/Db/Adapter/RecordingAdapter.php b/src/Db/Adapter/RecordingAdapter.php
index aab4b41b3..618095040 100644
--- a/src/Db/Adapter/RecordingAdapter.php
+++ b/src/Db/Adapter/RecordingAdapter.php
@@ -89,7 +89,7 @@ public function getInvertedCommands(): Intent
case $command instanceof RenameColumn:
/** @var \Migrations\Db\Action\RenameColumn $command */
$column = clone $command->getColumn();
- $name = (string)$column->getName();
+ $name = $column->getName();
$column->setName($command->getNewName());
$inverted->addAction(new RenameColumn($command->getTable(), $column, $name));
break;
@@ -107,7 +107,7 @@ public function getInvertedCommands(): Intent
default:
throw new IrreversibleMigrationException(sprintf(
'Cannot reverse a "%s" command',
- get_class($command),
+ $command::class,
));
}
}
diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php
index 9145e0cb8..9736f38f4 100644
--- a/src/Db/Adapter/SqliteAdapter.php
+++ b/src/Db/Adapter/SqliteAdapter.php
@@ -21,6 +21,8 @@
use Migrations\Db\Table\ForeignKey;
use Migrations\Db\Table\Index;
use Migrations\Db\Table\TableMetadata;
+use Migrations\Db\Table\Trigger;
+use Migrations\Db\Table\View;
use PDOException;
use RuntimeException;
use const FILTER_VALIDATE_BOOLEAN;
@@ -59,9 +61,6 @@ class SqliteAdapter extends AbstractAdapter
self::TYPE_UUID => 'uuid_text',
];
- /**
- * @var string
- */
protected string $suffix = '.sqlite3';
/**
@@ -89,7 +88,7 @@ public function setOptions(array $options): AdapterInterface
}
//don't "fix" the file extension if it is blank, some people
//might want a SQLITE db file with absolutely no extension.
- if ($this->suffix !== '' && strpos($this->suffix, '.') !== 0) {
+ if ($this->suffix !== '' && !str_starts_with($this->suffix, '.')) {
$this->suffix = '.' . $this->suffix;
}
@@ -129,11 +128,7 @@ protected function possiblyQuotedIdentifierRegex(string $identifier, bool $space
$identifiers[] = "'" . ($hasSingleQuote ? str_replace("'", "''", $identifier) : $identifier) . "'";
if (!$hasTick && !$hasDoubleQuote && !$hasSingleQuote) {
- if ($spacedNoQuotes) {
- $identifiers[] = "\s+$identifier\s+";
- } else {
- $identifiers[] = $identifier;
- }
+ $identifiers[] = $spacedNoQuotes ? sprintf('\s+%s\s+', $identifier) : $identifier;
}
return '(' . implode('|', $identifiers) . ')';
@@ -179,7 +174,7 @@ protected function resolveTable(string $tableName): array
// the temp schema is always first to be searched
$schemata = ['temp'];
foreach ($rows as $row) {
- if (strtolower($row['name']) !== 'temp') {
+ if (strtolower((string)$row['name']) !== 'temp') {
$schemata[] = $row['name'];
}
}
@@ -190,9 +185,9 @@ protected function resolveTable(string $tableName): array
$defaultSchema = $info['schema'];
}
- $table = strtolower($info['table']);
+ $table = strtolower((string)$info['table']);
foreach ($schemata as $schema) {
- if (strtolower($schema) === 'temp') {
+ if (strtolower((string)$schema) === 'temp') {
$master = 'sqlite_temp_master';
} else {
$master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master');
@@ -200,14 +195,14 @@ protected function resolveTable(string $tableName): array
$rows = [];
try {
$result = $this->query(
- "SELECT name FROM {$master} WHERE type = 'table' AND lower(name) = ?",
+ sprintf("SELECT name FROM %s WHERE type = 'table' AND lower(name) = ?", $master),
[$table],
);
// null on error
if ($result !== null) {
$rows = $result->fetchAll('assoc');
}
- } catch (PDOException $e) {
+ } catch (PDOException) {
// an exception can occur if the schema part of the table refers to a database which is not attached
break;
}
@@ -215,7 +210,7 @@ protected function resolveTable(string $tableName): array
// this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined,
// and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII
foreach ($rows as $row) {
- if (strtolower($row['name']) === $table) {
+ if (strtolower((string)$row['name']) === $table) {
return ['schema' => $schema, 'table' => $row['name'], 'exists' => true];
}
}
@@ -292,7 +287,7 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
if (is_string($options['primary_key'])) { // handle primary_key => 'id'
$sql .= $this->quoteColumnName($options['primary_key']);
} elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
- $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
+ $sql .= implode(',', array_map($this->quoteColumnName(...), $options['primary_key']));
}
$sql .= ')';
} else {
@@ -406,6 +401,22 @@ public function truncateTable(string $tableName): void
}
}
+ /**
+ * @inheritDoc
+ */
+ public function disableForeignKeyConstraints(): void
+ {
+ $this->execute('PRAGMA foreign_keys = OFF');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enableForeignKeyConstraints(): void
+ {
+ $this->execute('PRAGMA foreign_keys = ON');
+ }
+
/**
* Parses a default-value expression to yield either a Literal representing
* a string value, a string representing an expression, or some other scalar
@@ -436,44 +447,50 @@ protected function parseDefaultValue(mixed $default, string $columnType): mixed
PCRE_PATTERN;
preg_match_all($pattern, $default, $matches);
// strip out any comment tokens
- $matches = array_map(function ($v) {
+ $matches = array_map(function (string $v): string {
return preg_match('/^(?:\/\*|--)/', $v) ? ' ' : $v;
}, $matches[0]);
// reconstitute the string, trimming whitespace as well as parentheses
$defaultClean = trim(implode('', $matches));
$defaultBare = rtrim(ltrim($defaultClean, $trimChars . '('), $trimChars . ')');
-
// match the string against one of several patterns
if ($columnType === TableSchemaInterface::TYPE_TEXT || $columnType === TableschemaInterface::TYPE_STRING) {
// string literal
return Literal::from($default);
- } elseif ($columnType === TableSchemaInterface::TYPE_BOOLEAN) {
+ }
+ if ($columnType === TableSchemaInterface::TYPE_BOOLEAN) {
// boolean literal
return (int)filter_var($defaultClean, FILTER_VALIDATE_BOOLEAN);
- } elseif (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $default)) {
+ }
+ if (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $default)) {
// magic date or time
return strtoupper($default);
- } elseif (preg_match('/^[+-]?\d+$/i', $default)) {
+ }
+ if (preg_match('/^[+-]?\d+$/i', $default)) {
$int = (int)$default;
// integer literal
if ($columnType === self::TYPE_BOOLEAN && ($int === 0 || $int === 1)) {
return (bool)$int;
- } else {
- return $int;
}
- } elseif (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $default)) {
+
+ return $int;
+ }
+ if (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $default)) {
// float literal
return (float)$default;
- } elseif (preg_match('/^0x[0-9a-f]+$/i', $default)) {
+ }
+ if (preg_match('/^0x[0-9a-f]+$/i', $default)) {
// hexadecimal literal
return hexdec(substr($default, 2));
- } elseif (preg_match('/^null$/i', $defaultBare)) {
+ }
+
+ // match the string against one of several patterns
+ if (preg_match('/^null$/i', $defaultBare)) {
// null literal
return null;
- } else {
- // any other expression: return the expression with parentheses, but without comments
- return Expression::from($default);
}
+ // any other expression: return the expression with parentheses, but without comments
+ return Expression::from($default);
}
/**
@@ -489,24 +506,21 @@ protected function resolveIdentity(string $tableName): ?string
$result = null;
// make sure the table has only one primary key column which is of type integer
foreach ($this->getColumnData($tableName) as $col) {
- $type = strtolower($col['type']);
+ $type = strtolower((string)$col['type']);
if ($col['pk'] > 1) {
// the table has a composite primary key
return null;
- } elseif ($col['pk'] == 0) {
+ }
+ if ($col['pk'] == 0) {
// the column is not a primary key column and is thus not relevant
continue;
- } elseif ($type !== 'integer') {
+ }
+ if ($type !== 'integer') {
// if the primary key's type is not exactly INTEGER, it cannot be a row ID alias
return null;
- } else {
- // the column is a candidate for a row ID alias
- $result = $col['name'];
}
- }
- // if there is no suitable PK column, stop now
- if ($result === null) {
- return null;
+ // the column is a candidate for a row ID alias
+ $result = $col['name'];
}
return $result;
@@ -560,7 +574,7 @@ protected function getAddColumnInstructions(TableMetadata $table, Column $column
$instructions = $this->beginAlterByCopyTable($tableName);
- $instructions->addPostStep(function ($state) use ($tableName, $column) {
+ $instructions->addPostStep(function (array $state) use ($tableName, $column): array {
// we use the final column to anchor our regex to insert the new column,
// as the alternative is unwinding all possible table constraints which
// gets messy quickly with CHECK constraints.
@@ -570,7 +584,7 @@ protected function getAddColumnInstructions(TableMetadata $table, Column $column
return $state;
}
$finalColumnName = end($columns)->getName();
- $sql = preg_replace(
+ $sql = (string)preg_replace(
sprintf(
"/(%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+)([,)])/",
$this->quoteColumnName((string)$finalColumnName),
@@ -587,7 +601,7 @@ protected function getAddColumnInstructions(TableMetadata $table, Column $column
return $state;
});
- $instructions->addPostStep(function ($state) use ($tableName) {
+ $instructions->addPostStep(function ($state) use ($tableName): array {
$newState = $this->calculateNewTableColumns($tableName, false, false);
return $newState + $state;
@@ -615,25 +629,23 @@ protected function getDeclaringSql(string $tableName): string
$columnsInfo = $this->getColumnData($tableName);
foreach ($columnsInfo as $column) {
- $columnName = preg_quote($column['name'], '#');
- $columnNamePattern = "\"$columnName\"|`$columnName`|\\[$columnName\\]|$columnName";
- $columnNamePattern = "#([\(,]+\\s*)($columnNamePattern)(\\s)#iU";
+ $columnName = preg_quote((string)$column['name'], '#');
+ $columnNamePattern = sprintf('"%s"|`%s`|\[%s\]|%s', $columnName, $columnName, $columnName, $columnName);
+ $columnNamePattern = sprintf('#([\(,]+\s*)(%s)(\s)#iU', $columnNamePattern);
- $sql = preg_replace_callback(
+ $sql = (string)preg_replace_callback(
$columnNamePattern,
- function ($matches) use ($column) {
+ function (array $matches) use ($column): string {
return $matches[1] . $this->quoteColumnName($column['name']) . $matches[3];
},
- $sql,
+ (string)$sql,
);
}
- $tableNamePattern = "\"$tableName\"|`$tableName`|\\[$tableName\\]|$tableName";
- $tableNamePattern = "#^(CREATE TABLE)\s*($tableNamePattern)\s*(\()#Ui";
-
- $sql = preg_replace($tableNamePattern, "$1 `$tableName` $3", $sql, 1);
+ $tableNamePattern = sprintf('"%s"|`%s`|\[%s\]|%s', $tableName, $tableName, $tableName, $tableName);
+ $tableNamePattern = sprintf('#^(CREATE TABLE)\s*(%s)\s*(\()#Ui', $tableNamePattern);
- return $sql;
+ return (string)preg_replace($tableNamePattern, sprintf('$1 `%s` $3', $tableName), $sql, 1);
}
/**
@@ -791,8 +803,8 @@ protected function updateIndicesForRenamedColumn(
$state['indices'][$key]['sql'] = preg_replace(
sprintf($pattern, preg_quote($oldColumnName, '/')),
- "\\1\\2$newColumnName\\4\\5\\6",
- $index['sql'],
+ sprintf('\1\2%s\4\5\6', $newColumnName),
+ (string)$index['sql'],
);
}
}
@@ -852,14 +864,14 @@ protected function validateForeignKeys(AlterInstructions $instructions, string $
foreach ($otherTables as $otherTable) {
$foreignKeyList = $this->getForeignKeys($otherTable['name']);
foreach ($foreignKeyList as $foreignKey) {
- if (strcasecmp($foreignKey['references'][0], $tableName) === 0) {
+ if (strcasecmp((string)$foreignKey['references'][0], $tableName) === 0) {
$tablesToCheck[] = $otherTable['name'];
break;
}
}
}
- $tablesToCheck = array_unique(array_map('strtolower', $tablesToCheck));
+ $tablesToCheck = array_unique(array_map(strtolower(...), $tablesToCheck));
foreach ($tablesToCheck as $tableToCheck) {
$schema = $this->getSchemaName($tableToCheck, true)['schema'];
@@ -915,7 +927,7 @@ protected function copyDataToNewTable(string $tableName, string $tmpTableName, a
*/
protected function copyAndDropTmpTable(AlterInstructions $instructions, string $tableName): AlterInstructions
{
- $instructions->addPostStep(function ($state) use ($tableName) {
+ $instructions->addPostStep(function (array $state) use ($tableName): array {
$this->copyDataToNewTable(
$state['tmpTableName'],
$tableName,
@@ -974,14 +986,14 @@ protected function calculateNewTableColumns(string $tableName, string|false $col
$writeColumns[] = $writeName;
}
}
- $selectColumns = array_filter($selectColumns, fn($value) => $value !== '');
- $writeColumns = array_filter($writeColumns, fn($value) => $value !== '');
- $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
- $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
+ $selectColumns = array_filter($selectColumns, fn($value): bool => $value !== '');
+ $writeColumns = array_filter($writeColumns, fn($value): bool => $value !== '');
+ $selectColumns = array_map($this->quoteColumnName(...), $selectColumns);
+ $writeColumns = array_map($this->quoteColumnName(...), $writeColumns);
if ($columnName && !$found) {
throw new InvalidArgumentException(sprintf(
- 'The specified column doesn\'t exist: %s',
+ "The specified column doesn't exist: %s",
$columnName,
));
}
@@ -999,8 +1011,8 @@ protected function calculateNewTableColumns(string $tableName, string|false $col
protected function beginAlterByCopyTable(string $tableName): AlterInstructions
{
$instructions = new AlterInstructions();
- $instructions->addPostStep(function ($state) use ($tableName) {
- $tmpTableName = "tmp_{$tableName}";
+ $instructions->addPostStep(function ($state) use ($tableName): array {
+ $tmpTableName = 'tmp_' . $tableName;
$createSQL = $this->getDeclaringSql($tableName);
// Table name in SQLite can be hilarious inside declaring SQL:
@@ -1016,7 +1028,7 @@ protected function beginAlterByCopyTable(string $tableName): AlterInstructions
$createSQL,
);
- $createSQL = "CREATE TABLE {$this->quoteTableName($tmpTableName)} ({$createSQL}";
+ $createSQL = sprintf('CREATE TABLE %s (%s', $this->quoteTableName($tmpTableName), $createSQL);
return compact('createSQL', 'tmpTableName') + $state;
});
@@ -1055,7 +1067,7 @@ protected function endAlterByCopyTable(
}
$result = $this->fetchRow('PRAGMA foreign_keys');
- $foreignKeysEnabled = $result ? (bool)$result['foreign_keys'] : false;
+ $foreignKeysEnabled = $result && (bool)$result['foreign_keys'];
if ($foreignKeysEnabled) {
$instructions->addPostStep('PRAGMA foreign_keys = OFF');
@@ -1072,7 +1084,7 @@ protected function endAlterByCopyTable(
$foreignKeysEnabled &&
$validateForeignKeys
) {
- $instructions = $this->validateForeignKeys($instructions, $tableName);
+ return $this->validateForeignKeys($instructions, $tableName);
}
return $instructions;
@@ -1085,7 +1097,7 @@ protected function getRenameColumnInstructions(string $tableName, string $column
{
$instructions = $this->beginAlterByCopyTable($tableName);
- $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
+ $instructions->addPostStep(function (array $state) use ($columnName, $newColumnName): array {
$sql = str_replace(
$this->quoteColumnName($columnName),
$this->quoteColumnName($newColumnName),
@@ -1096,7 +1108,7 @@ protected function getRenameColumnInstructions(string $tableName, string $column
return $state;
});
- $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
+ $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName): array {
$newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
return $newState + $state;
@@ -1112,10 +1124,10 @@ protected function getChangeColumnInstructions(string $tableName, string $column
{
$instructions = $this->beginAlterByCopyTable($tableName);
- $newColumnName = (string)$newColumn->getName();
- $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
+ $newColumnName = $newColumn->getName();
+ $instructions->addPostStep(function (array $state) use ($columnName, $newColumn): array {
$dialect = $this->getSchemaDialect();
- $sql = preg_replace(
+ $sql = (string)preg_replace(
sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
sprintf('%s$1', $dialect->columnDefinitionSql($newColumn->toArray())),
(string)$state['createSQL'],
@@ -1126,7 +1138,7 @@ protected function getChangeColumnInstructions(string $tableName, string $column
return $state;
});
- $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
+ $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName): array {
$newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
return $newState + $state;
@@ -1142,20 +1154,20 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa
{
$instructions = $this->beginAlterByCopyTable($tableName);
- $instructions->addPostStep(function ($state) use ($tableName, $columnName) {
+ $instructions->addPostStep(function ($state) use ($tableName, $columnName): array {
$newState = $this->calculateNewTableColumns($tableName, $columnName, false);
return $newState + $state;
});
- $instructions->addPostStep(function ($state) use ($columnName) {
- $sql = preg_replace(
+ $instructions->addPostStep(function (array $state) use ($columnName): array {
+ $sql = (string)preg_replace(
sprintf("/%s\s\w+.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName))),
'',
(string)$state['createSQL'],
);
- if (substr($sql, -2) === ', ') {
+ if (str_ends_with($sql, ', ')) {
$sql = substr($sql, 0, -2) . ')';
}
@@ -1176,9 +1188,8 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa
protected function getIndexes(string $tableName): array
{
$dialect = $this->getSchemaDialect();
- $indexes = $dialect->describeIndexes($tableName);
- return $indexes;
+ return $dialect->describeIndexes($tableName);
}
/**
@@ -1190,12 +1201,12 @@ protected function getIndexes(string $tableName): array
*/
protected function resolveIndex(string $tableName, string|array $columns): array
{
- $columns = array_map('strtolower', (array)$columns);
+ $columns = array_map(strtolower(...), (array)$columns);
$indexes = $this->getIndexes($tableName);
$matches = [];
foreach ($indexes as $index) {
- $indexCols = array_map('strtolower', $index['columns']);
+ $indexCols = array_map(strtolower(...), $index['columns']);
if ($columns == $indexCols) {
$matches[] = $index['name'];
}
@@ -1214,9 +1225,10 @@ protected function getAddIndexInstructions(TableMetadata $table, Index $index):
$indexColumnArray[] = sprintf('%s ASC', $this->quoteColumnName($column));
}
$indexColumns = implode(',', $indexColumnArray);
- $where = (string)$index->getWhere();
- if ($where) {
- $where = ' WHERE ' . $where;
+ $where = '';
+ $whereClause = $index->getWhere();
+ if ($whereClause) {
+ $where = ' WHERE ' . $whereClause;
}
$sql = sprintf(
'CREATE %s ON %s (%s)%s',
@@ -1242,7 +1254,7 @@ protected function getDropIndexByColumnsInstructions(string $tableName, $columns
if ($indexName === 'primary') {
continue;
}
- if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
+ if (!str_starts_with((string)$indexName, 'sqlite_autoindex_')) {
$instructions->addPostStep(sprintf(
'DROP INDEX %s%s',
$schema,
@@ -1265,7 +1277,7 @@ protected function getDropIndexByNameInstructions(string $tableName, string $ind
$found = false;
foreach ($indexes as $index) {
- if ($indexName === strtolower($index['name'])) {
+ if ($indexName === strtolower((string)$index['name'])) {
$found = true;
break;
}
@@ -1294,14 +1306,10 @@ public function hasPrimaryKey(string $tableName, $columns, ?string $constraint =
throw new InvalidArgumentException('SQLite does not support named constraints.');
}
- $columns = array_map('strtolower', (array)$columns);
- $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName));
-
- if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) {
- return false;
- }
+ $columns = array_map(strtolower(...), (array)$columns);
+ $primaryKey = array_map(strtolower(...), $this->getPrimaryKey($tableName));
- return true;
+ return !array_diff($primaryKey, $columns) && !array_diff($columns, $primaryKey);
}
/**
@@ -1332,9 +1340,8 @@ protected function getPrimaryKey(string $tableName): array
protected function getForeignKeys(string $tableName): array
{
$dialect = $this->getSchemaDialect();
- $keys = $dialect->describeForeignKeys($tableName);
- return $keys;
+ return $dialect->describeForeignKeys($tableName);
}
/**
@@ -1347,26 +1354,22 @@ protected function getAddPrimaryKeyInstructions(TableMetadata $table, string $co
$instructions = $this->beginAlterByCopyTable($table->getName());
$tableName = $table->getName();
- $instructions->addPostStep(function ($state) use ($column) {
+ $instructions->addPostStep(function (array $state) use ($column): array {
$quotedColumn = preg_quote($column);
- $columnPattern = "`{$quotedColumn}`|\"{$quotedColumn}\"|\[{$quotedColumn}\]";
- $matchPattern = "/($columnPattern)\s+(\w+(\(\d+\))?)(\s+(NOT )?NULL)?(\s+(?:PRIMARY KEY\s+)?AUTOINCREMENT)?/i";
+ $columnPattern = sprintf('`%s`|"%s"|\[%s\]', $quotedColumn, $quotedColumn, $quotedColumn);
+ $matchPattern = sprintf('/(%s)\s+(\w+(\(\d+\))?)(\s+(NOT )?NULL)?(\s+(?:PRIMARY KEY\s+)?AUTOINCREMENT)?/i', $columnPattern);
$sql = $state['createSQL'];
- if (preg_match($matchPattern, $state['createSQL'], $matches)) {
- if (isset($matches[2])) {
- $hasAutoIncrement = isset($matches[6]) && stripos($matches[6], 'AUTOINCREMENT') !== false;
-
- if ($matches[2] === 'INTEGER' && $hasAutoIncrement) {
- // Only add AUTOINCREMENT if the column already had it
- $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT';
- } else {
- $replace = '$1 $2 NOT NULL PRIMARY KEY';
- }
-
- $sql = preg_replace($matchPattern, $replace, (string)$state['createSQL'], 1);
+ if (preg_match($matchPattern, (string)$state['createSQL'], $matches) && isset($matches[2])) {
+ $hasAutoIncrement = isset($matches[6]) && stripos($matches[6], 'AUTOINCREMENT') !== false;
+ if ($matches[2] === 'INTEGER' && $hasAutoIncrement) {
+ // Only add AUTOINCREMENT if the column already had it
+ $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT';
+ } else {
+ $replace = '$1 $2 NOT NULL PRIMARY KEY';
}
+ $sql = preg_replace($matchPattern, $replace, (string)$state['createSQL'], 1);
}
$this->execute($sql);
@@ -1374,9 +1377,9 @@ protected function getAddPrimaryKeyInstructions(TableMetadata $table, string $co
return $state;
});
- $instructions->addPostStep(function ($state) {
+ $instructions->addPostStep(function (array $state): array {
$columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
- $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
+ $names = array_map($this->quoteColumnName(...), array_column($columns, 'name'));
$selectColumns = $writeColumns = $names;
return compact('selectColumns', 'writeColumns') + $state;
@@ -1395,7 +1398,7 @@ protected function getDropPrimaryKeyInstructions(TableMetadata $table, string $c
$tableName = $table->getName();
$instructions = $this->beginAlterByCopyTable($tableName);
- $instructions->addPostStep(function ($state) {
+ $instructions->addPostStep(function (array $state): array {
$search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/";
$sql = preg_replace($search, '', (string)$state['createSQL'], 1);
@@ -1406,7 +1409,7 @@ protected function getDropPrimaryKeyInstructions(TableMetadata $table, string $c
return $state;
});
- $instructions->addPostStep(function ($state) use ($column) {
+ $instructions->addPostStep(function (array $state) use ($column): array {
$newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column);
return $newState + $state;
@@ -1423,16 +1426,16 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey
$instructions = $this->beginAlterByCopyTable($table->getName());
$tableName = $table->getName();
- $instructions->addPostStep(function ($state) use ($foreignKey, $tableName) {
+ $instructions->addPostStep(function (array $state) use ($foreignKey, $tableName): array {
$this->execute('pragma foreign_keys = ON');
- $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . '); ';
+ $sql = substr((string)$state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey, $tableName) . '); ';
//Delete indexes from original table and recreate them in temporary table
$schema = $this->getSchemaName($tableName, true)['schema'];
$tmpTableName = $state['tmpTableName'];
$indexes = $this->getIndexes($tableName);
foreach ($indexes as $index) {
- if (strpos($index['name'], 'sqlite_autoindex_') !== 0) {
+ if (!str_starts_with((string)$index['name'], 'sqlite_autoindex_')) {
$sql .= sprintf(
'DROP INDEX %s%s; ',
$schema,
@@ -1440,7 +1443,7 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey
);
$createIndexSQL = $this->getDeclaringIndexSQL($tableName, $index['name']);
$sql .= preg_replace(
- "/\b{$tableName}\b/",
+ sprintf('/\b%s\b/', $tableName),
$tmpTableName,
$createIndexSQL,
);
@@ -1452,9 +1455,9 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey
return $state;
});
- $instructions->addPostStep(function ($state) {
+ $instructions->addPostStep(function (array $state): array {
$columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
- $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
+ $names = array_map($this->quoteColumnName(...), array_column($columns, 'name'));
$selectColumns = $writeColumns = $names;
return compact('selectColumns', 'writeColumns') + $state;
@@ -1491,13 +1494,13 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr
$instructions = $this->beginAlterByCopyTable($tableName);
- $instructions->addPostStep(function ($state) use ($columns) {
+ $instructions->addPostStep(function (array $state) use ($columns): array {
$search = sprintf(
"/,[^,]+?\(\s*%s\s*\)\s*REFERENCES[^,]*\([^\)]*\)[^,)]*/is",
implode(
'\s*,\s*',
array_map(
- fn($column) => $this->possiblyQuotedIdentifierRegex($column, false),
+ fn(string $column): string => $this->possiblyQuotedIdentifierRegex($column, false),
$columns,
),
),
@@ -1511,7 +1514,7 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr
return $state;
});
- $instructions->addPostStep(function ($state) {
+ $instructions->addPostStep(function (array $state): array {
$newState = $this->calculateNewTableColumns($state['tmpTableName'], false, false);
return $newState + $state;
@@ -1541,9 +1544,9 @@ protected function getAddCheckConstraintInstructions(TableMetadata $table, Check
$tableName = $table->getName();
$instructions = $this->beginAlterByCopyTable($tableName);
- $instructions->addPostStep(function ($state) use ($checkConstraint) {
+ $instructions->addPostStep(function (array $state) use ($checkConstraint): array {
$constraintName = $checkConstraint->getName();
- if ($constraintName === null) {
+ if ($constraintName === null || $constraintName === '') {
// Auto-generate constraint name if not provided
$constraintName = 'chk_' . substr(md5($checkConstraint->getExpression()), 0, 8);
}
@@ -1555,13 +1558,13 @@ protected function getAddCheckConstraintInstructions(TableMetadata $table, Check
);
// Add the check constraint before the closing parenthesis
- $sql = substr($state['createSQL'], 0, -1) . ', ' . $checkDef . ')';
+ $sql = substr((string)$state['createSQL'], 0, -1) . ', ' . $checkDef . ')';
$this->execute($sql);
return $state;
});
- $instructions->addPostStep(function ($state) use ($tableName) {
+ $instructions->addPostStep(function ($state) use ($tableName): array {
$newState = $this->calculateNewTableColumns($tableName, false, false);
return $newState + $state;
@@ -1577,11 +1580,11 @@ protected function getDropCheckConstraintInstructions(string $tableName, string
{
$instructions = $this->beginAlterByCopyTable($tableName);
- $instructions->addPostStep(function ($state) use ($constraintName) {
+ $instructions->addPostStep(function (array $state) use ($constraintName): array {
// Remove the check constraint from the CREATE TABLE statement
// Match CONSTRAINT name CHECK (expression) or just CHECK (expression)
$quotedName = $this->possiblyQuotedIdentifierRegex($constraintName, false);
- $pattern = "/,?\s*CONSTRAINT\s+{$quotedName}\s+CHECK\s*\([^)]+(?:\([^)]*\)[^)]*)*\)/is";
+ $pattern = sprintf('/,?\s*CONSTRAINT\s+%s\s+CHECK\s*\([^)]+(?:\([^)]*\)[^)]*)*\)/is', $quotedName);
$sql = preg_replace($pattern, '', (string)$state['createSQL'], 1);
if ($sql) {
@@ -1591,7 +1594,7 @@ protected function getDropCheckConstraintInstructions(string $tableName, string
return $state;
});
- $instructions->addPostStep(function ($state) use ($tableName) {
+ $instructions->addPostStep(function ($state) use ($tableName): array {
$newState = $this->calculateNewTableColumns($tableName, false, false);
return $newState + $state;
@@ -1640,11 +1643,7 @@ public function dropDatabase(string $name): void
*/
protected function getIndexSqlDefinition(TableMetadata $table, Index $index): string
{
- if ($index->getType() === Index::UNIQUE) {
- $def = 'UNIQUE INDEX';
- } else {
- $def = 'INDEX';
- }
+ $def = $index->getType() === Index::UNIQUE ? 'UNIQUE INDEX' : 'INDEX';
$indexName = $index->getName();
if ($indexName == '') {
$indexName = $table->getName() . '_';
@@ -1653,9 +1652,8 @@ protected function getIndexSqlDefinition(TableMetadata $table, Index $index): st
}
$indexName .= 'index';
}
- $def .= ' ' . $this->quoteColumnName($indexName);
- return $def;
+ return $def . ' ' . $this->quoteColumnName($indexName);
}
/**
@@ -1670,14 +1668,13 @@ public function getColumnTypes(): array
* Gets the SQLite Foreign Key Definition for an ForeignKey object.
*
* @param \Migrations\Db\Table\ForeignKey $foreignKey Foreign key
+ * @param string $tableName Table name for auto-generating constraint name
* @return string
*/
- protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
+ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
{
- $def = '';
- if ($foreignKey->getName()) {
- $def .= ' CONSTRAINT ' . $this->quoteColumnName((string)$foreignKey->getName());
- }
+ $constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
+ $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
$columnNames = [];
foreach ($foreignKey->getColumns() as $column) {
$columnNames[] = $this->quoteColumnName($column);
@@ -1687,7 +1684,11 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
foreach ($foreignKey->getReferencedColumns() as $column) {
$refColumnNames[] = $this->quoteColumnName($column);
}
- $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()) . ' (' . implode(',', $refColumnNames) . ')';
+ $referencedTable = $foreignKey->getReferencedTable();
+ if ($referencedTable === null) {
+ throw new InvalidArgumentException('Foreign key must have a referenced table.');
+ }
+ $def .= ' REFERENCES ' . $this->quoteTableName($referencedTable) . ' (' . implode(',', $refColumnNames) . ')';
if ($foreignKey->getOnDelete()) {
$def .= ' ON DELETE ' . $foreignKey->getOnDelete();
}
@@ -1698,6 +1699,31 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
return $def;
}
+ /**
+ * Generate a unique foreign key constraint name.
+ *
+ * @param string $tableName Table name
+ * @param array $columns Column names
+ * @return string
+ */
+ protected function getUniqueForeignKeyName(string $tableName, array $columns): string
+ {
+ $baseName = $tableName . '_' . implode('_', $columns);
+ $existingKeys = $this->getForeignKeys($tableName);
+ $existingNames = array_column($existingKeys, 'name');
+
+ if (!in_array($baseName, $existingNames, true)) {
+ return $baseName;
+ }
+
+ $counter = 2;
+ while (in_array($baseName . '_' . $counter, $existingNames, true)) {
+ $counter++;
+ }
+
+ return $baseName . '_' . $counter;
+ }
+
/**
* @inheritDoc
*/
@@ -1745,4 +1771,67 @@ protected function getUpsertClause(?InsertMode $mode, ?array $updateColumns, ?ar
return ' ON CONFLICT (' . implode(', ', $quotedConflictColumns) . ') DO UPDATE SET ' . implode(', ', $updates);
}
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getCreateViewInstructions(View $view): AlterInstructions
+ {
+ $sql = sprintf(
+ 'CREATE VIEW IF NOT EXISTS %s AS %s',
+ $this->quoteTableName($view->getName()),
+ $view->getDefinition(),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions
+ {
+ $sql = sprintf(
+ 'DROP VIEW IF EXISTS %s',
+ $this->quoteTableName($viewName),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions
+ {
+ $events = is_array($trigger->getEvent()) ? $trigger->getEvent() : [$trigger->getEvent()];
+ $eventStr = implode(' OR ', $events);
+
+ $forEach = $trigger->getForEach() ? 'FOR EACH ROW' : '';
+
+ $sql = sprintf(
+ 'CREATE TRIGGER %s %s %s ON %s %s BEGIN %s END',
+ $this->quoteColumnName($trigger->getName()),
+ $trigger->getTiming(),
+ $eventStr,
+ $this->quoteTableName($tableName),
+ $forEach,
+ $trigger->getDefinition(),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions
+ {
+ $sql = sprintf(
+ 'DROP TRIGGER IF EXISTS %s',
+ $this->quoteColumnName($triggerName),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
}
diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php
index e804d5908..9d50c84f0 100644
--- a/src/Db/Adapter/SqlserverAdapter.php
+++ b/src/Db/Adapter/SqlserverAdapter.php
@@ -21,6 +21,8 @@
use Migrations\Db\Table\ForeignKey;
use Migrations\Db\Table\Index;
use Migrations\Db\Table\TableMetadata;
+use Migrations\Db\Table\Trigger;
+use Migrations\Db\Table\View;
use Migrations\MigrationInterface;
/**
@@ -28,6 +30,11 @@
*/
class SqlserverAdapter extends AbstractAdapter
{
+ /**
+ * Maximum length for identifiers (table names, column names, constraint names, etc.)
+ */
+ protected const IDENTIFIER_MAX_LENGTH = 128;
+
/**
* @var string[]
*/
@@ -36,9 +43,6 @@ class SqlserverAdapter extends AbstractAdapter
self::TYPE_NATIVE_UUID,
];
- /**
- * @var string
- */
protected string $schema = 'dbo';
/**
@@ -213,10 +217,15 @@ protected function getChangeCommentInstructions(TableMetadata $table, ?string $n
*/
protected function getColumnCommentSqlDefinition(Column $column, ?string $tableName): string
{
+ $columnName = $column->getName();
+ if ($tableName === null) {
+ throw new InvalidArgumentException('Table name must be set.');
+ }
+
// passing 'null' is to remove column comment
- $currentComment = $this->getColumnComment((string)$tableName, $column->getName());
+ $currentComment = $this->getColumnComment($tableName, $columnName);
- $comment = strcasecmp((string)$column->getComment(), 'NULL') !== 0 ? $this->quoteString((string)$column->getComment()) : '\'\'';
+ $comment = strcasecmp((string)$column->getComment(), 'NULL') !== 0 ? $this->quoteString((string)$column->getComment()) : "''";
$command = $currentComment === null ? 'sp_addextendedproperty' : 'sp_updateextendedproperty';
return sprintf(
@@ -224,8 +233,8 @@ protected function getColumnCommentSqlDefinition(Column $column, ?string $tableN
$command,
$comment,
$this->schema,
- (string)$tableName,
- (string)$column->getName(),
+ $tableName,
+ $columnName,
);
}
@@ -268,6 +277,40 @@ public function truncateTable(string $tableName): void
$this->execute($sql);
}
+ /**
+ * @inheritDoc
+ */
+ public function disableForeignKeyConstraints(): void
+ {
+ // SQL Server doesn't support disabling FK checks globally.
+ // We drop all foreign key constraints instead.
+ $sql = "SELECT
+ fk.name AS constraint_name,
+ SCHEMA_NAME(t.schema_id) AS schema_name,
+ t.name AS table_name
+ FROM sys.foreign_keys fk
+ INNER JOIN sys.tables t ON fk.parent_object_id = t.object_id
+ WHERE SCHEMA_NAME(t.schema_id) = ?";
+
+ $rows = $this->query($sql, [$this->schema])->fetchAll('assoc');
+
+ foreach ($rows as $row) {
+ $constraintName = $this->quoteColumnName($row['constraint_name']);
+ $tableName = $this->quoteTableName($row['table_name']);
+ $this->execute(sprintf('ALTER TABLE %s DROP CONSTRAINT %s', $tableName, $constraintName));
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enableForeignKeyConstraints(): void
+ {
+ // SQL Server FK constraints were dropped, not disabled.
+ // They would need to be recreated, but after a reset/drop operation
+ // the tables will be recreated with their constraints by migrations.
+ }
+
/**
* @param string $tableName Table name
* @param ?string $columnName Column name
@@ -290,7 +333,7 @@ public function getColumnComment(string $tableName, ?string $columnName): ?strin
$row = $this->query($sql, $params)->fetch('assoc');
if ($row) {
- return trim($row['comment']);
+ return trim((string)$row['comment']);
}
return null;
@@ -338,7 +381,7 @@ protected function parseDefault(?string $default): int|string|null
$result = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default);
- if (strtoupper($result) === 'NULL') {
+ if (strtoupper((string)$result) === 'NULL') {
$result = null;
} elseif (is_numeric($result)) {
$result = (int)$result;
@@ -370,13 +413,13 @@ protected function getAddColumnInstructions(TableMetadata $table, Column $column
protected function getRenameColumnInstructions(string $tableName, string $columnName, string $newColumnName): AlterInstructions
{
if (!$this->hasColumn($tableName, $columnName)) {
- throw new InvalidArgumentException("The specified column does not exist: $columnName");
+ throw new InvalidArgumentException('The specified column does not exist: ' . $columnName);
}
$instructions = new AlterInstructions();
- $oldConstraintName = "DF_{$tableName}_{$columnName}";
- $newConstraintName = "DF_{$tableName}_{$newColumnName}";
+ $oldConstraintName = sprintf('DF_%s_%s', $tableName, $columnName);
+ $newConstraintName = sprintf('DF_%s_%s', $tableName, $newColumnName);
$sql = sprintf(
'IF (OBJECT_ID(%s, \'D\') IS NOT NULL)
BEGIN
@@ -389,7 +432,7 @@ protected function getRenameColumnInstructions(string $tableName, string $column
$instructions->addPostStep($sql);
$instructions->addPostStep(sprintf(
- 'EXECUTE sp_rename %s, %s, N\'COLUMN\'',
+ "EXECUTE sp_rename %s, %s, N'COLUMN'",
$this->quoteString($tableName . '.' . $columnName),
$this->quoteString($newColumnName),
));
@@ -406,26 +449,23 @@ protected function getRenameColumnInstructions(string $tableName, string $column
*/
protected function getChangeDefault(string $tableName, Column $newColumn): AlterInstructions
{
- $constraintName = "DF_{$tableName}_{$newColumn->getName()}";
+ $constraintName = sprintf('DF_%s_%s', $tableName, $newColumn->getName());
$default = $newColumn->getDefault();
$instructions = new AlterInstructions();
- if ($default === null) {
- $default = 'DEFAULT NULL';
- } else {
- $default = ltrim($this->getDefaultValueDefinition($default, (string)$newColumn->getType()));
- }
+ $default = $default === null ? 'DEFAULT NULL' : ltrim($this->getDefaultValueDefinition($default, $newColumn->getType()));
if (!$default) {
return $instructions;
}
+ $newColumnName = $newColumn->getName();
$instructions->addPostStep(sprintf(
'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s',
$this->quoteTableName($tableName),
$constraintName,
$default,
- $this->quoteColumnName((string)$newColumn->getName()),
+ $this->quoteColumnName($newColumnName),
));
return $instructions;
@@ -445,7 +485,7 @@ protected function getChangeColumnInstructions(string $tableName, string $column
}
}
if ($oldColumn === null) {
- throw new InvalidArgumentException("Unknown column {$columnName} cannot be changed.");
+ throw new InvalidArgumentException(sprintf('Unknown column %s cannot be changed.', $columnName));
}
$changeDefault =
@@ -455,14 +495,15 @@ protected function getChangeColumnInstructions(string $tableName, string $column
$instructions = new AlterInstructions();
$dialect = $this->getSchemaDialect();
- if ($columnName !== $newColumn->getName()) {
+ $newColumnName = $newColumn->getName();
+ if ($columnName !== $newColumnName) {
$instructions->merge(
- $this->getRenameColumnInstructions($tableName, $columnName, (string)$newColumn->getName()),
+ $this->getRenameColumnInstructions($tableName, $columnName, $newColumnName),
);
}
if ($changeDefault) {
- $instructions->merge($this->getDropDefaultConstraint($tableName, (string)$newColumn->getName()));
+ $instructions->merge($this->getDropDefaultConstraint($tableName, $newColumnName));
}
// Sqlserver doesn't support defaults
@@ -474,7 +515,8 @@ protected function getChangeColumnInstructions(string $tableName, string $column
$this->quoteTableName($tableName),
$dialect->columnDefinitionSql($columnData),
);
- $alterColumn = preg_replace('/DEFAULT NULL/', '', $alterColumn);
+ $alterColumn = (string)preg_replace('/DEFAULT NULL/', '', $alterColumn);
+
$instructions->addPostStep($alterColumn);
// change column comment if needed
@@ -557,7 +599,7 @@ protected function getIndexColumns(string $tableId, string $indexId): array
$rows = $this->query($sql, $params)->fetchAll('assoc');
$columns = [];
foreach ($rows as $row) {
- $columns[] = strtolower($row['column_name']);
+ $columns[] = strtolower((string)$row['column_name']);
}
return $columns;
@@ -598,7 +640,7 @@ protected function getDropIndexByColumnsInstructions(string $tableName, $columns
}
$indexes = $this->getIndexes($tableName);
- $columns = array_map('strtolower', $columns);
+ $columns = array_map(strtolower(...), $columns);
$instructions = new AlterInstructions();
foreach ($indexes as $index) {
@@ -828,7 +870,7 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
$indexName = sprintf('%s_%s', $parts['table'], implode('_', $columnNames));
}
$order = $index->getOrder() ?? [];
- $columnNames = array_map(function ($columnName) use ($order) {
+ $columnNames = array_map(function (string $columnName) use ($order): string {
$ret = '[' . $columnName . ']';
if (isset($order[$columnName])) {
$ret .= ' ' . $order[$columnName];
@@ -839,9 +881,10 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
$include = $index->getInclude();
$includedColumns = $include ? sprintf(' INCLUDE ([%s])', implode('],[', $include)) : '';
- $where = (string)$index->getWhere();
- if ($where) {
- $where = ' WHERE ' . $where;
+ $where = '';
+ $whereClause = $index->getWhere();
+ if ($whereClause) {
+ $where = ' WHERE ' . $whereClause;
}
return sprintf(
@@ -864,23 +907,56 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
*/
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
{
- $constraintName = $foreignKey->getName() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
+ $constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
$columnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getColumns()));
$refColumnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getReferencedColumns()));
$def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
$def .= ' FOREIGN KEY (' . $columnList . ')';
- $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()) . ' (' . $refColumnList . ')';
+ $referencedTable = $foreignKey->getReferencedTable();
+ if ($referencedTable === null) {
+ throw new InvalidArgumentException('Foreign key must have a referenced table.');
+ }
+ $def .= ' REFERENCES ' . $this->quoteTableName($referencedTable) . ' (' . $refColumnList . ')';
if ($foreignKey->getOnDelete()) {
- $def .= " ON DELETE {$foreignKey->getOnDelete()}";
+ $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
}
if ($foreignKey->getOnUpdate()) {
- $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
+ $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
}
return $def;
}
+ /**
+ * Generate a unique foreign key constraint name.
+ *
+ * @param string $tableName Table name
+ * @param array $columns Column names
+ * @return string
+ */
+ protected function getUniqueForeignKeyName(string $tableName, array $columns): string
+ {
+ $baseName = $tableName . '_' . implode('_', $columns);
+ $maxLength = static::IDENTIFIER_MAX_LENGTH - 3;
+ if (strlen($baseName) > $maxLength) {
+ $baseName = substr($baseName, 0, $maxLength);
+ }
+ $existingKeys = $this->getForeignKeys($tableName);
+ $existingNames = array_column($existingKeys, 'name');
+
+ if (!in_array($baseName, $existingNames, true)) {
+ return $baseName;
+ }
+
+ $counter = 2;
+ while (in_array($baseName . '_' . $counter, $existingNames, true)) {
+ $counter++;
+ }
+
+ return $baseName . '_' . $counter;
+ }
+
/**
* Creates the specified schema.
*
@@ -969,7 +1045,7 @@ protected function getSchemaName(string $tableName): array
{
$schema = $this->getGlobalSchemaName();
$table = $tableName;
- if (strpos($tableName, '.') !== false) {
+ if (str_contains($tableName, '.')) {
[$schema, $table] = explode('.', $tableName);
}
@@ -1072,7 +1148,7 @@ public function bulkinsert(
if ($v instanceof Literal) {
$placeholder = (string)$v;
}
- if ($placeholder == '?') {
+ if ($placeholder === '?') {
if ($v instanceof DateTime) {
$vals[] = $v->toDateTimeString();
} elseif ($v instanceof Date) {
@@ -1159,4 +1235,76 @@ protected function getInsertPrefix(?InsertMode $mode = null): string
return parent::getInsertPrefix($mode);
}
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getCreateViewInstructions(View $view): AlterInstructions
+ {
+ $drop = '';
+ if ($view->getReplace()) {
+ $drop = sprintf(
+ "IF OBJECT_ID('%s', 'V') IS NOT NULL DROP VIEW %s; ",
+ $view->getName(),
+ $this->quoteTableName($view->getName()),
+ );
+ }
+
+ $sql = sprintf(
+ '%sCREATE VIEW %s AS %s',
+ $drop,
+ $this->quoteTableName($view->getName()),
+ $view->getDefinition(),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions
+ {
+ $sql = sprintf(
+ "IF OBJECT_ID('%s', 'V') IS NOT NULL DROP VIEW %s",
+ $viewName,
+ $this->quoteTableName($viewName),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions
+ {
+ $events = is_array($trigger->getEvent()) ? $trigger->getEvent() : [$trigger->getEvent()];
+ $eventStr = implode(', ', $events);
+
+ $sql = sprintf(
+ 'CREATE TRIGGER %s ON %s %s %s AS %s',
+ $this->quoteColumnName($trigger->getName()),
+ $this->quoteTableName($tableName),
+ $trigger->getTiming(),
+ $eventStr,
+ $trigger->getDefinition(),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions
+ {
+ $sql = sprintf(
+ "IF OBJECT_ID('%s', 'TR') IS NOT NULL DROP TRIGGER %s",
+ $triggerName,
+ $this->quoteColumnName($triggerName),
+ );
+
+ return new AlterInstructions([], [$sql]);
+ }
}
diff --git a/src/Db/Adapter/TimedOutputAdapter.php b/src/Db/Adapter/TimedOutputAdapter.php
index f5e11be62..8b5feff33 100644
--- a/src/Db/Adapter/TimedOutputAdapter.php
+++ b/src/Db/Adapter/TimedOutputAdapter.php
@@ -54,16 +54,16 @@ public function startCommandTimer(): callable
public function writeCommand(string $command, array $args = []): void
{
$io = $this->getIo();
- if ($io && $io->level() < ConsoleIo::VERBOSE) {
+ if ($io instanceof ConsoleIo && $io->level() < ConsoleIo::VERBOSE) {
return;
}
- if (count($args)) {
+ if ($args !== []) {
$outArr = [];
foreach ($args as $arg) {
if (is_array($arg)) {
$arg = array_map(
- function ($value) {
- return '\'' . $value . '\'';
+ function (string $value): string {
+ return "'" . $value . "'";
},
$arg,
);
@@ -71,14 +71,14 @@ function ($value) {
continue;
}
- $outArr[] = '\'' . $arg . '\'';
+ $outArr[] = "'" . $arg . "'";
}
$this->getIo()?->verbose(' -- ' . $command . '(' . implode(', ', $outArr) . ')');
return;
}
- $this->getIo()->verbose(' -- ' . $command);
+ $this->getIo()?->verbose(' -- ' . $command);
}
/**
diff --git a/src/Db/Expression.php b/src/Db/Expression.php
index 46d9a4f66..10b6dc5d2 100644
--- a/src/Db/Expression.php
+++ b/src/Db/Expression.php
@@ -8,7 +8,9 @@
namespace Migrations\Db;
-class Expression
+use Stringable;
+
+class Expression implements Stringable
{
/**
* @var string The expression
diff --git a/src/Db/Literal.php b/src/Db/Literal.php
index b318daa14..843ad5645 100644
--- a/src/Db/Literal.php
+++ b/src/Db/Literal.php
@@ -8,11 +8,13 @@
namespace Migrations\Db;
+use Stringable;
+
/**
* Represent a value that should be used as a literal value when being
* interpolated into SQL commands.
*/
-class Literal
+class Literal implements Stringable
{
/**
* @var string The literal's value
diff --git a/src/Db/Plan/AlterTable.php b/src/Db/Plan/AlterTable.php
index 47d0ac64a..3c5146139 100644
--- a/src/Db/Plan/AlterTable.php
+++ b/src/Db/Plan/AlterTable.php
@@ -18,8 +18,6 @@ class AlterTable
{
/**
* The table
- *
- * @var \Migrations\Db\Table\TableMetadata
*/
protected TableMetadata $table;
diff --git a/src/Db/Plan/NewTable.php b/src/Db/Plan/NewTable.php
index 826283b6a..0e0723fc8 100644
--- a/src/Db/Plan/NewTable.php
+++ b/src/Db/Plan/NewTable.php
@@ -19,8 +19,6 @@ class NewTable
{
/**
* The table to create
- *
- * @var \Migrations\Db\Table\TableMetadata
*/
protected TableMetadata $table;
diff --git a/src/Db/Plan/Plan.php b/src/Db/Plan/Plan.php
index dcbaa718d..4fe9d7f24 100644
--- a/src/Db/Plan/Plan.php
+++ b/src/Db/Plan/Plan.php
@@ -9,6 +9,7 @@
namespace Migrations\Db\Plan;
use ArrayObject;
+use Migrations\Db\Action\AddCheckConstraint;
use Migrations\Db\Action\AddColumn;
use Migrations\Db\Action\AddForeignKey;
use Migrations\Db\Action\AddIndex;
@@ -17,10 +18,15 @@
use Migrations\Db\Action\ChangeComment;
use Migrations\Db\Action\ChangePrimaryKey;
use Migrations\Db\Action\CreateTable;
+use Migrations\Db\Action\CreateTrigger;
+use Migrations\Db\Action\CreateView;
+use Migrations\Db\Action\DropCheckConstraint;
use Migrations\Db\Action\DropForeignKey;
use Migrations\Db\Action\DropIndex;
use Migrations\Db\Action\DropPartition;
use Migrations\Db\Action\DropTable;
+use Migrations\Db\Action\DropTrigger;
+use Migrations\Db\Action\DropView;
use Migrations\Db\Action\RemoveColumn;
use Migrations\Db\Action\RenameColumn;
use Migrations\Db\Action\RenameTable;
@@ -87,6 +93,13 @@ class Plan
*/
protected array $columnRemoves = [];
+ /**
+ * List of view and trigger operations
+ *
+ * @var \Migrations\Db\Plan\AlterTable[]
+ */
+ protected array $viewsAndTriggers = [];
+
/**
* Constructor
*
@@ -111,6 +124,7 @@ protected function createPlan(array $actions): void
$this->gatherIndexes($actions);
$this->gatherConstraints($actions);
$this->gatherPartitions($actions);
+ $this->gatherViewsAndTriggers($actions);
$this->resolveConflicts();
}
@@ -126,6 +140,7 @@ protected function updatesSequence(): array
$this->constraints,
$this->indexes,
$this->partitions,
+ $this->viewsAndTriggers,
$this->columnRemoves,
$this->tableMoves,
];
@@ -141,6 +156,7 @@ protected function inverseUpdatesSequence(): array
return [
$this->constraints,
$this->tableMoves,
+ $this->viewsAndTriggers,
$this->partitions,
$this->indexes,
$this->columnRemoves,
@@ -211,7 +227,7 @@ protected function resolveConflicts(): void
$splitter = new ActionSplitter(
RenameColumn::class,
ChangeColumn::class,
- function (RenameColumn $a, ChangeColumn $b) {
+ function (RenameColumn $a, ChangeColumn $b): bool {
return $a->getNewName() === $b->getColumnName();
},
);
@@ -231,7 +247,7 @@ function (RenameColumn $a, ChangeColumn $b) {
$splitter = new ActionSplitter(
DropForeignKey::class,
AddForeignKey::class,
- function (DropForeignKey $a, AddForeignKey $b) {
+ function (DropForeignKey $a, AddForeignKey $b): bool {
return $a->getForeignKey()->getColumns() === $b->getForeignKey()->getColumns();
},
);
@@ -308,7 +324,7 @@ protected function remapContraintAndIndexConflicts(AlterTable $alter): AlterTabl
protected function forgetDropIndex(TableMetadata $table, array $columns, array $actions): array
{
$dropIndexActions = new ArrayObject();
- $indexes = array_map(function ($alter) use ($table, $columns, $dropIndexActions) {
+ $indexes = array_map(function (AlterTable $alter) use ($table, $columns, $dropIndexActions): AlterTable {
if ($alter->getTable()->getName() !== $table->getName()) {
return $alter;
}
@@ -340,7 +356,7 @@ protected function forgetDropIndex(TableMetadata $table, array $columns, array $
protected function forgetRemoveColumn(TableMetadata $table, array $columns, array $actions): array
{
$removeColumnActions = new ArrayObject();
- $indexes = array_map(function ($alter) use ($table, $columns, $removeColumnActions) {
+ $indexes = array_map(function (AlterTable $alter) use ($table, $columns, $removeColumnActions): AlterTable {
if ($alter->getTable()->getName() !== $table->getName()) {
return $alter;
}
@@ -407,8 +423,9 @@ protected function gatherUpdates(array $actions): void
&& !($action instanceof RemoveColumn)
&& !($action instanceof RenameColumn)
) {
- continue;
- } elseif (isset($this->tableCreates[$action->getTable()->getName()])) {
+ continue;
+ }
+ if (isset($this->tableCreates[$action->getTable()->getName()])) {
continue;
}
$table = $action->getTable();
@@ -467,7 +484,8 @@ protected function gatherIndexes(array $actions): void
foreach ($actions as $action) {
if (!($action instanceof AddIndex) && !($action instanceof DropIndex)) {
continue;
- } elseif (isset($this->tableCreates[$action->getTable()->getName()])) {
+ }
+ if (isset($this->tableCreates[$action->getTable()->getName()])) {
continue;
}
@@ -483,7 +501,9 @@ protected function gatherIndexes(array $actions): void
}
/**
- * Collects all foreign key creation and drops from the given intent
+ * Collects all constraint creation and drops from the given intent
+ *
+ * This includes foreign keys and check constraints.
*
* @param \Migrations\Db\Action\Action[] $actions The actions to parse
* @return void
@@ -491,7 +511,12 @@ protected function gatherIndexes(array $actions): void
protected function gatherConstraints(array $actions): void
{
foreach ($actions as $action) {
- if (!($action instanceof AddForeignKey || $action instanceof DropForeignKey)) {
+ if (
+ !($action instanceof AddForeignKey)
+ && !($action instanceof DropForeignKey)
+ && !($action instanceof AddCheckConstraint)
+ && !($action instanceof DropCheckConstraint)
+ ) {
continue;
}
$table = $action->getTable();
@@ -520,7 +545,8 @@ protected function gatherPartitions(array $actions): void
&& !($action instanceof SetPartitioning)
) {
continue;
- } elseif (isset($this->tableCreates[$action->getTable()->getName()])) {
+ }
+ if (isset($this->tableCreates[$action->getTable()->getName()])) {
continue;
}
@@ -534,4 +560,32 @@ protected function gatherPartitions(array $actions): void
$this->partitions[$name]->addAction($action);
}
}
+
+ /**
+ * Collects all view and trigger creation and drops from the given intent
+ *
+ * @param \Migrations\Db\Action\Action[] $actions The actions to parse
+ * @return void
+ */
+ protected function gatherViewsAndTriggers(array $actions): void
+ {
+ foreach ($actions as $action) {
+ if (
+ !($action instanceof CreateView)
+ && !($action instanceof DropView)
+ && !($action instanceof CreateTrigger)
+ && !($action instanceof DropTrigger)
+ ) {
+ continue;
+ }
+ $table = $action->getTable();
+ $name = $table->getName();
+
+ if (!isset($this->viewsAndTriggers[$name])) {
+ $this->viewsAndTriggers[$name] = new AlterTable($table);
+ }
+
+ $this->viewsAndTriggers[$name]->addAction($action);
+ }
+ }
}
diff --git a/src/Db/Plan/Solver/ActionSplitter.php b/src/Db/Plan/Solver/ActionSplitter.php
index 48dd92a3e..358b30120 100644
--- a/src/Db/Plan/Solver/ActionSplitter.php
+++ b/src/Db/Plan/Solver/ActionSplitter.php
@@ -8,6 +8,7 @@
namespace Migrations\Db\Plan\Solver;
+use Migrations\Db\Action\Action;
use Migrations\Db\Plan\AlterTable;
/**
@@ -21,16 +22,12 @@ class ActionSplitter
{
/**
* The fully qualified class name of the Action class to match for conflicts
- *
- * @var string
*/
protected string $conflictClass;
/**
* The fully qualified class name of the Action class to match for conflicts, which
* is the dual of $conflictClass. For example `AddColumn` and `DropColumn` are duals.
- *
- * @var string
*/
protected string $conflictClassDual;
@@ -70,7 +67,7 @@ public function __construct(string $conflictClass, string $conflictClassDual, ca
*/
public function __invoke(AlterTable $alter): array
{
- $conflictActions = array_filter($alter->getActions(), function ($action) {
+ $conflictActions = array_filter($alter->getActions(), function (Action $action): bool {
return $action instanceof $this->conflictClass;
});
diff --git a/src/Db/Table.php b/src/Db/Table.php
index 711efff0d..7f4768ede 100644
--- a/src/Db/Table.php
+++ b/src/Db/Table.php
@@ -11,6 +11,7 @@
use Cake\Collection\Collection;
use Cake\Core\Configure;
use InvalidArgumentException;
+use Migrations\Db\Action\AddCheckConstraint;
use Migrations\Db\Action\AddColumn;
use Migrations\Db\Action\AddForeignKey;
use Migrations\Db\Action\AddIndex;
@@ -19,10 +20,15 @@
use Migrations\Db\Action\ChangeComment;
use Migrations\Db\Action\ChangePrimaryKey;
use Migrations\Db\Action\CreateTable;
+use Migrations\Db\Action\CreateTrigger;
+use Migrations\Db\Action\CreateView;
+use Migrations\Db\Action\DropCheckConstraint;
use Migrations\Db\Action\DropForeignKey;
use Migrations\Db\Action\DropIndex;
use Migrations\Db\Action\DropPartition;
use Migrations\Db\Action\DropTable;
+use Migrations\Db\Action\DropTrigger;
+use Migrations\Db\Action\DropView;
use Migrations\Db\Action\RemoveColumn;
use Migrations\Db\Action\RenameColumn;
use Migrations\Db\Action\RenameTable;
@@ -31,6 +37,7 @@
use Migrations\Db\Adapter\MysqlAdapter;
use Migrations\Db\Plan\Intent;
use Migrations\Db\Plan\Plan;
+use Migrations\Db\Table\CheckConstraint;
use Migrations\Db\Table\Column;
use Migrations\Db\Table\ForeignKey;
use Migrations\Db\Table\Index;
@@ -51,30 +58,16 @@
*/
class Table
{
- /**
- * @var \Migrations\Db\Table\TableMetadata
- */
protected TableMetadata $table;
- /**
- * @var \Migrations\Db\Adapter\AdapterInterface|null
- */
protected ?AdapterInterface $adapter = null;
- /**
- * @var \Migrations\Db\Plan\Intent
- */
protected Intent $actions;
- /**
- * @var array
- */
protected array $data = [];
/**
* Insert mode for data operations
- *
- * @var \Migrations\Db\InsertMode|null
*/
protected ?InsertMode $insertMode = null;
@@ -111,7 +104,7 @@ public function __construct(string $name, array $options = [], ?AdapterInterface
$this->table = new TableMetadata($name, $options);
$this->actions = new Intent();
- if ($adapter !== null) {
+ if ($adapter instanceof AdapterInterface) {
$this->setAdapter($adapter);
}
}
@@ -167,7 +160,7 @@ public function setAdapter(AdapterInterface $adapter)
*/
public function getAdapter(): AdapterInterface
{
- if (!$this->adapter) {
+ if (!$this->adapter instanceof AdapterInterface) {
throw new RuntimeException('There is no database adapter set yet, cannot proceed');
}
@@ -181,7 +174,11 @@ public function getAdapter(): AdapterInterface
*/
public function hasPendingActions(): bool
{
- return count($this->actions->getActions()) > 0 || count($this->data) > 0;
+ if ($this->actions->getActions() !== []) {
+ return true;
+ }
+
+ return $this->data !== [];
}
/**
@@ -277,7 +274,7 @@ public function getColumn(string $name): ?Column
{
$columns = array_filter(
$this->getColumns(),
- function ($column) use ($name) {
+ function (Column $column) use ($name): bool {
return $column->getName() === $name;
},
);
@@ -358,19 +355,21 @@ public function addPrimaryKey(string|array $columns)
*/
public function addColumn(string|Column $columnName, ?string $type = null, array $options = [])
{
- assert($columnName instanceof Column || $type !== null);
if ($columnName instanceof Column) {
$action = new AddColumn($this->table, $columnName);
+ } elseif ($type === null) {
+ throw new InvalidArgumentException('Column type must not be null when column name is a string.');
} else {
$action = new AddColumn($this->table, $this->getAdapter()->getColumnForType($columnName, $type, $options));
}
// Delegate to Adapters to check column type
- if (!$this->getAdapter()->isValidColumnType($action->getColumn())) {
+ $column = $action->getColumn();
+ if (!$this->getAdapter()->isValidColumnType($column)) {
throw new InvalidArgumentException(sprintf(
'An invalid column type "%s" was specified for column "%s".',
- (string)$action->getColumn()->getType(),
- (string)$action->getColumn()->getName(),
+ $column->getType(),
+ $column->getName(),
));
}
@@ -459,13 +458,13 @@ public function changeColumn(string $columnName, string|Column|null $newColumnTy
if ($newColumnType === null) {
if (!$this->hasColumn($columnName)) {
throw new RuntimeException(
- "Cannot preserve column type for '$columnName' - column does not exist in table '{$this->getName()}'",
+ sprintf("Cannot preserve column type for '%s' - column does not exist in table '%s'", $columnName, $this->getName()),
);
}
$existingColumn = $this->getColumn($columnName);
- if ($existingColumn === null) {
+ if (!$existingColumn instanceof Column) {
throw new RuntimeException(
- "Cannot retrieve column definition for '$columnName' in table '{$this->getName()}'",
+ sprintf("Cannot retrieve column definition for '%s' in table '%s'", $columnName, $this->getName()),
);
}
$newColumnType = $existingColumn->getType();
@@ -474,7 +473,7 @@ public function changeColumn(string $columnName, string|Column|null $newColumnTy
if ($preserveUnspecified && $this->hasColumn($columnName)) {
// Get existing column definition
$existingColumn = $this->getColumn($columnName);
- if ($existingColumn !== null) {
+ if ($existingColumn instanceof Column) {
// Merge existing attributes with new ones
$options = $this->mergeColumnOptions($existingColumn, $newColumnType, $options);
}
@@ -582,7 +581,7 @@ public function addForeignKey(string|array|ForeignKey $columns, string|TableMeta
if ($columns instanceof ForeignKey) {
$action = new AddForeignKey($this->table, $columns);
} else {
- if (!$referencedTable) {
+ if ($referencedTable === null) {
throw new InvalidArgumentException('Referenced table is required');
}
$action = AddForeignKey::build($this->table, $columns, $referencedTable, $referencedColumns, $options);
@@ -619,6 +618,50 @@ public function hasForeignKey(string|array $columns, ?string $constraint = null)
return $this->getAdapter()->hasForeignKey($this->getName(), $columns, $constraint);
}
+ /**
+ * Add a check constraint to a database table.
+ *
+ * @param string|\Migrations\Db\Table\CheckConstraint $expression The check constraint expression or object
+ * @param array $options Options for the check constraint (e.g., 'name')
+ * @return $this
+ */
+ public function addCheckConstraint(string|CheckConstraint $expression, array $options = [])
+ {
+ if ($expression instanceof CheckConstraint) {
+ $action = new AddCheckConstraint($this->table, $expression);
+ } else {
+ $action = AddCheckConstraint::build($this->table, $expression, $options);
+ }
+ $this->actions->addAction($action);
+
+ return $this;
+ }
+
+ /**
+ * Removes the given check constraint from the table.
+ *
+ * @param string $constraintName The name of the check constraint to drop
+ * @return $this
+ */
+ public function dropCheckConstraint(string $constraintName)
+ {
+ $action = new DropCheckConstraint($this->table, $constraintName);
+ $this->actions->addAction($action);
+
+ return $this;
+ }
+
+ /**
+ * Checks to see if a check constraint exists.
+ *
+ * @param string $constraintName The name of the check constraint
+ * @return bool
+ */
+ public function hasCheckConstraint(string $constraintName): bool
+ {
+ return $this->getAdapter()->hasCheckConstraint($this->getName(), $constraintName);
+ }
+
/**
* Add partitioning to the table.
*
@@ -646,7 +689,7 @@ public function partitionBy(string $type, string|array|Literal $columns, array $
public function addPartition(string $name, mixed $value = null, array $options = [])
{
$partition = $this->table->getPartition();
- if ($partition === null) {
+ if (!$partition instanceof Partition) {
throw new RuntimeException('Must call partitionBy() before addPartition()');
}
@@ -707,15 +750,15 @@ public function addPartitionToExisting(string $name, mixed $value, array $option
*/
public function addTimestamps(string|false|null $createdAt = 'created', string|false|null $updatedAt = 'updated', bool $withTimezone = false)
{
- $createdAt = $createdAt ?? 'created';
- $updatedAt = $updatedAt ?? 'updated';
+ $createdAt ??= 'created';
+ $updatedAt ??= 'updated';
if (!$createdAt && !$updatedAt) {
throw new RuntimeException('Cannot set both created_at and updated_at columns to false');
}
$timestampConfig = (bool)Configure::read('Migrations.add_timestamps_use_datetime');
$timestampType = 'timestamp';
- if ($timestampConfig === true) {
+ if ($timestampConfig) {
$timestampType = 'datetime';
}
@@ -778,7 +821,7 @@ public function insert(array $data)
return $this;
}
- if (count($data) > 0) {
+ if ($data !== []) {
$this->data[] = $data;
}
@@ -851,7 +894,7 @@ public function insertOrUpdate(array $data, array $updateColumns, array $conflic
public function create(): void
{
$options = $this->getTable()->getOptions();
- if ((!isset($options['id']) || $options['id'] === false) && !empty($this->primaryKey)) {
+ if ((!isset($options['id']) || $options['id'] === false) && (isset($this->primaryKey) && !in_array($this->primaryKey, ['', '0', []], true))) {
$options['primary_key'] = (array)$this->primaryKey;
$this->filterPrimaryKey($options);
}
@@ -894,14 +937,14 @@ protected function filterPrimaryKey(array $options): void
/** @var \Cake\Collection\Collection $columnsCollection */
$columnsCollection = (new Collection($this->actions->getActions()))
- ->filter(function ($action) {
+ ->filter(function ($action): bool {
return $action instanceof AddColumn;
})
->map(function ($action) {
/** @var \Migrations\Db\Action\ChangeColumn|\Migrations\Db\Action\RenameColumn|\Migrations\Db\Action\RemoveColumn|\Migrations\Db\Action\AddColumn $action */
return $action->getColumn();
});
- $primaryKeyColumns = $columnsCollection->filter(function (Column $columnDef, $key) use ($primaryKey) {
+ $primaryKeyColumns = $columnsCollection->filter(function (Column $columnDef, $key) use ($primaryKey): bool {
return isset($primaryKey[$columnDef->getName()]);
})->toArray();
@@ -1013,6 +1056,87 @@ public function save(): void
}
}
+ /**
+ * Creates a view.
+ *
+ * @param string $viewName View name
+ * @param string $definition SQL SELECT statement for the view
+ * @param array $options View options
+ * @return $this
+ */
+ public function createView(string $viewName, string $definition, array $options = [])
+ {
+ $view = new Table\View(
+ $viewName,
+ $definition,
+ $options['replace'] ?? false,
+ $options['materialized'] ?? false,
+ );
+
+ $action = new Action\CreateView($this->table, $view);
+ $this->actions->addAction($action);
+
+ return $this;
+ }
+
+ /**
+ * Drops a view.
+ *
+ * @param string $viewName View name
+ * @param array $options View options
+ * @return $this
+ */
+ public function dropView(string $viewName, array $options = [])
+ {
+ $action = new Action\DropView(
+ $this->table,
+ $viewName,
+ $options['materialized'] ?? false,
+ );
+ $this->actions->addAction($action);
+
+ return $this;
+ }
+
+ /**
+ * Creates a trigger on this table.
+ *
+ * @param string $triggerName Trigger name
+ * @param string|array $event Event(s) that fire the trigger (INSERT, UPDATE, DELETE)
+ * @param string $definition Trigger body/definition
+ * @param array $options Trigger options
+ * @return $this
+ */
+ public function createTrigger(string $triggerName, string|array $event, string $definition, array $options = [])
+ {
+ $trigger = new Table\Trigger(
+ $triggerName,
+ $options['timing'] ?? Table\Trigger::BEFORE,
+ $event,
+ $definition,
+ $options['forEach'] ?? true,
+ );
+
+ $action = new Action\CreateTrigger($this->table, $trigger);
+ $this->actions->addAction($action);
+
+ return $this;
+ }
+
+ /**
+ * Drops a trigger from this table.
+ *
+ * @param string $triggerName Trigger name
+ * @return $this
+ */
+ public function dropTrigger(string $triggerName)
+ {
+ $action = new Action\DropTrigger($this->table, $triggerName);
+ $this->actions->addAction($action);
+
+ return $this;
+ }
+
/**
* Executes all the pending actions for this table
*
@@ -1036,15 +1160,35 @@ protected function executeActions(bool $exists): void
// If table exists and has partition configuration, create SetPartitioning action
if ($exists) {
$partition = $this->table->getPartition();
- if ($partition !== null && $partition->getDefinitions()) {
+ if ($partition instanceof Partition && $partition->getDefinitions()) {
$this->actions->addAction(new SetPartitioning($this->table, $partition));
}
}
// If the table does not exist, the last command in the chain needs to be
- // a CreateTable action.
+ // a CreateTable action - unless we're ONLY creating views/triggers.
if (!$exists) {
- $this->actions->addAction(new CreateTable($this->table));
+ $actions = $this->actions->getActions();
+ $hasTableActions = false;
+ $hasViewOrTriggerActions = false;
+
+ foreach ($actions as $action) {
+ if (
+ $action instanceof CreateView
+ || $action instanceof DropView
+ || $action instanceof CreateTrigger
+ || $action instanceof DropTrigger
+ ) {
+ $hasViewOrTriggerActions = true;
+ } else {
+ $hasTableActions = true;
+ }
+ }
+
+ // Only skip CreateTable if we have ONLY view/trigger actions (and at least one)
+ if (!$hasViewOrTriggerActions || $hasTableActions) {
+ $this->actions->addAction(new CreateTable($this->table));
+ }
}
$plan = new Plan($this->actions);
@@ -1063,8 +1207,8 @@ protected function executeActions(bool $exists): void
protected function mergeColumnOptions(Column $existingColumn, string $newColumnType, array $options): array
{
// Determine if type is changing
- $newTypeString = (string)$newColumnType;
- $existingTypeString = (string)$existingColumn->getType();
+ $newTypeString = $newColumnType;
+ $existingTypeString = $existingColumn->getType();
$typeChanging = $newTypeString !== $existingTypeString;
// Build array of existing column attributes
diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php
index 98f28b313..f49ff559b 100644
--- a/src/Db/Table/Column.php
+++ b/src/Db/Table/Column.php
@@ -42,81 +42,82 @@
class Column extends DatabaseColumn
{
public const BIGINTEGER = TableSchemaInterface::TYPE_BIGINTEGER;
+
public const SMALLINTEGER = TableSchemaInterface::TYPE_SMALLINTEGER;
+
public const TINYINTEGER = TableSchemaInterface::TYPE_TINYINTEGER;
+
public const BINARY = TableSchemaInterface::TYPE_BINARY;
+
public const BOOLEAN = TableSchemaInterface::TYPE_BOOLEAN;
+
public const CHAR = TableSchemaInterface::TYPE_CHAR;
+
public const DATE = TableSchemaInterface::TYPE_DATE;
+
public const DATETIME = TableSchemaInterface::TYPE_DATETIME;
+
public const DECIMAL = TableSchemaInterface::TYPE_DECIMAL;
+
public const FLOAT = TableSchemaInterface::TYPE_FLOAT;
+
public const INTEGER = TableSchemaInterface::TYPE_INTEGER;
+
public const STRING = TableSchemaInterface::TYPE_STRING;
+
public const TEXT = TableSchemaInterface::TYPE_TEXT;
+
public const TIME = TableSchemaInterface::TYPE_TIME;
+
public const TIMESTAMP = TableSchemaInterface::TYPE_TIMESTAMP;
+
public const UUID = TableSchemaInterface::TYPE_UUID;
+
public const BINARYUUID = TableSchemaInterface::TYPE_BINARY_UUID;
+
public const NATIVEUUID = TableSchemaInterface::TYPE_NATIVE_UUID;
+
/** MySQL-only column type */
public const YEAR = TableSchemaInterface::TYPE_YEAR;
+
/** MySQL/Postgres-only column type */
public const JSON = TableSchemaInterface::TYPE_JSON;
+
/** Postgres-only column type */
public const CIDR = TableSchemaInterface::TYPE_CIDR;
+
/** Postgres-only column type */
public const INET = TableSchemaInterface::TYPE_INET;
+
/** Postgres-only column type */
public const MACADDR = TableSchemaInterface::TYPE_MACADDR;
+
+ /** Postgres-only column type, requires the `citext` extension */
+ public const CITEXT = TableSchemaInterface::TYPE_CITEXT;
+
/** Postgres-only column type */
public const INTERVAL = TableSchemaInterface::TYPE_INTERVAL;
- /**
- * @var int|null
- */
protected ?int $seed = null;
- /**
- * @var int|null
- */
protected ?int $scale = null;
- /**
- * @var string|null
- */
protected ?string $update = null;
- /**
- * @var bool
- */
protected bool $timezone = false;
- /**
- * @var array
- */
protected array $properties = [];
- /**
- * @var string|null
- */
protected ?string $collation = null;
- /**
- * @var array|null
- */
protected ?array $values = null;
- /**
- * @var string|null
- */
protected ?string $algorithm = null;
- /**
- * @var string|null
- */
protected ?string $lock = null;
+ protected ?bool $fixed = null;
+
/**
* Column constructor
*
@@ -137,7 +138,6 @@ class Column extends DatabaseColumn
* @param int|null $srid The SRID for spatial columns.
* @param string|null $encoding The character set encoding for the column.
* @param string|null $baseType The base type for the column.
- * @return void
*/
public function __construct(
protected string $name = '',
@@ -177,9 +177,12 @@ public function setName(string $name)
/**
* Gets the column name.
*
- * @return string|null
+ * Narrows the return type from the parent's ?string to string,
+ * since $name is typed as string (not ?string) in this class.
+ *
+ * @return string
*/
- public function getName(): ?string
+ public function getName(): string
{
return $this->name;
}
@@ -229,7 +232,7 @@ public function setNull(bool $null)
*/
public function getNull(): bool
{
- return $this->null;
+ return $this->null ?? false;
}
/**
@@ -547,14 +550,9 @@ public function getUnsigned(): bool
if ($this->identity && Configure::read('Migrations.unsigned_primary_keys')) {
return true;
}
-
// Check general integer configuration
- if (Configure::read('Migrations.unsigned_ints')) {
- return true;
- }
-
// Default to signed for backward compatibility
- return false;
+ return (bool)Configure::read('Migrations.unsigned_ints');
}
/**
@@ -772,6 +770,31 @@ public function getLock(): ?string
return $this->lock;
}
+ /**
+ * Sets whether field should use fixed-length storage (for binary columns).
+ *
+ * When true, binary columns will use BINARY(n) instead of VARBINARY(n).
+ *
+ * @param bool|null $fixed Fixed
+ * @return $this
+ */
+ public function setFixed(?bool $fixed)
+ {
+ $this->fixed = $fixed;
+
+ return $this;
+ }
+
+ /**
+ * Gets whether field should use fixed-length storage.
+ *
+ * @return bool|null
+ */
+ public function getFixed(): ?bool
+ {
+ return $this->fixed;
+ }
+
/**
* Gets all allowed options. Each option must have a corresponding `setFoo` method.
*
@@ -802,6 +825,7 @@ protected function getValidOptions(): array
'generated',
'algorithm',
'lock',
+ 'fixed',
];
}
@@ -894,6 +918,7 @@ public function toArray(): array
'default' => $default,
'generated' => $this->getGenerated(),
'unsigned' => $this->getUnsigned(),
+ 'fixed' => $this->getFixed(),
'onUpdate' => $this->getUpdate(),
'collate' => $this->getCollation(),
'precision' => $precision,
diff --git a/src/Db/Table/ForeignKey.php b/src/Db/Table/ForeignKey.php
index 907e5f375..1bd85981a 100644
--- a/src/Db/Table/ForeignKey.php
+++ b/src/Db/Table/ForeignKey.php
@@ -23,11 +23,17 @@
class ForeignKey extends DatabaseForeignKey
{
public const CASCADE = 'CASCADE';
+
public const RESTRICT = 'RESTRICT';
+
public const SET_NULL = 'SET NULL';
+
public const NO_ACTION = 'NO ACTION';
+
public const DEFERRED = 'DEFERRABLE INITIALLY DEFERRED';
+
public const IMMEDIATE = 'DEFERRABLE INITIALLY IMMEDIATE';
+
public const NOT_DEFERRED = 'NOT DEFERRABLE';
/**
@@ -86,6 +92,19 @@ public function __construct(
}
}
+ /**
+ * {@inheritDoc}
+ *
+ * Narrows the return type from the parent's ?array to array,
+ * since $columns is always initialized as [] in this class.
+ *
+ * @return array
+ */
+ public function getColumns(): array
+ {
+ return $this->columns;
+ }
+
/**
* Utility method that maps an array of index options to this object's methods.
*
@@ -100,11 +119,11 @@ public function setOptions(array $options)
throw new RuntimeException(sprintf('"%s" is not a valid foreign key option.', $option));
}
- // handle $options['delete'] as $options['update']
+ // handle $options['delete'] and $options['update']
if ($option === 'delete') {
- $this->setOnDelete($value);
+ $this->delete = $this->normalizeAction($value);
} elseif ($option === 'update') {
- $this->setOnUpdate($value);
+ $this->update = $this->normalizeAction($value);
} elseif ($option === 'deferrable') {
$this->setDeferrableMode($value);
} else {
@@ -246,7 +265,9 @@ public function setOnDelete(string $onDelete)
*/
public function getOnDelete(): ?string
{
- return $this->mapAction($this->getDelete());
+ $delete = $this->getDelete();
+
+ return $delete !== null ? $this->mapAction($delete) : null;
}
/**
@@ -271,6 +292,8 @@ public function setOnUpdate(string $onUpdate)
*/
public function getOnUpdate(): ?string
{
- return $this->mapAction($this->getUpdate());
+ $update = $this->getUpdate();
+
+ return $update !== null ? $this->mapAction($update) : null;
}
}
diff --git a/src/Db/Table/Index.php b/src/Db/Table/Index.php
index f1ab782b4..507509af9 100644
--- a/src/Db/Table/Index.php
+++ b/src/Db/Table/Index.php
@@ -36,6 +36,46 @@ class Index extends DatabaseIndex
*/
public const FULLTEXT = 'fulltext';
+ /**
+ * PostgreSQL index access method: Generalized Inverted Index.
+ * Useful for full-text search, arrays, and JSONB columns.
+ *
+ * @var string
+ */
+ public const GIN = 'gin';
+
+ /**
+ * PostgreSQL index access method: Generalized Search Tree.
+ * Useful for geometric data, range types, and full-text search.
+ *
+ * @var string
+ */
+ public const GIST = 'gist';
+
+ /**
+ * PostgreSQL index access method: Space-Partitioned GiST.
+ * Useful for data with natural clustering like IP addresses or phone numbers.
+ *
+ * @var string
+ */
+ public const SPGIST = 'spgist';
+
+ /**
+ * PostgreSQL index access method: Block Range Index.
+ * Highly efficient for large, naturally-ordered tables like time-series data.
+ *
+ * @var string
+ */
+ public const BRIN = 'brin';
+
+ /**
+ * PostgreSQL index access method: Hash index.
+ * Handles simple equality comparisons. Rarely needed since B-tree handles equality efficiently.
+ *
+ * @var string
+ */
+ public const HASH = 'hash';
+
/**
* Constructor
*
@@ -47,6 +87,9 @@ class Index extends DatabaseIndex
* @param array|null $include The included columns for covering indexes.
* @param ?string $where The where clause for partial indexes.
* @param bool $concurrent Whether to create the index concurrently.
+ * @param ?string $algorithm The ALTER TABLE algorithm (MySQL-specific).
+ * @param ?string $lock The ALTER TABLE lock mode (MySQL-specific).
+ * @param array|null $opclass The operator class for each column (PostgreSQL).
*/
public function __construct(
protected string $name = '',
@@ -57,6 +100,9 @@ public function __construct(
protected ?array $include = null,
protected ?string $where = null,
protected bool $concurrent = false,
+ protected ?string $algorithm = null,
+ protected ?string $lock = null,
+ protected ?array $opclass = null,
) {
}
@@ -149,6 +195,80 @@ public function getConcurrently(): bool
return $this->concurrent;
}
+ /**
+ * Sets the ALTER TABLE algorithm (MySQL-specific).
+ *
+ * @param string $algorithm Algorithm
+ * @return $this
+ */
+ public function setAlgorithm(string $algorithm)
+ {
+ $this->algorithm = $algorithm;
+
+ return $this;
+ }
+
+ /**
+ * Gets the ALTER TABLE algorithm.
+ *
+ * @return string|null
+ */
+ public function getAlgorithm(): ?string
+ {
+ return $this->algorithm;
+ }
+
+ /**
+ * Sets the ALTER TABLE lock mode (MySQL-specific).
+ *
+ * @param string $lock Lock mode
+ * @return $this
+ */
+ public function setLock(string $lock)
+ {
+ $this->lock = $lock;
+
+ return $this;
+ }
+
+ /**
+ * Gets the ALTER TABLE lock mode.
+ *
+ * @return string|null
+ */
+ public function getLock(): ?string
+ {
+ return $this->lock;
+ }
+
+ /**
+ * Set the operator class for index columns.
+ *
+ * Operator classes specify which operators the index can use. This is primarily
+ * useful in PostgreSQL for specialized index types like GiST with trigram support.
+ *
+ * Example: ['column_name' => 'gist_trgm_ops']
+ *
+ * @param array $opclass Map of column names to operator classes.
+ * @return $this
+ */
+ public function setOpclass(array $opclass)
+ {
+ $this->opclass = $opclass;
+
+ return $this;
+ }
+
+ /**
+ * Get the operator class configuration for index columns.
+ *
+ * @return array|null
+ */
+ public function getOpclass(): ?array
+ {
+ return $this->opclass;
+ }
+
/**
* Utility method that maps an array of index options to this object's methods.
*
@@ -159,7 +279,7 @@ public function getConcurrently(): bool
public function setOptions(array $options)
{
// Valid Options
- $validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where'];
+ $validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where', 'algorithm', 'lock', 'opclass'];
foreach ($options as $option => $value) {
if (!in_array($option, $validOptions, true)) {
throw new RuntimeException(sprintf('"%s" is not a valid index option.', $option));
diff --git a/src/Db/Table/Partition.php b/src/Db/Table/Partition.php
index 12f8fe2a6..dffd7410a 100644
--- a/src/Db/Table/Partition.php
+++ b/src/Db/Table/Partition.php
@@ -18,10 +18,17 @@
class Partition
{
public const TYPE_RANGE = 'RANGE';
- public const TYPE_RANGE_COLUMNS = 'RANGE COLUMNS'; // MySQL: for DATE/STRING columns
+
+ public const TYPE_RANGE_COLUMNS = 'RANGE COLUMNS';
+
+ // MySQL: for DATE/STRING columns
public const TYPE_LIST = 'LIST';
- public const TYPE_LIST_COLUMNS = 'LIST COLUMNS'; // MySQL: for STRING columns
+
+ public const TYPE_LIST_COLUMNS = 'LIST COLUMNS';
+
+ // MySQL: for STRING columns
public const TYPE_HASH = 'HASH';
+
public const TYPE_KEY = 'KEY'; // MySQL only
/**
diff --git a/src/Db/Table/TableMetadata.php b/src/Db/Table/TableMetadata.php
index 09edd8500..3fc5b4a45 100644
--- a/src/Db/Table/TableMetadata.php
+++ b/src/Db/Table/TableMetadata.php
@@ -15,9 +15,6 @@
*/
class TableMetadata
{
- /**
- * @var string
- */
protected string $name;
/**
@@ -25,9 +22,6 @@ class TableMetadata
*/
protected array $options;
- /**
- * @var \Migrations\Db\Table\Partition|null
- */
protected ?Partition $partition = null;
/**
diff --git a/src/Db/Table/Trigger.php b/src/Db/Table/Trigger.php
new file mode 100644
index 000000000..a4e6286cd
--- /dev/null
+++ b/src/Db/Table/Trigger.php
@@ -0,0 +1,183 @@
+ $event The event(s) that fire the trigger (INSERT, UPDATE, DELETE).
+ * @param string $definition The trigger body/definition.
+ * @param bool $forEach Whether to fire for each row (true) or statement (false).
+ */
+ public function __construct(
+ protected string $name = '',
+ protected string $timing = self::BEFORE,
+ protected string|array $event = self::INSERT,
+ protected string $definition = '',
+ protected bool $forEach = true,
+ ) {
+ }
+
+ /**
+ * Sets the trigger name.
+ *
+ * @param string $name Name
+ * @return $this
+ */
+ public function setName(string $name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * Gets the trigger name.
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * Sets the trigger timing (BEFORE, AFTER, or INSTEAD OF).
+ *
+ * @param string $timing Timing
+ * @return $this
+ */
+ public function setTiming(string $timing)
+ {
+ $this->timing = $timing;
+
+ return $this;
+ }
+
+ /**
+ * Gets the trigger timing.
+ *
+ * @return string
+ */
+ public function getTiming(): string
+ {
+ return $this->timing;
+ }
+
+ /**
+ * Sets the trigger event(s).
+ *
+ * @param string|array $event Event(s)
+ * @return $this
+ */
+ public function setEvent(string|array $event)
+ {
+ $this->event = $event;
+
+ return $this;
+ }
+
+ /**
+ * Gets the trigger event(s).
+ *
+ * @return string|array
+ */
+ public function getEvent(): string|array
+ {
+ return $this->event;
+ }
+
+ /**
+ * Sets the trigger definition/body.
+ *
+ * @param string $definition Definition
+ * @return $this
+ */
+ public function setDefinition(string $definition)
+ {
+ $this->definition = $definition;
+
+ return $this;
+ }
+
+ /**
+ * Gets the trigger definition.
+ *
+ * @return string
+ */
+ public function getDefinition(): string
+ {
+ return $this->definition;
+ }
+
+ /**
+ * Sets whether to fire for each row (true) or per statement (false).
+ *
+ * @param bool $forEach For each row flag
+ * @return $this
+ */
+ public function setForEach(bool $forEach)
+ {
+ $this->forEach = $forEach;
+
+ return $this;
+ }
+
+ /**
+ * Gets whether to fire for each row.
+ *
+ * @return bool
+ */
+ public function getForEach(): bool
+ {
+ return $this->forEach;
+ }
+}
diff --git a/src/Db/Table/View.php b/src/Db/Table/View.php
new file mode 100644
index 000000000..ac45da392
--- /dev/null
+++ b/src/Db/Table/View.php
@@ -0,0 +1,128 @@
+name = $name;
+
+ return $this;
+ }
+
+ /**
+ * Gets the view name.
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * Sets the view definition (SELECT statement).
+ *
+ * @param string $definition Definition
+ * @return $this
+ */
+ public function setDefinition(string $definition)
+ {
+ $this->definition = $definition;
+
+ return $this;
+ }
+
+ /**
+ * Gets the view definition.
+ *
+ * @return string
+ */
+ public function getDefinition(): string
+ {
+ return $this->definition;
+ }
+
+ /**
+ * Sets whether to replace the view if it exists.
+ *
+ * @param bool $replace Replace flag
+ * @return $this
+ */
+ public function setReplace(bool $replace)
+ {
+ $this->replace = $replace;
+
+ return $this;
+ }
+
+ /**
+ * Gets whether to replace the view if it exists.
+ *
+ * @return bool
+ */
+ public function getReplace(): bool
+ {
+ return $this->replace;
+ }
+
+ /**
+ * Sets whether this is a materialized view (PostgreSQL only).
+ *
+ * @param bool $materialized Materialized flag
+ * @return $this
+ */
+ public function setMaterialized(bool $materialized)
+ {
+ $this->materialized = $materialized;
+
+ return $this;
+ }
+
+ /**
+ * Gets whether this is a materialized view.
+ *
+ * @return bool
+ */
+ public function getMaterialized(): bool
+ {
+ return $this->materialized;
+ }
+}
diff --git a/src/DirectionalMigrationInterface.php b/src/DirectionalMigrationInterface.php
new file mode 100644
index 000000000..a5004d105
--- /dev/null
+++ b/src/DirectionalMigrationInterface.php
@@ -0,0 +1,32 @@
+{MigrationInterface::INIT}();
}
- $atomic = $adapter->hasTransactions();
- if (method_exists($migration, 'useTransactions')) {
- $atomic = $migration->useTransactions();
- }
+ $atomic = $migration->useTransactions();
// begin the transaction if the adapter supports it
if ($atomic) {
$adapter->beginTransaction();
}
if (!$fake) {
- // Run the migration
- if (method_exists($migration, MigrationInterface::CHANGE)) {
+ // Run the migration. Dispatch order: capability interfaces first
+ // (statically narrowable for IDEs and static analysis), then a
+ // method_exists fallback for migrations that haven't yet adopted
+ // either ReversibleMigrationInterface or DirectionalMigrationInterface.
+ if (
+ $migration instanceof ReversibleMigrationInterface
+ || (!$migration instanceof DirectionalMigrationInterface
+ && method_exists($migration, MigrationInterface::CHANGE))
+ ) {
if ($direction === MigrationInterface::DOWN) {
// Create an instance of the RecordingAdapter so we can record all
// of the migration commands for reverse playback
@@ -112,7 +103,9 @@ public function executeMigration(MigrationInterface $migration, string $directio
} else {
$migration->{MigrationInterface::CHANGE}();
}
- } else {
+ } elseif ($migration instanceof DirectionalMigrationInterface) {
+ $direction === MigrationInterface::UP ? $migration->up() : $migration->down();
+ } elseif (method_exists($migration, $direction)) {
$migration->{$direction}();
}
}
@@ -323,7 +316,7 @@ public function getAdapter(): AdapterInterface
// Get the driver classname as those are aligned with adapter names.
$driver = $connection->getDriver();
- $driverClass = get_class($driver);
+ $driverClass = $driver::class;
$driverName = strtolower(substr($driverClass, (int)strrpos($driverClass, '\\') + 1));
$options['adapter'] = $driverName;
@@ -340,7 +333,7 @@ public function getAdapter(): AdapterInterface
}
$io = $this->getIo();
- if ($io) {
+ if ($io instanceof ConsoleIo) {
$adapter->setIo($io);
}
$this->setAdapter($adapter);
diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php
index 4a9f1886d..65ef382f4 100644
--- a/src/Migration/Manager.php
+++ b/src/Migration/Manager.php
@@ -10,7 +10,6 @@
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
-use Cake\Core\Configure;
use DateTime;
use Exception;
use InvalidArgumentException;
@@ -24,23 +23,16 @@
class Manager
{
public const BREAKPOINT_TOGGLE = 1;
+
public const BREAKPOINT_SET = 2;
+
public const BREAKPOINT_UNSET = 3;
- /**
- * @var \Migrations\Config\ConfigInterface
- */
protected ConfigInterface $config;
- /**
- * @var \Cake\Console\ConsoleIo
- */
protected ConsoleIo $io;
- /**
- * @var \Migrations\Migration\Environment|null
- */
- protected ?Environment $environment;
+ protected ?Environment $environment = null;
/**
* @var \Migrations\MigrationInterface[]|null
@@ -52,9 +44,6 @@ class Manager
*/
protected ?array $seeds = null;
- /**
- * @var \Psr\Container\ContainerInterface
- */
protected ContainerInterface $container;
/**
@@ -224,21 +213,11 @@ public function isSeedExecuted(SeedInterface $seed): bool
$seedLog = $adapter->getSeedLog();
- $plugin = null;
- $className = get_class($seed);
-
- if (str_contains($className, '\\')) {
- $parts = explode('\\', $className);
- $appNamespace = Configure::read('App.namespace', 'App');
- if (count($parts) > 1 && $parts[0] !== $appNamespace) {
- $plugin = $parts[0];
- }
- }
-
+ $plugin = Util::getSeedPlugin($seed);
$seedName = $seed->getName();
foreach ($seedLog as $entry) {
- if ($entry['seed_name'] === $seedName && $entry['plugin'] === $plugin) {
+ if ($entry['seed_name'] === $seedName && Util::matchesSeedPlugin($entry['plugin'], $plugin)) {
return true;
}
}
@@ -306,7 +285,7 @@ public function markMigrated(int $version, string $path): bool
}
// Check if the file returns an anonymous class instance
- if (is_object($migrationInstance) && $migrationInstance instanceof MigrationInterface) {
+ if ($migrationInstance instanceof MigrationInterface) {
$migration = $migrationInstance;
$migration->setVersion($version);
} elseif (class_exists($className)) {
@@ -332,19 +311,25 @@ public function markMigrated(int $version, string $path): bool
* Resolves a migration class name based on $path
*
* @param string $path Path to the migration file of which we want the class name
- * @return class-string<\Migrations\MigrationInterface> Migration class name
+ * @return string Migration class name
+ * @phpstan-return class-string<\Migrations\MigrationInterface>
*/
protected function getMigrationClassName(string $path): string
{
- $class = (string)preg_replace('/^[0-9]+_/', '', basename($path));
+ $class = (string)preg_replace('/^\d+_/', '', basename($path));
$class = str_replace('_', ' ', $class);
$class = ucwords($class);
$class = str_replace(' ', '', $class);
- if (strpos($class, '.') !== false) {
- $class = substr($class, 0, strpos($class, '.'));
+
+ $dotPos = strpos($class, '.');
+ if ($dotPos !== false) {
+ /** @var class-string<\Migrations\MigrationInterface> $name */
+ $name = substr($class, 0, $dotPos);
+
+ return $name;
}
- /** @var class-string<\Migrations\MigrationInterface> */
+ /** @var class-string<\Migrations\MigrationInterface> $class */
return $class;
}
@@ -376,17 +361,17 @@ public function getVersionsToMark(Arguments $args): array
if ($args->getOption('only') || $versionArg) {
if (!in_array($version, $versions)) {
- throw new InvalidArgumentException("Migration `$version` was not found !");
+ throw new InvalidArgumentException(sprintf('Migration `%d` was not found !', $version));
}
return [$version];
}
$lengthIncrease = $args->getOption('exclude') ? 0 : 1;
- $index = array_search($version, $versions);
+ $index = array_search($version, $versions, true);
if ($index === false) {
- throw new InvalidArgumentException("Migration `$version` was not found !");
+ throw new InvalidArgumentException(sprintf('Migration `%d` was not found !', $version));
}
return array_slice($versions, 0, $index + $lengthIncrease);
@@ -459,16 +444,15 @@ public function migrate(?int $version = null, bool $fake = false, ?int $count =
}
if ($version === null) {
- $version = max(array_merge($versions, array_keys($migrations)));
- } else {
- if ($version !== 0 && !isset($migrations[$version])) {
- $this->getIo()->out(sprintf(
- 'warning %s is not a valid version',
- $version,
- ));
+ $candidates = [...$versions, ...array_keys($migrations)];
+ $version = $candidates ? max($candidates) : 0;
+ } elseif ($version !== 0 && !isset($migrations[$version])) {
+ $this->getIo()->out(sprintf(
+ 'warning %s is not a valid version',
+ $version,
+ ));
- return;
- }
+ return;
}
// are we migrating up or down?
@@ -514,6 +498,10 @@ public function executeMigration(MigrationInterface $migration, string $directio
{
$this->getIo()->out('');
+ // Make the adapter available so shouldExecute() can inspect the database.
+ // The environment sets it again (and wraps it for down/change) before running the migration body.
+ $migration->setAdapter($this->getEnvironment()->getAdapter());
+
// Skip the migration if it should not be executed
if (!$migration->shouldExecute()) {
$this->printMigrationStatus($migration, 'skipped');
@@ -545,6 +533,9 @@ public function executeMigration(MigrationInterface $migration, string $directio
*/
public function executeSeed(SeedInterface $seed, bool $force = false, bool $fake = false): void
{
+ // Make the adapter available so shouldExecute() can inspect the database.
+ $seed->setAdapter($this->getEnvironment()->getAdapter());
+
// Skip the seed if it should not be executed
if (!$seed->shouldExecute()) {
$this->getIo()->out('');
@@ -583,14 +574,12 @@ public function executeSeed(SeedInterface $seed, bool $force = false, bool $fake
// Auto-execute missing dependencies
$missingDeps = $this->getSeedDependenciesNotExecuted($seed);
- if (!empty($missingDeps)) {
- foreach ($missingDeps as $depSeed) {
- $this->getIo()->verbose(sprintf(
- ' Auto-executing dependency: %s',
- $depSeed->getName(),
- ));
- $this->executeSeed($depSeed, $force, $fake);
- }
+ foreach ($missingDeps as $depSeed) {
+ $this->getIo()->verbose(sprintf(
+ ' Auto-executing dependency: %s',
+ $depSeed->getName(),
+ ));
+ $this->executeSeed($depSeed, $force, $fake);
}
$this->printSeedStatus($seed, 'seeding');
@@ -733,7 +722,7 @@ public function rollback(int|string|null $target = null, bool $force = false, bo
$target = 0;
} elseif (!is_numeric($target) && $target !== null) { // try to find a target version based on name
// search through the migrations using the name
- $migrationNames = array_map(function ($item) {
+ $migrationNames = array_map(function (array $item) {
return $item['migration_name'];
}, $executedVersions);
$found = array_search($target, $migrationNames, true);
@@ -742,7 +731,7 @@ public function rollback(int|string|null $target = null, bool $force = false, bo
if ($found !== false) {
$target = (string)$found;
} else {
- $io->out("No migration found with name ($target)");
+ $io->out(sprintf('No migration found with name (%s)', $target));
return;
}
@@ -765,7 +754,7 @@ public function rollback(int|string|null $target = null, bool $force = false, bo
// If the target must match a version, check the target version exists
if ($targetMustMatchVersion && $target !== 0 && !isset($migrations[$target])) {
- $io->out("Target version ($target) not found");
+ $io->out(sprintf('Target version (%s) not found', $target));
return;
}
@@ -781,13 +770,8 @@ public function rollback(int|string|null $target = null, bool $force = false, bo
if (in_array($migration->getVersion(), $executedVersionCreationTimes)) {
$executedArray = $executedVersions[$migration->getVersion()];
- if (!$targetMustMatchVersion) {
- if (
- ($this->getConfig()->isVersionOrderCreationTime() && $executedArray['version'] <= $target) ||
- (!$this->getConfig()->isVersionOrderCreationTime() && $executedArray['start_time'] <= $target)
- ) {
- break;
- }
+ if (!$targetMustMatchVersion && ($this->getConfig()->isVersionOrderCreationTime() && $executedArray['version'] <= $target || !$this->getConfig()->isVersionOrderCreationTime() && $executedArray['start_time'] <= $target)) {
+ break;
}
if ((int)$executedArray['breakpoint'] !== 0 && !$force) {
@@ -843,7 +827,7 @@ public function seed(?string $seed = null, bool $force = false, bool $fake = fal
*/
public function getEnvironment(): Environment
{
- if (isset($this->environment)) {
+ if ($this->environment instanceof Environment) {
return $this->environment;
}
@@ -885,7 +869,6 @@ public function getIo(): ConsoleIo
/**
* Replace the environment
*
- * @param \Migrations\Migration\Environment $environment
* @return $this
*/
public function setEnvironment(Environment $environment)
@@ -937,8 +920,8 @@ public function getMigrations(): array
$io->verbose('Migration file');
$io->verbose(
array_map(
- function ($phpFile) {
- return " {$phpFile}";
+ function (string $phpFile): string {
+ return sprintf(' %s', $phpFile);
},
$phpFiles,
),
@@ -952,7 +935,7 @@ function ($phpFile) {
$io = $this->getIo();
foreach ($phpFiles as $filePath) {
if (Util::isValidMigrationFileName(basename($filePath))) {
- $io->verbose("Valid migration file {$filePath}.");
+ $io->verbose(sprintf('Valid migration file %s.', $filePath));
$version = Util::getVersionFromFileName(basename($filePath));
@@ -973,9 +956,10 @@ function ($phpFile) {
$fileNames[$class] = basename($filePath);
- $io->verbose("Loading class $class from $filePath.");
+ $io->verbose(sprintf('Loading class %s from %s.', $class, $filePath));
+
+ $this->checkMigrationClass($filePath);
- // load the migration file
$orig_display_errors_setting = ini_get('display_errors');
ini_set('display_errors', 'On');
@@ -991,13 +975,13 @@ function ($phpFile) {
ini_set('display_errors', $orig_display_errors_setting);
// Check if the file returns an anonymous class instance
- if (is_object($migrationInstance) && $migrationInstance instanceof MigrationInterface) {
- $io->verbose("Using anonymous class from $filePath.");
+ if ($migrationInstance instanceof MigrationInterface) {
+ $io->verbose(sprintf('Using anonymous class from %s.', $filePath));
$migration = $migrationInstance;
$migration->setVersion($version);
} elseif (class_exists($class)) {
// Fall back to traditional class-based migration
- $io->verbose("Constructing $class.");
+ $io->verbose(sprintf('Constructing %s.', $class));
$migration = new $class($version);
} else {
throw new InvalidArgumentException(sprintf(
@@ -1014,7 +998,7 @@ function ($phpFile) {
$versions[$version] = $migration;
} else {
- $io->verbose("Invalid migration file {$filePath}.");
+ $io->verbose(sprintf('Invalid migration file %s.', $filePath));
}
}
@@ -1025,6 +1009,32 @@ function ($phpFile) {
return (array)$this->migrations;
}
+ /**
+ * Prevent fatal errors when loading legacy migration files that still reference old classes
+ *
+ * @param string $filePath Migration file path
+ * @return void
+ */
+ protected function checkMigrationClass(string $filePath): void
+ {
+ $contents = file_get_contents($filePath);
+ if ($contents === false) {
+ return;
+ }
+
+ $usesLegacyAbstractMigration =
+ str_contains($contents, 'use Migrations\AbstractMigration;') ||
+ str_contains($contents, 'extends AbstractMigration') ||
+ str_contains($contents, 'extends \Migrations\AbstractMigration');
+
+ if ($usesLegacyAbstractMigration) {
+ throw new RuntimeException(sprintf(
+ 'Migration file `%s` uses the legacy `Migrations\\AbstractMigration` class, which is not available in this version. Update the migration to extend `Migrations\\BaseMigration`.',
+ $filePath,
+ ));
+ }
+ }
+
/**
* Returns a list of migration files found in the provided migration paths.
*
@@ -1178,19 +1188,15 @@ public function getSeeds(): array
}
// Check if the file returns an anonymous class instance
- if (is_object($seedInstance) && $seedInstance instanceof SeedInterface) {
- $io->verbose("Using anonymous class from $filePath.");
+ if ($seedInstance instanceof SeedInterface) {
+ $io->verbose(sprintf('Using anonymous class from %s.', $filePath));
$seed = $seedInstance;
} elseif (class_exists($class)) {
// Fall back to traditional class-based seed
- $io->verbose("Instantiating $class.");
+ $io->verbose(sprintf('Instantiating %s.', $class));
// instantiate it
/** @var \Migrations\SeedInterface $seed */
- if (isset($this->container)) {
- $seed = $this->container->get($class);
- } else {
- $seed = new $class();
- }
+ $seed = isset($this->container) ? $this->container->get($class) : new $class();
} else {
throw new InvalidArgumentException(sprintf(
'Could not find class `%s` in file `%s` and file did not return a seed instance',
@@ -1405,7 +1411,7 @@ public function cleanupMissingMigrations(): int
// Find missing migrations (those in migration table but not in filesystem)
$missingVersions = [];
- foreach ($versions as $versionId => $versionInfo) {
+ foreach (array_keys($versions) as $versionId) {
if (!isset($defaultMigrations[$versionId])) {
$missingVersions[] = $versionId;
}
diff --git a/src/Migration/ManagerFactory.php b/src/Migration/ManagerFactory.php
index 362a06305..d4636f6c7 100644
--- a/src/Migration/ManagerFactory.php
+++ b/src/Migration/ManagerFactory.php
@@ -99,10 +99,10 @@ public function createConfig(): ConfigInterface
$connectionConfig = ConnectionManager::getConfig($connectionName);
}
if (!$connectionConfig) {
- throw new RuntimeException("Could not find connection `{$connectionName}`");
+ throw new RuntimeException(sprintf('Could not find connection `%s`', $connectionName));
}
if (!isset($connectionConfig['database'])) {
- throw new RuntimeException("The `{$connectionName}` connection has no `database` key defined.");
+ throw new RuntimeException(sprintf('The `%s` connection has no `database` key defined.', $connectionName));
}
$adapter = $connectionConfig['scheme'] ?? null;
diff --git a/src/Migrations.php b/src/Migrations.php
index b61bf07cf..3555f5f42 100644
--- a/src/Migrations.php
+++ b/src/Migrations.php
@@ -33,8 +33,6 @@ class Migrations
* Current command being run.
* Useful if some logic needs to be applied in the ConfigurationTrait depending
* on the command
- *
- * @var string
*/
protected string $command = '';
diff --git a/src/MigrationsPlugin.php b/src/MigrationsPlugin.php
index b627f1cd4..f0fbded80 100644
--- a/src/MigrationsPlugin.php
+++ b/src/MigrationsPlugin.php
@@ -26,6 +26,7 @@
use Migrations\Command\EntryCommand;
use Migrations\Command\MarkMigratedCommand;
use Migrations\Command\MigrateCommand;
+use Migrations\Command\ResetCommand;
use Migrations\Command\RollbackCommand;
use Migrations\Command\SeedCommand;
use Migrations\Command\SeedResetCommand;
@@ -73,6 +74,7 @@ public function console(CommandCollection $commands): CommandCollection
DumpCommand::class,
MarkMigratedCommand::class,
MigrateCommand::class,
+ ResetCommand::class,
RollbackCommand::class,
StatusCommand::class,
];
diff --git a/src/Rector/AddMigrationCapabilityInterfaceRector.php b/src/Rector/AddMigrationCapabilityInterfaceRector.php
new file mode 100644
index 000000000..9ef0ab7a6
--- /dev/null
+++ b/src/Rector/AddMigrationCapabilityInterfaceRector.php
@@ -0,0 +1,157 @@
+>
+ */
+ public function getNodeTypes(): array
+ {
+ return [Class_::class];
+ }
+
+ /**
+ * @param \PhpParser\Node\Stmt\Class_ $node
+ */
+ public function refactor(Node $node): ?Node
+ {
+ $classReflection = ScopeFetcher::fetch($node)->getClassReflection();
+ if (!$classReflection instanceof ClassReflection) {
+ return null;
+ }
+
+ // Both abstract bases and anonymous migrations are valid targets: abstract
+ // bases propagate the interface to every leaf, and anonymous migrations are
+ // baked the same way concrete ones are. The only excluded type is
+ // BaseMigration itself, since it must remain style-agnostic.
+ if (
+ $classReflection->getName() === BaseMigration::class
+ || !$classReflection->isSubclassOf(BaseMigration::class)
+ ) {
+ return null;
+ }
+
+ // Already annotated with either capability interface: leave it alone so the
+ // rule stays idempotent and never ends up adding the second, conflicting one.
+ if (
+ $classReflection->implementsInterface(ReversibleMigrationInterface::class)
+ || $classReflection->implementsInterface(DirectionalMigrationInterface::class)
+ ) {
+ return null;
+ }
+
+ $hasChange = $this->classHasMethod($node, MigrationInterface::CHANGE);
+ $hasUp = $this->classHasMethod($node, MigrationInterface::UP);
+ $hasDown = $this->classHasMethod($node, MigrationInterface::DOWN);
+
+ // Mixed style is user error. Leave it untouched so the developer picks deliberately.
+ if ($hasChange && ($hasUp || $hasDown)) {
+ return null;
+ }
+
+ // Only adopt the directional interface when BOTH methods are present. A one-way
+ // migration (only up() or only down()) must stay on the Environment method_exists()
+ // fallback: DirectionalMigrationInterface makes Environment call the missing
+ // direction unconditionally, turning a rollback no-op into a fatal error.
+ $targetInterface = null;
+ if ($hasChange) {
+ $targetInterface = ReversibleMigrationInterface::class;
+ } elseif ($hasUp && $hasDown) {
+ $targetInterface = DirectionalMigrationInterface::class;
+ }
+
+ if ($targetInterface === null) {
+ return null;
+ }
+
+ $node->implements[] = new FullyQualified($targetInterface);
+
+ return $node;
+ }
+
+ /**
+ * @param \PhpParser\Node\Stmt\Class_ $class Class node to inspect.
+ * @param string $methodName Method name to look for on the class itself.
+ * @return bool
+ */
+ private function classHasMethod(Class_ $class, string $methodName): bool
+ {
+ foreach ($class->getMethods() as $method) {
+ if ($this->getName($method) === $methodName) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/ReversibleMigrationInterface.php b/src/ReversibleMigrationInterface.php
new file mode 100644
index 000000000..ba0d01a4c
--- /dev/null
+++ b/src/ReversibleMigrationInterface.php
@@ -0,0 +1,31 @@
+ $migrationSet) {
$migrationSet += ['connection' => 'test'];
$skip = $migrationSet['skip'] ?? [];
@@ -98,9 +97,29 @@ public function runMany(
$connectionsList[$connectionName] = ['name' => $connectionName, 'skip' => $skip];
}
+ // Sets sharing a connection and a plugin also share a migration history,
+ // while each of them only has its own source directory on disk. Group
+ // them so that a set is not told that its siblings' applied migrations
+ // are missing.
+ $groupKey = $connectionName . '|' . ($migrationSet['plugin'] ?? '');
+ $historyGroups[$groupKey][] = $migrationSet;
+ $sets[] = ['options' => $migrationSet, 'skip' => $skip, 'group' => $groupKey];
+ }
+
+ $groupMigrationIds = [];
+ foreach ($historyGroups as $groupKey => $group) {
+ $groupMigrationIds[$groupKey] = $this->getSiblingMigrationIds($group);
+ }
+
+ foreach ($sets as $set) {
+ $connectionName = $set['options']['connection'];
+ if (isset($connectionsToDrop[$connectionName])) {
+ continue;
+ }
+
$migrations = new Migrations();
- if (!isset($connectionsToDrop[$connectionName]) && $this->shouldDropTables($migrations, $migrationSet)) {
- $connectionsToDrop[$connectionName] = ['name' => $connectionName, 'skip' => $skip];
+ if ($this->shouldDropTables($migrations, $set['options'], $groupMigrationIds[$set['group']])) {
+ $connectionsToDrop[$connectionName] = ['name' => $connectionName, 'skip' => $set['skip']];
}
}
@@ -115,7 +134,7 @@ public function runMany(
try {
if (!$migrations->migrate($migrationSet)) {
throw new RuntimeException(
- "Unable to migrate fixtures for `{$migrationSet['connection']}`.",
+ sprintf('Unable to migrate fixtures for `%s`.', $migrationSet['connection']),
);
}
} catch (Exception $e) {
@@ -124,7 +143,7 @@ public function runMany(
"Migrations failed to apply with message:\n\n" .
$e->getMessage() . "\n\n" .
'If you are using the `skip` option and running multiple sets of migrations ' .
- 'on the same connection, you can\'t skip tables managed by CakePHP in the connection.',
+ "on the same connection, you can't skip tables managed by CakePHP in the connection.",
0,
$e,
);
@@ -161,16 +180,49 @@ public function truncate(string $connection, array $skip = []): void
}
}
+ /**
+ * Collect the migration ids that exist on disk for a group of migration sets
+ * that share a single migration history.
+ *
+ * Returns an empty list for groups of one, where no sibling source exists.
+ *
+ * @param array> $group Migration sets sharing a history.
+ * @return array Migration ids present on disk, keyed by id.
+ */
+ protected function getSiblingMigrationIds(array $group): array
+ {
+ if (count($group) < 2) {
+ return [];
+ }
+
+ $ids = [];
+ foreach ($group as $options) {
+ $migrations = new Migrations();
+ foreach ($migrations->status($options) as $migration) {
+ if ($migration['missing'] ?? false) {
+ continue;
+ }
+ $ids[$migration['id']] = true;
+ }
+ }
+
+ return $ids;
+ }
+
/**
* Detect if migrations have changed and the database needs to be wiped.
*
* @param \Migrations\Migrations $migrations The migrations service.
* @param array $options The connection options.
+ * @param array $siblingMigrationIds Migration ids found in sources sharing the history.
* @return bool
*/
- protected function shouldDropTables(Migrations $migrations, array $options): bool
- {
- Log::write('debug', "Reading migrations status for {$options['connection']}...");
+ protected function shouldDropTables(
+ Migrations $migrations,
+ array $options,
+ array $siblingMigrationIds = [],
+ ): bool {
+ Log::write('debug', sprintf('Reading migrations status for %s...', $options['connection']));
$messages = [
'down' => [],
@@ -178,25 +230,29 @@ protected function shouldDropTables(Migrations $migrations, array $options): boo
];
foreach ($migrations->status($options) as $migration) {
if ($migration['status'] === 'up' && ($migration['missing'] ?? false)) {
+ // The migration belongs to another source sharing this history.
+ if (isset($siblingMigrationIds[$migration['id']])) {
+ continue;
+ }
$messages['missing'][] = 'Applied but, missing Migration ' .
- "source={$migration['name']} id={$migration['id']}";
+ sprintf('source=%s id=%s', $migration['name'], $migration['id']);
}
if ($migration['status'] === 'down') {
- $messages['down'][] = "Migration to reverse. source={$migration['name']} id={$migration['id']}";
+ $messages['down'][] = sprintf('Migration to reverse. source=%s id=%s', $migration['name'], $migration['id']);
}
}
$output = [];
$hasProblems = false;
- $itemize = function ($item) {
+ $itemize = function (string $item): string {
return '- ' . $item;
};
- if (!empty($messages['down'])) {
+ if ($messages['down'] !== []) {
$hasProblems = true;
$output[] = 'Migrations needing to be reversed:';
$output = array_merge($output, array_map($itemize, $messages['down']));
$output[] = '';
}
- if (!empty($messages['missing'])) {
+ if ($messages['missing'] !== []) {
$hasProblems = true;
$output[] = 'Applied but missing migrations:';
$output = array_merge($output, array_map($itemize, $messages['missing']));
@@ -216,7 +272,7 @@ protected function shouldDropTables(Migrations $migrations, array $options): boo
/**
* Drops the regular tables of the provided connection
- * and truncates the phinx tables.
+ * and truncates the migration metadata tables.
*
* @param string $connection Connection on which tables are dropped.
* @param string[] $skip A fnmatch compatible list of tables to skip.
@@ -225,29 +281,29 @@ protected function shouldDropTables(Migrations $migrations, array $options): boo
protected function dropTables(string $connection, array $skip = []): void
{
$dropTables = $this->getNonPhinxTables($connection, $skip);
- if (count($dropTables)) {
+ if ($dropTables !== []) {
$this->helper->dropTables($connection, $dropTables);
}
- $phinxTables = $this->getPhinxTables($connection);
- if (count($phinxTables)) {
- $this->helper->truncateTables($connection, $phinxTables);
+ $migrationTables = $this->getMigrationTables($connection);
+ if ($migrationTables !== []) {
+ $this->helper->truncateTables($connection, $migrationTables);
}
}
/**
- * Get the list of tables that are phinxlog
+ * Get the list of migration metadata tables.
*
* @param string $connection The connection name to operate on.
- * @return string[] The list of tables that are not related to phinx in the provided connection.
+ * @return string[] The list of migration metadata tables in the provided connection.
*/
- protected function getPhinxTables(string $connection): array
+ protected function getMigrationTables(string $connection): array
{
$connection = ConnectionManager::get($connection);
assert($connection instanceof Connection);
$tables = $connection->getSchemaCollection()->listTables();
- return array_filter($tables, function ($table) {
- return strpos($table, 'phinxlog') !== false;
+ return array_filter($tables, function (string $table): bool {
+ return str_contains($table, 'phinxlog') || $table === 'cake_migrations';
});
}
@@ -266,9 +322,9 @@ protected function getNonPhinxTables(string $connection, array $skip): array
$skip[] = '*phinxlog*';
$skip[] = 'cake_migrations';
- return array_filter($tables, function ($table) use ($skip) {
+ return array_filter($tables, function (string $table) use ($skip): bool {
foreach ($skip as $pattern) {
- if (fnmatch($pattern, $table) === true) {
+ if (fnmatch($pattern, $table)) {
return false;
}
}
diff --git a/src/Util/ColumnParser.php b/src/Util/ColumnParser.php
index 97b9efdc0..fbfbafdca 100644
--- a/src/Util/ColumnParser.php
+++ b/src/Util/ColumnParser.php
@@ -16,8 +16,6 @@ class ColumnParser
{
/**
* Regex used to parse the column definition passed through the shell
- *
- * @var string
*/
protected string $regexpParseColumn = '/
^
@@ -36,8 +34,6 @@ class ColumnParser
/**
* Regex used to parse the field type and length
- *
- * @var string
*/
protected string $regexpParseField = '/(\w+\??)\[([0-9,]+)\]/';
@@ -50,6 +46,7 @@ class ColumnParser
public function parseFields(array $arguments): array
{
$fields = [];
+ $primaryKeys = [];
$arguments = $this->validArguments($arguments);
foreach ($arguments as $field) {
preg_match($this->regexpParseColumn, $field, $matches);
@@ -74,7 +71,7 @@ public function parseFields(array $arguments): array
$type = str_contains($type, '?') ? 'integer?' : 'integer';
}
- $nullable = str_contains($type, '?');
+ $nullable = str_contains((string)$type, '?');
$type = $nullable ? str_replace('?', '', $type) : $type;
[$type, $length] = $this->getTypeAndLength($field, $type);
@@ -94,11 +91,23 @@ public function parseFields(array $arguments): array
}
}
- if ($isPrimaryKey === true && $type === 'integer') {
+ if ($isPrimaryKey) {
+ $primaryKeys[] = $field;
+ }
+
+ if ($isPrimaryKey && $type === 'integer') {
$fields[$field]['options']['autoIncrement'] = true;
}
}
+ // A composite primary key (e.g. a join table) must not auto-increment.
+ // Only a single integer primary key column gets auto-increment.
+ if (count($primaryKeys) > 1) {
+ foreach ($primaryKeys as $field) {
+ unset($fields[$field]['options']['autoIncrement']);
+ }
+ }
+
return $fields;
}
@@ -120,15 +129,16 @@ public function parseIndexes(array $arguments): array
$indexName = Hash::get($matches, 5);
// Skip references - they create foreign keys, not indexes
- if ($type && str_starts_with($type, 'references')) {
+ if ($type && str_starts_with((string)$type, 'references')) {
continue;
}
-
- if (
- in_array($type, ['primary', 'primary_key'], true) ||
- in_array($indexType, ['primary', 'primary_key'], true) ||
- $indexType === null
- ) {
+ if (in_array($type, ['primary', 'primary_key'], true)) {
+ continue;
+ }
+ if (in_array($indexType, ['primary', 'primary_key'], true)) {
+ continue;
+ }
+ if ($indexType === null) {
continue;
}
@@ -202,7 +212,7 @@ public function parseForeignKeys(array $arguments): array
$indexName = Hash::get($matches, 5);
// Check if type is 'references' or 'references?'
- $isReference = str_starts_with($type, 'references');
+ $isReference = str_starts_with((string)$type, 'references');
if (!$isReference) {
continue;
}
@@ -213,7 +223,7 @@ public function parseForeignKeys(array $arguments): array
$referencedTable = $indexType;
if (!$referencedTable) {
// Remove common suffixes like _id and pluralize
- $referencedTable = preg_replace('/_id$/', '', $fieldName);
+ $referencedTable = (string)preg_replace('/_id$/', '', $fieldName);
$referencedTable = Inflector::pluralize($referencedTable);
}
@@ -242,7 +252,7 @@ public function validArguments(array $arguments): array
{
$collection = new Collection($arguments);
- return $collection->filter(function ($value, $field) {
+ return $collection->filter(function ($value, $field): int|false {
return preg_match($this->regexpParseColumn, (string)$field);
})->toArray();
}
@@ -259,11 +269,7 @@ public function getTypeAndLength(string $field, ?string $type): array
{
if ($type && preg_match($this->regexpParseField, $type, $matches)) {
$length = $matches[2];
- if (str_contains($length, ',')) {
- $length = array_map('intval', explode(',', $length));
- } else {
- $length = (int)$length;
- }
+ $length = str_contains($length, ',') ? array_map(intval(...), explode(',', $length)) : (int)$length;
return [$matches[1], $length];
}
@@ -287,9 +293,9 @@ public function getType(string $field, ?string $type): ?string
$reflector = new ReflectionClass(AdapterInterface::class);
$collection = new Collection($reflector->getConstants());
- $validTypes = $collection->filter(function ($value, $constant) {
- return substr($constant, 0, strlen('TYPE_')) === 'TYPE_' ||
- substr($constant, 0, strlen('PHINX_TYPE_')) === 'PHINX_TYPE_';
+ $validTypes = $collection->filter(function ($value, $constant): bool {
+ return str_starts_with($constant, 'TYPE_') ||
+ str_starts_with($constant, 'PHINX_TYPE_');
})->toArray();
$fieldType = $type;
if ($type === null || !in_array($type, $validTypes, true)) {
@@ -297,7 +303,7 @@ public function getType(string $field, ?string $type): ?string
$fieldType = 'integer';
} elseif ($field === 'id') {
$fieldType = 'integer';
- } elseif (in_array($field, ['created', 'modified', 'updated'], true) || substr($field, -3) === '_at') {
+ } elseif (in_array($field, ['created', 'modified', 'updated'], true) || str_ends_with($field, '_at')) {
$fieldType = 'datetime';
} elseif (in_array($field, ['latitude', 'longitude', 'lat', 'lng'], true)) {
$fieldType = 'decimal';
@@ -402,12 +408,12 @@ public function parseDefaultValue(?string $value, string $columnType): string|in
}
// Handle integers
- if (preg_match('/^-?[0-9]+$/', $value)) {
+ if (preg_match('/^-?\d+$/', $value)) {
return (int)$value;
}
// Handle floats
- if (preg_match('/^-?[0-9]+\.[0-9]+$/', $value)) {
+ if (preg_match('/^-?\d+\.\d+$/', $value)) {
return (float)$value;
}
diff --git a/src/Util/TableFinder.php b/src/Util/TableFinder.php
index a96b4e03b..27da85d94 100644
--- a/src/Util/TableFinder.php
+++ b/src/Util/TableFinder.php
@@ -17,6 +17,7 @@
use Cake\Core\Plugin as CorePlugin;
use Cake\Database\Schema\CollectionInterface;
use Cake\Datasource\ConnectionManager;
+use Cake\ORM\Association;
use Cake\ORM\TableRegistry;
use ReflectionClass;
@@ -34,8 +35,6 @@ class TableFinder
/**
* Regex of Table name to skip
- *
- * @var string
*/
public string $skipTablesRegex = '_phinxlog';
@@ -82,7 +81,7 @@ public function getTablesToBake(CollectionInterface $collection, array $options
$config = (array)ConnectionManager::getConfig($this->connection);
$key = isset($config['schema']) ? 'schema' : 'database';
- if (isset($split[0], $split[1]) && $config[$key] === $split[1]) {
+ if (isset($split[1]) && $config[$key] === $split[1]) {
$table = $split[0];
}
}
@@ -94,7 +93,7 @@ public function getTablesToBake(CollectionInterface $collection, array $options
$tables = $tableNamesInPlugin;
} else {
foreach ($tables as $num => $table) {
- if (in_array($table, $this->skipTables, true) || (strpos($table, $this->skipTablesRegex) !== false)) {
+ if (in_array($table, $this->skipTables, true) || (str_contains($table, $this->skipTablesRegex))) {
unset($tables[$num]);
continue;
}
@@ -138,17 +137,13 @@ public function getTableNames(?string $pluginName = null): array
public function findTables(?string $pluginName = null): array
{
$path = 'Model' . DS . 'Table' . DS;
- if ($pluginName) {
- $path = CorePlugin::path($pluginName) . 'src' . DS . $path;
- } else {
- $path = APP . $path;
- }
+ $path = $pluginName ? CorePlugin::path($pluginName) . 'src' . DS . $path : APP . $path;
if (!is_dir($path)) {
return [];
}
- return array_map('basename', glob($path . '*.php') ?: []);
+ return array_map(basename(...), glob($path . '*.php') ?: []);
}
/**
@@ -183,9 +178,10 @@ public function fetchTableName(string $className, ?string $pluginName = null): a
$table = TableRegistry::getTableLocator()->get($className);
foreach ($table->associations()->keys() as $key) {
- if ($table->associations()->get($key)->type() === 'belongsToMany') {
+ $association = $table->associations()->get($key);
+ if ($association instanceof Association && $association->type() === 'belongsToMany') {
/** @var \Cake\ORM\Association\BelongsToMany $belongsToMany */
- $belongsToMany = $table->associations()->get($key);
+ $belongsToMany = $association;
$tables[] = $belongsToMany->junction()->getTable();
}
}
@@ -195,7 +191,7 @@ public function fetchTableName(string $className, ?string $pluginName = null): a
$config = ConnectionManager::getConfig($this->connection);
if (is_array($config)) {
$key = isset($config['schema']) ? 'schema' : 'database';
- if (isset($splitted[0]) && $config[$key] === $splitted[1]) {
+ if ($config[$key] === $splitted[1]) {
$tableName = $splitted[0];
}
}
diff --git a/src/Util/Util.php b/src/Util/Util.php
index e8138cd09..cd1517257 100644
--- a/src/Util/Util.php
+++ b/src/Util/Util.php
@@ -12,7 +12,9 @@
use Cake\Utility\Inflector;
use DateTime;
use DateTimeZone;
+use Migrations\Config\ConfigInterface;
use Migrations\Db\Adapter\UnifiedMigrationsTableStorage;
+use Migrations\SeedInterface;
use RuntimeException;
/**
@@ -37,7 +39,7 @@ class Util
* @var string
* @phpstan-var non-empty-string
*/
- protected const MIGRATION_FILE_NAME_NO_NAME_PATTERN = '/^[0-9]{14}\.php$/';
+ protected const MIGRATION_FILE_NAME_NO_NAME_PATTERN = '/^\d{14}\.php$/';
/**
* Enhanced migration file name pattern with readable timestamp and CamelCase
@@ -115,7 +117,7 @@ public static function getVersionFromFileName(string $fileName): int
}
// Traditional format
- preg_match('/^[0-9]+/', $baseName, $matches);
+ preg_match('/^\d+/', $baseName, $matches);
$value = (int)($matches[0] ?? null);
if (!$value) {
throw new RuntimeException(sprintf('Cannot get a valid version from filename `%s`', $fileName));
@@ -134,13 +136,12 @@ public static function getVersionFromFileName(string $fileName): int
*/
public static function mapClassNameToFileName(string $className): string
{
- $snake = function ($matches) {
- return '_' . strtolower($matches[0]);
+ $snake = function ($matches): string {
+ return '_' . strtolower((string)$matches[0]);
};
$fileName = preg_replace_callback('/\d+|[A-Z]/', $snake, $className);
- $fileName = static::getCurrentTimestamp() . "$fileName.php";
- return $fileName;
+ return static::getCurrentTimestamp() . $fileName . '.php';
}
/**
@@ -209,6 +210,46 @@ public static function getSeedDisplayName(string $seedName): string
return $seedName;
}
+ /**
+ * Get the plugin a seed belongs to.
+ *
+ * Seed classes are not namespaced, so the plugin cannot be derived from the class
+ * name. The plugin of the run the seed was loaded in is used instead.
+ *
+ * @param \Migrations\SeedInterface $seed The seed to get the plugin for.
+ * @return string|null The plugin name, or null for application seeds.
+ */
+ public static function getSeedPlugin(SeedInterface $seed): ?string
+ {
+ $config = $seed->getConfig();
+ if (!$config instanceof ConfigInterface || !isset($config['plugin'])) {
+ return null;
+ }
+
+ return (string)$config['plugin'] ?: null;
+ }
+
+ /**
+ * Check whether a seed log entry belongs to the given plugin.
+ *
+ * Seeds executed before plugin attribution was fixed were logged without a plugin.
+ * Those entries are still matched for plugin seeds so that they are not executed twice.
+ * As a trade-off, an application seed sharing its name with a plugin seed can be
+ * matched as well, which is preferred over re-running a seed that already ran.
+ *
+ * @param string|null $entryPlugin The plugin stored in the seed log entry.
+ * @param string|null $plugin The plugin of the seed being checked.
+ * @return bool
+ */
+ public static function matchesSeedPlugin(?string $entryPlugin, ?string $plugin): bool
+ {
+ if ($entryPlugin === $plugin) {
+ return true;
+ }
+
+ return $plugin !== null && $entryPlugin === null;
+ }
+
/**
* Expands a set of paths with curly braces (if supported by the OS).
*
@@ -250,7 +291,7 @@ public static function glob(string $path): array
*/
public static function getFiles(string|array $paths): array
{
- $files = static::globAll(array_map(function ($path) {
+ $files = static::globAll(array_map(function (string $path): string {
return $path . DIRECTORY_SEPARATOR . '*.php';
}, (array)$paths));
// glob() can return the same file multiple times
@@ -263,7 +304,6 @@ public static function getFiles(string|array $paths): array
}
/**
- * @param string|null $plugin
* @return string
*/
public static function tableName(?string $plugin): string
diff --git a/src/Util/UtilTrait.php b/src/Util/UtilTrait.php
index fc3ab3b89..ad49afee5 100644
--- a/src/Util/UtilTrait.php
+++ b/src/Util/UtilTrait.php
@@ -53,7 +53,7 @@ protected function getPhinxTable(?string $plugin = null, ?Connection $connection
}
// Autodetect mode (config is null or not set)
- if ($connection !== null && $this->detectLegacyTables($connection)) {
+ if ($connection instanceof Connection && $this->detectLegacyTables($connection)) {
return $this->getLegacyTableName($plugin);
}
@@ -113,7 +113,7 @@ protected function isUsingLegacyTables(?Connection $connection = null): bool
}
// Autodetect
- if ($connection !== null) {
+ if ($connection instanceof Connection) {
return $this->detectLegacyTables($connection);
}
diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php
index 3302e7e5c..5cc4cd399 100644
--- a/src/View/Helper/MigrationHelper.php
+++ b/src/View/Helper/MigrationHelper.php
@@ -18,11 +18,12 @@
use Cake\Database\Connection;
use Cake\Database\Driver\Mysql;
use Cake\Database\Schema\CollectionInterface;
+use Cake\Database\Schema\TableSchema;
use Cake\Database\Schema\TableSchemaInterface;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;
use Cake\View\Helper;
-use Cake\View\View;
+use Migrations\Db\Adapter\MysqlAdapter;
use Migrations\Db\Table\ForeignKey;
/**
@@ -74,22 +75,6 @@ public function getReturnedData(): array
return $this->returnedData;
}
- /**
- * Constructor
- *
- * ### Settings
- *
- * - `collection` \Cake\Database\Schema\Collection
- * - `connection` \Cake\Database\Connection
- *
- * @param \Cake\View\View $View The View this helper is being attached to.
- * @param array $config Configuration settings for the helper.
- */
- public function __construct(View $View, array $config = [])
- {
- parent::__construct($View, $config);
- }
-
/**
* Returns the method to be used for the Table::save()
*
@@ -204,10 +189,8 @@ public function indexes(TableSchemaInterface|string $table): array
$tableIndexes = $tableSchema->indexes();
$indexes = [];
- if ($tableIndexes) {
- foreach ($tableIndexes as $name) {
- $indexes[$name] = $tableSchema->getIndex($name);
- }
+ foreach ($tableIndexes as $name) {
+ $indexes[$name] = $tableSchema->getIndex($name);
}
return $indexes;
@@ -389,6 +372,8 @@ public function getColumnOption(array $options): array
'scale',
'after',
'collate',
+ 'fixed',
+ 'onUpdate',
]);
$columnOptions = array_intersect_key($options, $wantedOptions);
if (empty($columnOptions['comment'])) {
@@ -400,6 +385,13 @@ public function getColumnOption(array $options): array
if (empty($columnOptions['collate'])) {
unset($columnOptions['collate']);
}
+ if (empty($columnOptions['onUpdate'])) {
+ unset($columnOptions['onUpdate']);
+ }
+ // isset() returns false for null values, so this handles both missing and null cases
+ if (!isset($columnOptions['fixed'])) {
+ unset($columnOptions['fixed']);
+ }
// currently only MySQL supports the signed option
$driver = $connection->getDriver();
@@ -418,6 +410,12 @@ public function getColumnOption(array $options): array
unset($columnOptions['collate']);
}
+ if (!empty($columnOptions['onUpdate'])) {
+ // Phinx uses 'update' not 'onUpdate'
+ $columnOptions['update'] = $columnOptions['onUpdate'];
+ unset($columnOptions['onUpdate']);
+ }
+
// Handle precision/scale conversion between CakePHP's TableSchema format and SQL standard format.
// TableSchema uses: length=total digits, precision=decimal places
// Migrations uses SQL standard: precision=total digits, scale=decimal places
@@ -440,6 +438,13 @@ public function getColumnOption(array $options): array
}
}
+ // Convert CakePHP's LENGTH_LONG to migrations TEXT_LONG for text columns
+ // CakePHP uses LENGTH_LONG = 4294967295, but migrations expects TEXT_LONG = 2147483647
+ // (LENGTH_TINY and LENGTH_MEDIUM have the same values as TEXT_TINY and TEXT_MEDIUM)
+ if (isset($columnOptions['limit']) && $columnOptions['limit'] === TableSchema::LENGTH_LONG) {
+ $columnOptions['limit'] = MysqlAdapter::TEXT_LONG;
+ }
+
return $columnOptions;
}
@@ -452,7 +457,7 @@ public function getColumnOption(array $options): array
*/
public function value(string|float|int|bool|null $value, bool $numbersAsString = false): string|float
{
- if ($value === null || $value === 'null' || $value === 'NULL') {
+ if (in_array($value, [null, 'null', 'NULL'], true)) {
return 'null';
}
@@ -495,7 +500,8 @@ public function attributes(TableSchemaInterface|string $table, string $column):
'comment', 'unsigned',
'signed', 'properties',
'autoIncrement', 'unique',
- 'collate',
+ 'collate', 'fixed',
+ 'onUpdate',
];
$attributes = [];
@@ -545,6 +551,10 @@ public function attributes(TableSchemaInterface|string $table, string $column):
unset($attributes['collate']);
}
+ if (empty($attributes['onUpdate'])) {
+ unset($attributes['onUpdate']);
+ }
+
ksort($attributes);
return $attributes;
@@ -593,7 +603,7 @@ public function stringifyList(array $list, array $options = [], array $wantedOpt
$v = $this->value($v, $k === 'default');
}
if (!is_numeric($k)) {
- $v = "'$k' => $v";
+ $v = sprintf("'%s' => %s", $k, $v);
}
}
@@ -618,14 +628,14 @@ public function stringifyList(array $list, array $options = [], array $wantedOpt
*/
public function tableStatement(string $table, bool $reset = false): string
{
- if ($reset === true) {
+ if ($reset) {
unset($this->tableStatementStatus[$table]);
}
if (!isset($this->tableStatementStatus[$table])) {
$this->tableStatementStatus[$table] = true;
- return '$this->table(\'' . addslashes($table) . '\')';
+ return '$this->table(\'' . addslashes($table) . "')";
}
return '';
@@ -692,7 +702,7 @@ public function getCreateTableData(TableSchemaInterface|string $table): array
$indexes = $this->indexes($table);
$foreignKeys = [];
foreach ($constraints as $constraint) {
- if ($constraint['type'] === 'foreign') {
+ if (isset($constraint['type']) && $constraint['type'] === 'foreign') {
$foreignKeys[] = $constraint['columns'];
}
}
@@ -713,14 +723,11 @@ public function getCreateTablesElementData(array $tables): array
'tables' => [],
];
foreach ($tables as $table) {
- $tableName = $table;
- if ($table instanceof TableSchemaInterface) {
- $tableName = $table->name();
- }
+ $tableName = $table instanceof TableSchemaInterface ? $table->name() : $table;
$data = $this->getCreateTableData($table);
$tableConstraintsNoUnique = array_filter(
$data['constraints'],
- function ($constraint) {
+ function (array $constraint): bool {
return $constraint['type'] !== 'unique';
},
);
diff --git a/templates/bake/Seed/seed-anonymous.twig b/templates/bake/Seed/seed-anonymous.twig
index b4e0bbaf4..207bce9d8 100644
--- a/templates/bake/Seed/seed-anonymous.twig
+++ b/templates/bake/Seed/seed-anonymous.twig
@@ -26,7 +26,7 @@ return new class extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/templates/bake/Seed/seed.twig b/templates/bake/Seed/seed.twig
index 17b079d47..34c144787 100644
--- a/templates/bake/Seed/seed.twig
+++ b/templates/bake/Seed/seed.twig
@@ -29,7 +29,7 @@ class {{ name }}Seed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/templates/bake/config/diff.twig b/templates/bake/config/diff.twig
index 20514d2a2..da5b7e6e1 100644
--- a/templates/bake/config/diff.twig
+++ b/templates/bake/config/diff.twig
@@ -20,8 +20,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class {{ name }} extends BaseMigration
+class {{ name }} extends BaseMigration implements DirectionalMigrationInterface
{
{% if not autoId %}
public bool $autoId = false;
@@ -31,7 +32,7 @@ class {{ name }} extends BaseMigration
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -118,7 +119,7 @@ not empty %}
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/templates/bake/config/skeleton-anonymous.twig b/templates/bake/config/skeleton-anonymous.twig
index 1d19b868a..560be4723 100644
--- a/templates/bake/config/skeleton-anonymous.twig
+++ b/templates/bake/config/skeleton-anonymous.twig
@@ -21,8 +21,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-return new class extends BaseMigration
+return new class extends BaseMigration implements ReversibleMigrationInterface
{
{% if tableMethod == 'create' and columns['primaryKey'] %}
public bool $autoId = false;
@@ -32,7 +33,7 @@ return new class extends BaseMigration
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
* @return void
*/
public function change(): void
diff --git a/templates/bake/config/skeleton.twig b/templates/bake/config/skeleton.twig
index 745aeb8d3..a13115735 100644
--- a/templates/bake/config/skeleton.twig
+++ b/templates/bake/config/skeleton.twig
@@ -21,8 +21,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class {{ name }} extends BaseMigration
+class {{ name }} extends BaseMigration implements ReversibleMigrationInterface
{
{% if tableMethod == 'create' and columns['primaryKey'] %}
public bool $autoId = false;
@@ -32,7 +33,7 @@ class {{ name }} extends BaseMigration
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
diff --git a/templates/bake/config/snapshot.twig b/templates/bake/config/snapshot.twig
index 42296d4e5..b40b40bd3 100644
--- a/templates/bake/config/snapshot.twig
+++ b/templates/bake/config/snapshot.twig
@@ -23,8 +23,13 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+{% if useChange %}
+use Migrations\ReversibleMigrationInterface;
+{% else %}
+use Migrations\DirectionalMigrationInterface;
+{% endif %}
-class {{ name }} extends BaseMigration
+class {{ name }} extends BaseMigration implements {{ useChange ? 'ReversibleMigrationInterface' : 'DirectionalMigrationInterface' }}
{
{% if not autoId %}
public bool $autoId = false;
@@ -35,7 +40,7 @@ class {{ name }} extends BaseMigration
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
@@ -52,7 +57,7 @@ class {{ name }} extends BaseMigration
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -69,7 +74,7 @@ class {{ name }} extends BaseMigration
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/templates/bake/element/add-foreign-keys.twig b/templates/bake/element/add-foreign-keys.twig
index 774cf80d8..9f67c0e5c 100644
--- a/templates/bake/element/add-foreign-keys.twig
+++ b/templates/bake/element/add-foreign-keys.twig
@@ -28,8 +28,8 @@
$this->foreignKey({{ columnsList | raw }})
->setReferencedTable('{{ constraint['references'][0] }}')
->setReferencedColumns({{ columnsReference | raw }})
- ->setOnDelete('{{ Migration.formatConstraintAction(constraint['delete']) | raw }}')
- ->setOnUpdate('{{ Migration.formatConstraintAction(constraint['update']) | raw }}')
+ ->setDelete('{{ Migration.formatConstraintAction(constraint['delete']) | raw }}')
+ ->setUpdate('{{ Migration.formatConstraintAction(constraint['update']) | raw }}')
->setName('{{ constraintName }}')
)
{%~ endif %}
diff --git a/templates/bake/element/change-method-body.twig b/templates/bake/element/change-method-body.twig
index e1370cd4f..e36e7fb44 100644
--- a/templates/bake/element/change-method-body.twig
+++ b/templates/bake/element/change-method-body.twig
@@ -59,8 +59,8 @@
$this->foreignKey({{ columnsList | raw }})
->setReferencedTable('{{ constraint['references'][0] }}')
->setReferencedColumns({{ columnsReference | raw }})
- ->setOnDelete('{{ Migration.formatConstraintAction(constraint['delete']) | raw }}')
- ->setOnUpdate('{{ Migration.formatConstraintAction(constraint['update']) | raw }}')
+ ->setDelete('{{ Migration.formatConstraintAction(constraint['delete']) | raw }}')
+ ->setUpdate('{{ Migration.formatConstraintAction(constraint['update']) | raw }}')
->setName('{{ constraintName }}')
);
{%~ endif %}
diff --git a/tests/TestCase/Command/BakeMigrationCommandTest.php b/tests/TestCase/Command/BakeMigrationCommandTest.php
index 5e525e50a..aa1fd0f63 100644
--- a/tests/TestCase/Command/BakeMigrationCommandTest.php
+++ b/tests/TestCase/Command/BakeMigrationCommandTest.php
@@ -33,14 +33,14 @@ class BakeMigrationCommandTest extends TestCase
*
* @return void
*/
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
$this->_compareBasePath = Plugin::path('Migrations') . 'tests' . DS . 'comparisons' . DS . 'Migration' . DS;
}
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
$files = glob(ROOT . DS . 'config' . DS . 'Migrations' . DS . '*Users.php');
@@ -84,7 +84,7 @@ public function tearDown(): void
*
* @return void
*/
- public function testNoContents()
+ public function testNoContents(): void
{
$this->exec('bake migration NoContents --connection test');
@@ -101,7 +101,7 @@ public function testNoContents()
*
* @return array
*/
- public static function nameVariations()
+ public static function nameVariations(): array
{
return [
['name', '.php'],
@@ -118,9 +118,9 @@ public static function nameVariations()
* @return void
*/
#[DataProvider('nameVariations')]
- public function testCreate($name, $fileSuffix)
+ public function testCreate(string $name, string $fileSuffix): void
{
- $this->exec("bake migration CreateUsers {$name} --connection test");
+ $this->exec(sprintf('bake migration CreateUsers %s --connection test', $name));
$file = glob(ROOT . DS . 'config' . DS . 'Migrations' . DS . '*_CreateUsers.php');
$filePath = current($file);
@@ -133,7 +133,7 @@ public function testCreate($name, $fileSuffix)
/**
* Tests that baking a migration with the name as another will throw an exception.
*/
- public function testCreateDuplicateName()
+ public function testCreateDuplicateName(): void
{
$this->exec('bake migration CreateUsers --connection test');
$this->exec('bake migration CreateUsers --connection test');
@@ -144,7 +144,7 @@ public function testCreateDuplicateName()
/**
* Tests that baking a migration with the name as reserved keyword triggers prefixing.
*/
- public function testCreateWithReservedKeyword()
+ public function testCreateWithReservedKeyword(): void
{
$this->exec('bake migration New --connection test', ['Prefix']);
@@ -152,7 +152,7 @@ public function testCreateWithReservedKeyword()
$this->assertOutputRegExp('/Wrote.*?PrefixNew\.php/');
}
- public function testCreateBuiltInAlias()
+ public function testCreateBuiltInAlias(): void
{
$this->exec('migrations create CreateUsers --connection test');
$this->assertExitCode(BaseCommand::CODE_SUCCESS);
@@ -162,7 +162,7 @@ public function testCreateBuiltInAlias()
/**
* Tests that baking a migration with the "drop" string inside the name generates a valid drop migration.
*/
- public function testCreateDropMigration()
+ public function testCreateDropMigration(): void
{
$this->exec('bake migration DropUsers --connection test');
$this->assertOutputRegExp('/Wrote.*?DropUsers\.php/');
@@ -178,7 +178,7 @@ public function testCreateDropMigration()
/**
* Tests that baking a migration with the name as another with the parameter "force", will delete the existing file.
*/
- public function testCreateDuplicateNameWithForce()
+ public function testCreateDuplicateNameWithForce(): void
{
$this->exec('bake migration CreateUsers --connection test --force');
@@ -197,7 +197,7 @@ public function testCreateDuplicateNameWithForce()
*
* @return void
*/
- public function testAddPrimaryKeyToExistingTable()
+ public function testAddPrimaryKeyToExistingTable(): void
{
$this->exec('bake migration AddPkToUsers somefield:primary_key --connection test');
@@ -211,7 +211,7 @@ public function testAddPrimaryKeyToExistingTable()
*
* @return void
*/
- public function testAddPrimaryKeyToExistingUsersTable()
+ public function testAddPrimaryKeyToExistingUsersTable(): void
{
$this->exec('bake migration AlterUsers somefield:primary_key --connection test');
@@ -222,7 +222,7 @@ public function testAddPrimaryKeyToExistingUsersTable()
/**
* @return void
*/
- public function testDetectAction()
+ public function testDetectAction(): void
{
$command = new BakeMigrationCommand();
$this->assertEquals(
@@ -382,7 +382,7 @@ public function testDetectAction()
*
* @return void
*/
- public function testActionWithoutValidPrefix()
+ public function testActionWithoutValidPrefix(): void
{
$this->exec('bake migration SleepUsers name:string --connection test');
@@ -395,7 +395,7 @@ public function testActionWithoutValidPrefix()
*
* @return void
*/
- public function testCreateAnonymousStyle()
+ public function testCreateAnonymousStyle(): void
{
$this->exec('bake migration CreateUsers name:string --style=anonymous --connection test');
@@ -403,7 +403,7 @@ public function testCreateAnonymousStyle()
$this->assertCount(1, $files);
$filePath = current($files);
- $fileName = basename($filePath);
+ $fileName = basename((string)$filePath);
// Check the file name format
$this->assertMatchesRegularExpression('/^\d{4}_\d{2}_\d{2}_\d{6}_CreateUsers\.php$/', $fileName);
@@ -422,7 +422,7 @@ public function testCreateAnonymousStyle()
*
* @return void
*/
- public function testCreateAnonymousStyleWithConfigure()
+ public function testCreateAnonymousStyleWithConfigure(): void
{
Configure::write('Migrations.style', 'anonymous');
@@ -432,7 +432,7 @@ public function testCreateAnonymousStyleWithConfigure()
$this->assertCount(1, $files);
$filePath = current($files);
- $fileName = basename($filePath);
+ $fileName = basename((string)$filePath);
// Check the file name format
$this->assertMatchesRegularExpression('/^\d{4}_\d{2}_\d{2}_\d{6}_CreateUsers\.php$/', $fileName);
@@ -451,7 +451,7 @@ public function testCreateAnonymousStyleWithConfigure()
*
* @return void
*/
- public function testCreateWithReferences()
+ public function testCreateWithReferences(): void
{
$this->exec('bake migration CreatePosts title:string user_id:references --connection test');
@@ -468,7 +468,7 @@ public function testCreateWithReferences()
*
* @return void
*/
- public function testCreateWithReferencesCustomTable()
+ public function testCreateWithReferencesCustomTable(): void
{
$this->exec('bake migration CreateArticles title:string author_id:references:authors --connection test');
@@ -485,7 +485,7 @@ public function testCreateWithReferencesCustomTable()
*
* @return void
*/
- public function testAddFieldWithReference()
+ public function testAddFieldWithReference(): void
{
$this->exec('bake migration AddCategoryIdToProducts category_id:references --connection test');
@@ -497,10 +497,10 @@ public function testAddFieldWithReference()
$this->assertSameAsFile(__FUNCTION__ . '.php', $result);
}
- public function testBakeMigrationWithoutBake()
+ public function testBakeMigrationWithoutBake(): void
{
// Make sure to unload the Bake plugin
- $this->createApp()->getEventManager()->on('Console.buildCommands', function ($event, $commands) {
+ $this->createApp()->getEventManager()->on('Console.buildCommands', function ($event, $commands): void {
Plugin::getCollection()->remove('Bake');
});
diff --git a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php
index 2dc4d388a..0309c92af 100644
--- a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php
+++ b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php
@@ -49,7 +49,7 @@ class BakeMigrationDiffCommandTest extends TestCase
*
* @return void
*/
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -82,13 +82,13 @@ public function setUp(): void
try {
$connection = ConnectionManager::get('test_comparisons');
$connection->execute('DROP TABLE IF EXISTS test_decimal_types');
- } catch (Exception $e) {
+ } catch (Exception) {
// Ignore errors if connection doesn't exist yet
}
}
}
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
foreach ($this->generatedFiles as $file) {
@@ -121,7 +121,7 @@ public function tearDown(): void
$connection = ConnectionManager::get('test_comparisons');
$tables = ['articles', 'categories', 'comments', 'users', 'orphan_table', 'phinxlog', 'cake_migrations', 'tags', 'test_blog_phinxlog', 'test_decimal_types'];
foreach ($tables as $table) {
- $connection->execute("DROP TABLE IF EXISTS $table");
+ $connection->execute('DROP TABLE IF EXISTS ' . $table);
}
Cache::clear('_cake_model_');
}
@@ -132,7 +132,7 @@ public function tearDown(): void
*
* @return void
*/
- public function testHistoryNotInSync()
+ public function testHistoryNotInSync(): void
{
$expectedMessage = 'Your migrations history is not in sync with your migrations files. ' .
'Make sure all your migrations have been migrated before baking a diff.';
@@ -142,13 +142,60 @@ public function testHistoryNotInSync()
$this->assertExitCode(BaseCommand::CODE_ERROR);
}
+ /**
+ * Tests that baking a diff succeeds when app migrations are in sync
+ * even if a plugin has more recent migrations.
+ *
+ * This test specifically tests the unified table mode where all migrations
+ * (app and plugins) are stored in a single cake_migrations table.
+ *
+ * @see https://github.com/cakephp/migrations/issues/1060
+ * @return void
+ */
+ public function testCheckSyncWithPluginMigrationsMoreRecent(): void
+ {
+ // This bug only affects unified table mode
+ Configure::write('Migrations.legacyTables', false);
+
+ // Run app migrations first to get them in sync
+ $this->exec('migrations migrate -c test --no-lock');
+ $this->assertExitSuccess();
+
+ // Create a schema dump file (required for baking a diff)
+ $this->exec('migrations dump -c test');
+ $this->assertExitSuccess();
+
+ // Insert a more recent migration record for a plugin
+ // This simulates having a plugin migration that was run after the last app migration
+ $this->insertMigrationRecord('test', 99999999999999, 'PluginMigration', 'SomePlugin');
+
+ // Now try to bake a diff for the app - this should succeed
+ // because all app migration files have been migrated
+ $this->exec('bake migration_diff CheckSyncTest -c test');
+
+ // Should not contain the "not in sync" error
+ $this->assertOutputNotContains('Your migrations history is not in sync');
+ $this->assertExitSuccess();
+
+ // Clean up generated files
+ $path = ROOT . DS . 'config' . DS . 'Migrations' . DS;
+ $this->generatedFiles = array_merge(
+ $this->generatedFiles,
+ glob($path . '*_CheckSyncTest.php') ?: [],
+ );
+ $this->generatedFiles[] = $path . 'schema-dump-test.lock';
+
+ // Restore legacy table mode
+ Configure::delete('Migrations.legacyTables');
+ }
+
/**
* Tests that baking a diff while history is empty and no migration files exists
* will fall back to baking a snapshot
*
* @return void
*/
- public function testEmptyHistoryNoMigrations()
+ public function testEmptyHistoryNoMigrations(): void
{
$this->exec('bake migration_diff EmptyHistoryNoMigrations -c test -p Blog');
@@ -168,7 +215,7 @@ public function testEmptyHistoryNoMigrations()
*
* @return void
*/
- public function testBakeMigrationDiffInCustomFolder()
+ public function testBakeMigrationDiffInCustomFolder(): void
{
$customFolderName = 'CustomMigrationsFolder';
$this->exec('bake migration_diff MigrationDiffForCustomFolder -c test -s ' . $customFolderName);
@@ -190,7 +237,7 @@ public function testBakeMigrationDiffInCustomFolder()
*
* @return void
*/
- public function testBakeMigrationDiffGenerateOnly()
+ public function testBakeMigrationDiffGenerateOnly(): void
{
//$this->skipIf(!env('DB_URL_COMPARE'));
@@ -229,7 +276,7 @@ public function testBakeMigrationDiffGenerateOnly()
*
* @return void
*/
- public function testBakingDiff()
+ public function testBakingDiff(): void
{
$this->skipIf(!env('DB_URL_COMPARE'));
@@ -245,7 +292,7 @@ public function testBakingDiff()
*
* @return void
*/
- public function testBakingDiffSimple()
+ public function testBakingDiffSimple(): void
{
$this->skipIf(!env('DB_URL_COMPARE'));
@@ -258,7 +305,7 @@ public function testBakingDiffSimple()
*
* @return void
*/
- public function testBakingDiffAddRemove()
+ public function testBakingDiffAddRemove(): void
{
$this->skipIf(!env('DB_URL_COMPARE'));
@@ -415,11 +462,11 @@ protected function runDiffBakingTest(string $scenario): void
$diffMigrationsPath = $diffConfigFolder . $prefix . Inflector::underscore($scenario) . '_' . $db . '.php';
$diffDumpPath = $diffConfigFolder . 'schema-dump-test_comparisons_' . $db . '.lock';
- $destinationConfigDir = ROOT . DS . 'config' . DS . "MigrationsDiff{$scenario}" . DS;
+ $destinationConfigDir = ROOT . DS . 'config' . DS . 'MigrationsDiff' . $scenario . DS;
if (!is_dir($destinationConfigDir)) {
mkdir($destinationConfigDir, 0777, true);
}
- $destination = $destinationConfigDir . "20160415220805_{$classPrefix}{$scenario}" . ucfirst($db) . '.php';
+ $destination = $destinationConfigDir . sprintf('20160415220805_%s%s', $classPrefix, $scenario) . ucfirst($db) . '.php';
$destinationDumpPath = $destinationConfigDir . 'schema-dump-test_comparisons_' . $db . '.lock';
copy($diffMigrationsPath, $destination);
@@ -428,7 +475,7 @@ protected function runDiffBakingTest(string $scenario): void
$destinationDumpPath,
];
- $migrations = $this->getMigrations("MigrationsDiff$scenario");
+ $migrations = $this->getMigrations('MigrationsDiff' . $scenario);
$migrations->migrate();
copy($diffDumpPath, $destinationDumpPath);
@@ -446,14 +493,14 @@ protected function runDiffBakingTest(string $scenario): void
$this->_compareBasePath = Plugin::path('Migrations') . 'tests' . DS . 'comparisons' . DS . 'Diff' . DS . lcfirst($scenario) . DS;
- $bakeName = $this->getBakeName("TheDiff{$scenario}");
- $targetFolder = "MigrationsDiff{$scenario}";
+ $bakeName = $this->getBakeName('TheDiff' . $scenario);
+ $targetFolder = 'MigrationsDiff' . $scenario;
$comparison = lcfirst($scenario);
- $this->exec("custom bake migration_diff {$bakeName} -c test_comparisons --test-target-folder {$targetFolder} --comparison {$comparison}");
+ $this->exec(sprintf('custom bake migration_diff %s -c test_comparisons --test-target-folder %s --comparison %s', $bakeName, $targetFolder, $comparison));
$this->generatedFiles[] = ROOT . DS . 'config' . DS . 'Migrations' . DS . 'schema-dump-test_comparisons.lock';
- $generatedMigration = $this->getGeneratedMigrationName($destinationConfigDir, "*TheDiff$scenario*");
+ $generatedMigration = $this->getGeneratedMigrationName($destinationConfigDir, sprintf('*TheDiff%s*', $scenario));
$fileName = pathinfo($generatedMigration, PATHINFO_FILENAME);
$this->assertOutputContains('Marking the migration ' . $fileName . ' as migrated...');
$this->assertOutputContains('Creating a dump of the new database state...');
@@ -478,7 +525,7 @@ protected function runDiffBakingTest(string $scenario): void
->into($schemaTable)
->values($values)
->execute();
- $this->getMigrations("MigrationsDiff{$scenario}")->rollback(['target' => 'all']);
+ $this->getMigrations('MigrationsDiff' . $scenario)->rollback(['target' => 'all']);
}
/**
@@ -490,14 +537,17 @@ protected function getDbType(): string
{
$connection = ConnectionManager::get('test_comparisons');
$driver = $connection->getDriver();
-
if ($driver instanceof Mysql) {
return 'mysql';
- } elseif ($driver instanceof Postgres) {
+ }
+ if ($driver instanceof Postgres) {
return 'pgsql';
- } elseif ($driver instanceof Sqlite) {
+ }
+ if ($driver instanceof Sqlite) {
return 'sqlite';
- } elseif ($driver instanceof Sqlserver) {
+ }
+
+ if ($driver instanceof Sqlserver) {
return 'sqlserver';
}
@@ -510,15 +560,14 @@ protected function getDbType(): string
* @param string $name Name of the baked file, unaware of the DB environment
* @return string Baked filename
*/
- public function getBakeName($name)
+ public function getBakeName(string $name): string
{
$db = getenv('DB');
if (!$db) {
$db = $this->getDbType();
}
- $name .= ucfirst($db);
- return $name;
+ return $name . ucfirst($db);
}
/**
@@ -528,15 +577,14 @@ public function getBakeName($name)
* @param string $source Source folder where migrations are located
* @return Migrations
*/
- protected function getMigrations($source = 'MigrationsDiff')
+ protected function getMigrations($source = 'MigrationsDiff'): Migrations
{
$params = [
'connection' => 'test_comparisons',
'source' => $source,
];
- $migrations = new Migrations($params);
- return $migrations;
+ return new Migrations($params);
}
/**
@@ -561,7 +609,7 @@ public function assertSameAsFile(string $path, string $result): void
* @param string $result Results generated by the test to be compared
* @return void
*/
- public function assertCorrectSnapshot($bakeName, $result)
+ public function assertCorrectSnapshot($bakeName, string $result): void
{
$dbenv = getenv('DB');
if (!$dbenv) {
@@ -582,10 +630,10 @@ public function assertCorrectSnapshot($bakeName, $result)
* @param string $needle The filename pattern to find.
* @return string
*/
- public function getGeneratedMigrationName($configDir, $needle)
+ public function getGeneratedMigrationName(string $configDir, string $needle): string
{
$files = glob($configDir . $needle);
- $this->assertNotEmpty($files, "Could not find any files matching `{$needle}` in `{$configDir}`");
+ $this->assertNotEmpty($files, sprintf('Could not find any files matching `%s` in `%s`', $needle, $configDir));
// Record the generated file so we can cleanup if the test fails.
$this->generatedFiles[] = $files[0];
diff --git a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php
index d90980c7c..a703f7a5d 100644
--- a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php
+++ b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php
@@ -52,7 +52,7 @@ class BakeMigrationSnapshotCommandTest extends TestCase
*
* @return void
*/
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -68,7 +68,7 @@ public function setUp(): void
*
* @return void
*/
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
ConnectionManager::drop('alternative');
@@ -85,7 +85,7 @@ public function tearDown(): void
*
* @return void
*/
- public function testNotEmptySnapshot()
+ public function testNotEmptySnapshot(): void
{
$this->runSnapshotTest('NotEmpty');
}
@@ -95,10 +95,10 @@ public function testNotEmptySnapshot()
*
* @return void
*/
- public function testNotEmptySnapshotNoLock()
+ public function testNotEmptySnapshotNoLock(): void
{
$bakeName = $this->getBakeName('TestNotEmptySnapshot');
- $this->exec("bake migration_snapshot {$bakeName} -c test --no-lock");
+ $this->exec(sprintf('bake migration_snapshot %s -c test --no-lock', $bakeName));
$generatedMigration = glob($this->migrationPath . '*_TestNotEmptySnapshot*.php');
$this->generatedFiles = $generatedMigration;
@@ -115,14 +115,14 @@ public function testNotEmptySnapshotNoLock()
*
* @return void
*/
- public function testSnapshotGenerateOnly()
+ public function testSnapshotGenerateOnly(): void
{
if (file_exists($this->migrationPath . 'schema-dump-test.lock')) {
unlink($this->migrationPath . 'schema-dump-test.lock');
}
$bakeName = $this->getBakeName('TestGenerateOnlySnapshot');
- $this->exec("bake migration_snapshot {$bakeName} -c test --generate-only");
+ $this->exec(sprintf('bake migration_snapshot %s -c test --generate-only', $bakeName));
$generatedMigration = glob($this->migrationPath . '*_TestGenerateOnlySnapshot*.php');
$this->generatedFiles = $generatedMigration;
@@ -150,12 +150,12 @@ public function testSnapshotPostgresTimestampTzColumn(): void
$scenario = 'PostgresTimestampTz';
- $bakeName = $this->getBakeName("TestSnapshot{$scenario}");
- $this->exec("bake migration_snapshot {$bakeName} -c test");
+ $bakeName = $this->getBakeName('TestSnapshot' . $scenario);
+ $this->exec(sprintf('bake migration_snapshot %s -c test', $bakeName));
$connection->execute('DROP TABLE postgres_timestamp_tz');
- $generatedMigration = glob($this->migrationPath . "*_TestSnapshot{$scenario}*.php");
+ $generatedMigration = glob($this->migrationPath . sprintf('*_TestSnapshot%s*.php', $scenario));
$this->generatedFiles = $generatedMigration;
$this->generatedFiles[] = $this->migrationPath . 'schema-dump-test.lock';
@@ -172,7 +172,7 @@ public function testSnapshotPostgresTimestampTzColumn(): void
*
* @return void
*/
- public function testAutoIdDisabledSnapshot()
+ public function testAutoIdDisabledSnapshot(): void
{
$this->runSnapshotTest('AutoIdDisabled', '--disable-autoid');
}
@@ -182,7 +182,7 @@ public function testAutoIdDisabledSnapshot()
*
* @return void
*/
- public function testSnapshotWithChange()
+ public function testSnapshotWithChange(): void
{
$this->runSnapshotTest('WithChange', '--change');
}
@@ -247,7 +247,7 @@ public function testSnapshotDiffWithAutoIdIncompatibleUnsignedPrimaryKeys(): voi
*
* @return void
*/
- public function testPluginBlog()
+ public function testPluginBlog(): void
{
$this->loadPlugins(['TestBlog']);
$this->migrationPath = ROOT . DS . 'Plugin' . DS . 'TestBlog' . DS . 'config' . DS . 'Migrations' . DS;
@@ -263,13 +263,13 @@ public function testPluginBlog()
*
* @return void
*/
- public function testPluginWithCustomConnection()
+ public function testPluginWithCustomConnection(): void
{
$this->loadPlugins(['SimpleSnapshot']);
$this->migrationPath = ROOT . DS . 'Plugin' . DS . 'SimpleSnapshot' . DS . 'config' . DS . 'Migrations' . DS;
$bakeName = $this->getBakeName('TestSnapshotPluginCustomConnection');
- $this->exec("bake migration_snapshot {$bakeName} -c test -p SimpleSnapshot");
+ $this->exec(sprintf('bake migration_snapshot %s -c test -p SimpleSnapshot', $bakeName));
$generatedMigration = glob($this->migrationPath . '*_TestSnapshotPluginCustomConnection*.php');
$this->generatedFiles = $generatedMigration;
@@ -286,13 +286,13 @@ public function testPluginWithCustomConnection()
protected function runSnapshotTest(string $scenario, string $arguments = ''): void
{
if ($arguments) {
- $arguments = " $arguments";
+ $arguments = ' ' . $arguments;
}
- $bakeName = $this->getBakeName("TestSnapshot{$scenario}");
- $this->exec("bake migration_snapshot {$bakeName} -c test{$arguments}");
+ $bakeName = $this->getBakeName('TestSnapshot' . $scenario);
+ $this->exec(sprintf('bake migration_snapshot %s -c test%s', $bakeName, $arguments));
- $generatedMigration = glob($this->migrationPath . "*_TestSnapshot{$scenario}*.php");
+ $generatedMigration = glob($this->migrationPath . sprintf('*_TestSnapshot%s*.php', $scenario));
$this->generatedFiles = $generatedMigration;
$this->generatedFiles[] = $this->migrationPath . 'schema-dump-test.lock';
@@ -310,7 +310,7 @@ protected function runSnapshotTest(string $scenario, string $arguments = ''): vo
* @param string $name Name of the baked file, unaware of the DB environment
* @return string Baked filename
*/
- public function getBakeName($name)
+ public function getBakeName(string $name)
{
$dbenv = getenv('DB');
if ($dbenv !== 'mysql') {
@@ -348,6 +348,7 @@ public function assertSameAsFile(string $path, string $result): void
// Normalize utf8mb3 to utf8 for MySQL 8.0.30+ compatibility
$expected = str_replace('utf8mb3_', 'utf8_', $expected);
+
$result = str_replace('utf8mb3_', 'utf8_', $result);
// Normalize unified table name to legacy for comparison
@@ -363,7 +364,7 @@ public function assertSameAsFile(string $path, string $result): void
* @param string $result Results generated by the test to be compared
* @return void
*/
- public function assertCorrectSnapshot($bakeName, $result)
+ public function assertCorrectSnapshot($bakeName, string $result): void
{
$dbenv = getenv('DB');
$bakeName = Inflector::underscore($bakeName);
@@ -391,4 +392,25 @@ public function testSnapshotWithNonDefaultCollation(): void
$this->runSnapshotTest('WithNonDefaultCollation', '-p SimpleSnapshot');
$connection->execute('ALTER TABLE events MODIFY title VARCHAR(255)');
}
+
+ /**
+ * Tests that an ON UPDATE clause is kept in the initial snapshot.
+ */
+ public function testSnapshotWithOnUpdate(): void
+ {
+ $this->skipIf(env('DB') !== 'mysql');
+ $this->loadPlugins(['SimpleSnapshot']);
+
+ $this->migrationPath = ROOT . DS . 'Plugin' . DS . 'SimpleSnapshot' . DS . 'config' . DS . 'Migrations' . DS;
+
+ $connection = ConnectionManager::get('test');
+ assert($connection instanceof Connection);
+
+ $connection->execute('ALTER TABLE users MODIFY updated TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP');
+ try {
+ $this->runSnapshotTest('WithOnUpdate', '-p SimpleSnapshot');
+ } finally {
+ $connection->execute('ALTER TABLE users MODIFY updated TIMESTAMP NULL DEFAULT NULL');
+ }
+ }
}
diff --git a/tests/TestCase/Command/BakeSeedCommandTest.php b/tests/TestCase/Command/BakeSeedCommandTest.php
index a44d2c514..f28285ee6 100644
--- a/tests/TestCase/Command/BakeSeedCommandTest.php
+++ b/tests/TestCase/Command/BakeSeedCommandTest.php
@@ -46,7 +46,7 @@ class BakeSeedCommandTest extends TestCase
*
* @return void
*/
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
$this->_compareBasePath = Plugin::path('Migrations') . 'tests' . DS . 'comparisons' . DS . 'Seeds' . DS;
@@ -57,7 +57,7 @@ public function setUp(): void
*
* @return void
*/
- public function testBasicBaking()
+ public function testBasicBaking(): void
{
$this->generatedFile = ROOT . DS . 'config/Seeds/ArticlesSeed.php';
$this->exec('bake seed Articles --connection test');
@@ -72,7 +72,7 @@ public function testBasicBaking()
*
* @return void
*/
- public function testBakeWithApplicationTemplate()
+ public function testBakeWithApplicationTemplate(): void
{
copy(
ROOT . '/App/Template/plugin/Migrations/bake/Seed/custom-seed.twig',
@@ -93,13 +93,13 @@ public function testBakeWithApplicationTemplate()
*
* @return void
*/
- public function testWithData()
+ public function testWithData(): void
{
$this->generatedFile = ROOT . DS . 'config/Seeds/EventsSeed.php';
$this->exec('bake seed Events --connection test --data');
$path = __FUNCTION__ . '.php';
- if (in_array(getenv('DB'), ['pgsql', 'sqlserver'])) {
+ if (in_array(getenv('DB'), ['pgsql', 'sqlserver'], true)) {
$path = getenv('DB') . DS . $path;
} elseif (PHP_VERSION_ID >= 80100) {
$path = 'php81' . DS . $path;
@@ -115,7 +115,7 @@ public function testWithData()
*
* @return void
*/
- public function testWithDataAndFields()
+ public function testWithDataAndFields(): void
{
$this->generatedFile = ROOT . DS . 'config/Seeds/EventsSeed.php';
$this->exec('bake seed Events --connection test --data --fields title,description');
@@ -130,13 +130,13 @@ public function testWithDataAndFields()
*
* @return void
*/
- public function testWithDataAndLimit()
+ public function testWithDataAndLimit(): void
{
$this->generatedFile = ROOT . DS . 'config/Seeds/EventsSeed.php';
$this->exec('bake seed Events --connection test --data --limit 2');
$path = __FUNCTION__ . '.php';
- if (in_array(getenv('DB'), ['pgsql', 'sqlserver'])) {
+ if (in_array(getenv('DB'), ['pgsql', 'sqlserver'], true)) {
$path = getenv('DB') . DS . $path;
} elseif (PHP_VERSION_ID >= 80100) {
$path = 'php81' . DS . $path;
@@ -152,7 +152,7 @@ public function testWithDataAndLimit()
*
* @return void
*/
- public function testPrettifyArray()
+ public function testPrettifyArray(): void
{
$this->generatedFile = ROOT . DS . 'config/Seeds/TextsSeed.php';
$this->exec('bake seed Texts --connection test --data');
@@ -167,7 +167,7 @@ public function testPrettifyArray()
*
* @return void
*/
- public function testAnonymousStyleWithConfigure()
+ public function testAnonymousStyleWithConfigure(): void
{
Configure::write('Migrations.style', 'anonymous');
diff --git a/tests/TestCase/Command/CompletionTest.php b/tests/TestCase/Command/CompletionTest.php
index b05f0a4da..d61686dcb 100644
--- a/tests/TestCase/Command/CompletionTest.php
+++ b/tests/TestCase/Command/CompletionTest.php
@@ -24,12 +24,14 @@ class CompletionTest extends TestCase
{
use ConsoleIntegrationTestTrait;
+ public $Shell;
+
/**
* tearDown
*
* @return void
*/
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
unset($this->Shell);
@@ -41,17 +43,17 @@ public function tearDown(): void
*
* @return void
*/
- public function testMigrationsSubcommands()
+ public function testMigrationsSubcommands(): void
{
$this->exec('completion subcommands migrations.migrations');
// Upgrade command is hidden when legacyTables is disabled
if (Configure::read('Migrations.legacyTables') === false) {
$expected = [
- 'dump mark_migrated migrate rollback status',
+ 'dump mark_migrated migrate reset rollback status',
];
} else {
$expected = [
- 'dump mark_migrated migrate rollback status upgrade',
+ 'dump mark_migrated migrate reset rollback status upgrade',
];
}
$actual = $this->_out->messages();
@@ -64,7 +66,7 @@ public function testMigrationsSubcommands()
*
* @return void
*/
- public function testMigrationsOptionsMarkMigrated()
+ public function testMigrationsOptionsMarkMigrated(): void
{
$this->exec('completion options migrations.migrations mark_migrated');
$this->assertCount(1, $this->_out->messages());
@@ -85,7 +87,7 @@ public function testMigrationsOptionsMarkMigrated()
*
* @return void
*/
- public function testMigrationsOptionsMigrate()
+ public function testMigrationsOptionsMigrate(): void
{
$this->exec('completion options migrations.migrations migrate');
$this->assertCount(1, $this->_out->messages());
@@ -106,7 +108,7 @@ public function testMigrationsOptionsMigrate()
*
* @return void
*/
- public function testMigrationsOptionsRollback()
+ public function testMigrationsOptionsRollback(): void
{
$this->exec('completion options migrations.migrations rollback');
$this->assertCount(1, $this->_out->messages());
@@ -127,12 +129,12 @@ public function testMigrationsOptionsRollback()
*
* @return void
*/
- public function testMigrationsOptionsStatus()
+ public function testMigrationsOptionsStatus(): void
{
$this->exec('completion options migrations.migrations status');
$this->assertCount(1, $this->_out->messages());
$output = $this->_out->messages()[0];
- $expected = '--cleanup --connection -c --format -f --help -h --plugin -p --quiet -q --source -s --verbose -v';
+ $expected = '--all --cleanup --connection -c --format -f --help -h --plugin -p --quiet -q --source -s --verbose -v';
$outputExplode = explode(' ', trim($output));
sort($outputExplode);
$expectedExplode = explode(' ', $expected);
diff --git a/tests/TestCase/Command/DumpCommandTest.php b/tests/TestCase/Command/DumpCommandTest.php
index fe19b4072..a666415c6 100644
--- a/tests/TestCase/Command/DumpCommandTest.php
+++ b/tests/TestCase/Command/DumpCommandTest.php
@@ -14,10 +14,12 @@
class DumpCommandTest extends TestCase
{
protected Connection $connection;
+
protected string $_compareBasePath;
+
protected string $dumpFile;
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -32,7 +34,7 @@ public function setUp(): void
$this->dumpFile = ROOT . DS . 'config/TestsMigrations/schema-dump-test.lock';
}
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
diff --git a/tests/TestCase/Command/EntryCommandTest.php b/tests/TestCase/Command/EntryCommandTest.php
index 112a78454..30175ccd3 100644
--- a/tests/TestCase/Command/EntryCommandTest.php
+++ b/tests/TestCase/Command/EntryCommandTest.php
@@ -26,7 +26,7 @@ class EntryCommandTest extends TestCase
{
use ConsoleIntegrationTestTrait;
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
}
@@ -36,7 +36,7 @@ public function setUp(): void
*
* @return void
*/
- public function testExecuteHelp()
+ public function testExecuteHelp(): void
{
$this->exec('migrations --help');
@@ -51,11 +51,14 @@ public function testExecuteHelp()
*
* @return void
*/
- public function testExecuteMissingCommand()
+ public function testExecuteMissingCommand(): void
{
$this->exec('migrations derp');
$this->assertExitError();
- $this->assertErrorContains('Could not find migrations command named `derp`');
+ // CakePHP >= 5.4 rejects unknown subcommands before they reach EntryCommand,
+ // so only the parts both error messages have in common are asserted.
+ $this->assertErrorContains('derp');
+ $this->assertErrorContains('--help');
}
}
diff --git a/tests/TestCase/Command/MarkMigratedTest.php b/tests/TestCase/Command/MarkMigratedTest.php
index 669f19327..a417a2908 100644
--- a/tests/TestCase/Command/MarkMigratedTest.php
+++ b/tests/TestCase/Command/MarkMigratedTest.php
@@ -34,7 +34,7 @@ class MarkMigratedTest extends TestCase
*
* @return void
*/
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -51,7 +51,7 @@ public function setUp(): void
*
* @return void
*/
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
$this->connection->execute('DROP TABLE IF EXISTS migrator_phinxlog');
@@ -65,7 +65,7 @@ public function tearDown(): void
*
* @return void
*/
- public function testExecute()
+ public function testExecute(): void
{
$this->exec('migrations mark_migrated --connection=test --source=TestsMigrations');
@@ -102,7 +102,7 @@ public function testExecute()
$this->assertEquals(4, $result->fetchColumn(0));
}
- public function testExecuteTarget()
+ public function testExecuteTarget(): void
{
$this->exec('migrations mark_migrated --connection=test --source=TestsMigrations --target=20150704160200');
$this->assertExitSuccess();
@@ -157,7 +157,7 @@ public function testTargetNotFound(): void
);
}
- public function testExecuteTargetWithExclude()
+ public function testExecuteTargetWithExclude(): void
{
$this->exec('migrations mark_migrated --connection=test --source=TestsMigrations --target=20150724233100 --exclude');
$this->assertExitSuccess();
@@ -206,7 +206,7 @@ public function testExecuteTargetWithExcludeNotFound(): void
);
}
- public function testExecuteTargetWithOnly()
+ public function testExecuteTargetWithOnly(): void
{
$this->exec('migrations mark_migrated --connection=test --source=TestsMigrations --target=20150724233100 --only');
$this->assertExitSuccess();
@@ -252,7 +252,7 @@ public function testExecuteTargetWithOnlyNotFound(): void
);
}
- public function testExecuteInvalidUseOfExclude()
+ public function testExecuteInvalidUseOfExclude(): void
{
$this->exec('migrations mark_migrated --connection=test --source=TestsMigrations --exclude');
@@ -287,7 +287,7 @@ public function testExecutePluginInvalid(): void
try {
$this->exec('migrations mark_migrated -c test --plugin NotThere');
$this->fail('Should raise an error or exit with an error');
- } catch (MissingPluginException $e) {
+ } catch (MissingPluginException) {
$this->assertTrue(true);
}
/** @var \Cake\Database\Connection $connection */
diff --git a/tests/TestCase/Command/MigrateCommandTest.php b/tests/TestCase/Command/MigrateCommandTest.php
index 8b063afc6..5b2dca1e8 100644
--- a/tests/TestCase/Command/MigrateCommandTest.php
+++ b/tests/TestCase/Command/MigrateCommandTest.php
@@ -13,7 +13,7 @@ class MigrateCommandTest extends TestCase
{
protected array $createdFiles = [];
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -21,7 +21,7 @@ public function setUp(): void
$this->clearMigrationRecords('test', 'Migrator');
}
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
foreach ($this->createdFiles as $file) {
@@ -30,7 +30,7 @@ public function tearDown(): void
ConnectionManager::drop('invalid');
}
- public function testHelp()
+ public function testHelp(): void
{
$this->exec('migrations migrate --help');
@@ -127,7 +127,7 @@ public function testMigrateWithSourceMigration(): void
/**
* Test dry-run
*/
- public function testMigrateDryRun()
+ public function testMigrateDryRun(): void
{
$migrationPath = ROOT . DS . 'config' . DS . 'Migrations';
$this->exec('migrations migrate -c test --dry-run');
@@ -146,7 +146,7 @@ public function testMigrateDryRun()
/**
* Test that migrations only run to a certain date
*/
- public function testMigrateDate()
+ public function testMigrateDate(): void
{
$migrationPath = ROOT . DS . 'config' . DS . 'Migrations';
$this->exec('migrations migrate -c test --date 2020-01-01');
@@ -162,7 +162,7 @@ public function testMigrateDate()
/**
* Test output for dates with no matching migrations
*/
- public function testMigrateDateNotFound()
+ public function testMigrateDateNotFound(): void
{
$migrationPath = ROOT . DS . 'config' . DS . 'Migrations';
$this->exec('migrations migrate -c test --date 2000-01-01');
@@ -179,7 +179,7 @@ public function testMigrateDateNotFound()
/**
* Test advancing migrations with an offset.
*/
- public function testMigrateTarget()
+ public function testMigrateTarget(): void
{
$migrationPath = ROOT . DS . 'config' . DS . 'Migrations';
$this->exec('migrations migrate -c test --target 20150416223600');
@@ -196,7 +196,7 @@ public function testMigrateTarget()
$this->assertFileExists($dumpFile);
}
- public function testMigrateTargetNotFound()
+ public function testMigrateTargetNotFound(): void
{
$migrationPath = ROOT . DS . 'config' . DS . 'Migrations';
$this->exec('migrations migrate -c test --target 99');
@@ -214,7 +214,7 @@ public function testMigrateTargetNotFound()
$this->assertFileExists($dumpFile);
}
- public function testMigrateFakeAll()
+ public function testMigrateFakeAll(): void
{
$migrationPath = ROOT . DS . 'config' . DS . 'Migrations';
$this->exec('migrations migrate -c test --fake');
@@ -232,7 +232,7 @@ public function testMigrateFakeAll()
$this->assertFileExists($dumpFile);
}
- public function testMigratePlugin()
+ public function testMigratePlugin(): void
{
$this->loadPlugins(['Migrator']);
$migrationPath = ROOT . DS . 'Plugin' . DS . 'Migrator' . DS . 'config' . DS . 'Migrations';
@@ -250,12 +250,12 @@ public function testMigratePlugin()
$this->assertFileExists($dumpFile);
}
- public function testMigratePluginInvalid()
+ public function testMigratePluginInvalid(): void
{
try {
$this->exec('migrations migrate -c test --plugin NotThere');
$this->fail('Should raise an error or exit with an error');
- } catch (MissingPluginException $e) {
+ } catch (MissingPluginException) {
$this->assertTrue(true);
}
@@ -270,7 +270,7 @@ public function testMigratePluginInvalid()
*
* @return void
*/
- public function testMigrateWithNoLock()
+ public function testMigrateWithNoLock(): void
{
$migrationPath = ROOT . DS . 'config' . DS . 'Migrations';
$this->exec('migrations migrate -c test --no-lock');
diff --git a/tests/TestCase/Command/ResetCommandTest.php b/tests/TestCase/Command/ResetCommandTest.php
new file mode 100644
index 000000000..7ccd45a72
--- /dev/null
+++ b/tests/TestCase/Command/ResetCommandTest.php
@@ -0,0 +1,184 @@
+clearMigrationRecords('test');
+ $this->clearMigrationRecords('test', 'Migrator');
+
+ // Reset event manager to avoid pollution from other tests
+ EventManager::instance()->off('Migration.beforeReset');
+ EventManager::instance()->off('Migration.afterReset');
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ foreach ($this->createdFiles as $file) {
+ unlink($file);
+ }
+
+ // Clean up event listeners
+ EventManager::instance()->off('Migration.beforeReset');
+ EventManager::instance()->off('Migration.afterReset');
+ }
+
+ protected function resetOutput(): void
+ {
+ if ($this->_out instanceof StubConsoleOutput) {
+ $property = new ReflectionProperty($this->_out, '_out');
+ $property->setValue($this->_out, []);
+ }
+ $this->_in = null;
+ }
+
+ public function testHelp(): void
+ {
+ $this->exec('migrations reset --help');
+
+ $this->assertExitSuccess();
+ $this->assertOutputContains('Drop all tables and re-run all migrations');
+ $this->assertOutputContains('destructive operation');
+ }
+
+ public function testDryRunWithTables(): void
+ {
+ // The test db has fixture tables, so there will be tables to drop
+ $this->exec('migrations reset -c test --dry-run --no-lock');
+ $this->assertExitSuccess();
+
+ $this->assertOutputContains('DRY-RUN mode enabled');
+ $this->assertOutputContains('DRY-RUN: Would drop');
+ $this->assertOutputContains('DRY-RUN: Would re-run all migrations');
+ }
+
+ public function testResetAborted(): void
+ {
+ // Test aborting the reset (fixture tables exist)
+ $this->exec('migrations reset -c test --no-lock', ['n']);
+ $this->assertExitSuccess();
+
+ $this->assertOutputContains('The following tables will be dropped');
+ $this->assertErrorContains('Reset operation aborted');
+ }
+
+ public function testResetConfirmed(): void
+ {
+ // First run migrations to create some tables
+ $this->exec('migrations migrate -c test --no-lock');
+ $this->assertExitSuccess();
+ $this->resetOutput();
+
+ // Test confirming the reset
+ $this->exec('migrations reset -c test --no-lock', ['y']);
+ $this->assertExitSuccess();
+
+ $this->assertOutputContains('The following tables will be dropped');
+ $this->assertOutputContains('Dropped');
+ $this->assertOutputContains('All Done');
+ }
+
+ public function testResetWithLock(): void
+ {
+ $migrationPath = ROOT . DS . 'config' . DS . 'Migrations';
+
+ // First run migrations
+ $this->exec('migrations migrate -c test --no-lock');
+ $this->assertExitSuccess();
+ $this->resetOutput();
+
+ // Test reset with lock file generation
+ $this->exec('migrations reset -c test', ['y']);
+ $this->assertExitSuccess();
+
+ $this->assertOutputContains('All Done');
+
+ $dumpFile = $migrationPath . DS . 'schema-dump-test.lock';
+ $this->createdFiles[] = $dumpFile;
+ $this->assertFileExists($dumpFile);
+ }
+
+ public function testEventsFired(): void
+ {
+ // First create something to reset
+ $this->exec('migrations migrate -c test --no-lock');
+ $this->assertExitSuccess();
+ $this->resetOutput();
+
+ // Clean up any existing listeners before registering test listeners
+ EventManager::instance()->off('Migration.beforeReset');
+ EventManager::instance()->off('Migration.afterReset');
+
+ /** @var array $fired */
+ $fired = [];
+ EventManager::instance()->on('Migration.beforeReset', function (EventInterface $event) use (&$fired): void {
+ $fired[] = $event->getName();
+ });
+ EventManager::instance()->on('Migration.afterReset', function (EventInterface $event) use (&$fired): void {
+ $fired[] = $event->getName();
+ });
+
+ $this->exec('migrations reset -c test --no-lock', ['y']);
+ $this->assertExitSuccess();
+ $this->assertSame(['Migration.beforeReset', 'Migration.afterReset'], $fired);
+ }
+
+ public function testBeforeResetEventAbort(): void
+ {
+ // First create something to reset
+ $this->exec('migrations migrate -c test --no-lock');
+ $this->assertExitSuccess();
+ $this->resetOutput();
+
+ /** @var array $fired */
+ $fired = [];
+ EventManager::instance()->on('Migration.beforeReset', function (EventInterface $event) use (&$fired): void {
+ $fired[] = $event->getName();
+ $event->stopPropagation();
+ $event->setResult(0);
+ });
+ EventManager::instance()->on('Migration.afterReset', function (EventInterface $event) use (&$fired): void {
+ $fired[] = $event->getName();
+ });
+
+ $this->exec('migrations reset -c test --no-lock', ['y']);
+ $this->assertExitError();
+
+ // Only one event was fired
+ $this->assertSame(['Migration.beforeReset'], $fired);
+ }
+
+ public function testResetWithPlugin(): void
+ {
+ $this->loadPlugins(['Migrator']);
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+ $connection->execute('DROP TABLE IF EXISTS migrator');
+
+ // Run plugin migrations
+ $this->exec('migrations migrate -c test --plugin Migrator --no-lock');
+ $this->assertExitSuccess();
+ $this->resetOutput();
+
+ // Reset with plugin option
+ $this->exec('migrations reset -c test --plugin Migrator --no-lock', ['y']);
+ $this->assertExitSuccess();
+
+ $this->assertOutputContains('All Done');
+ }
+}
diff --git a/tests/TestCase/Command/RollbackCommandTest.php b/tests/TestCase/Command/RollbackCommandTest.php
index da94d200c..e24fdc250 100644
--- a/tests/TestCase/Command/RollbackCommandTest.php
+++ b/tests/TestCase/Command/RollbackCommandTest.php
@@ -3,6 +3,7 @@
namespace Migrations\Test\TestCase\Command;
+use Cake\Console\TestSuite\StubConsoleOutput;
use Cake\Datasource\ConnectionManager;
use Cake\Event\EventInterface;
use Cake\Event\EventManager;
@@ -14,7 +15,7 @@ class RollbackCommandTest extends TestCase
{
protected array $createdFiles = [];
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -22,7 +23,7 @@ public function setUp(): void
$this->clearMigrationRecords('test', 'Migrator');
}
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
foreach ($this->createdFiles as $file) {
@@ -32,7 +33,7 @@ public function tearDown(): void
protected function resetOutput(): void
{
- if ($this->_out) {
+ if ($this->_out instanceof StubConsoleOutput) {
$property = new ReflectionProperty($this->_out, '_out');
$property->setValue($this->_out, []);
}
diff --git a/tests/TestCase/Command/SeedCommandTest.php b/tests/TestCase/Command/SeedCommandTest.php
index e401e460b..e93984102 100644
--- a/tests/TestCase/Command/SeedCommandTest.php
+++ b/tests/TestCase/Command/SeedCommandTest.php
@@ -11,14 +11,14 @@
class SeedCommandTest extends TestCase
{
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
$this->clearMigrationRecords('test');
}
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
/** @var \Cake\Database\Connection $connection */
@@ -497,6 +497,105 @@ public function testSeedStatusCommand(): void
$this->assertOutputContains('Numbers');
}
+ public function testSeederPluginIsTrackedWithPluginName(): void
+ {
+ $this->_loadTestPlugin('TestBlog');
+ $this->createTables();
+
+ $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed');
+ $this->assertExitSuccess();
+
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+ $rows = $connection
+ ->execute('SELECT seed_name, plugin FROM cake_seeds ORDER BY seed_name')
+ ->fetchAll('assoc');
+
+ $expected = [
+ ['seed_name' => 'PluginLettersSeed', 'plugin' => 'TestBlog'],
+ ['seed_name' => 'PluginSubLettersSeed', 'plugin' => 'TestBlog'],
+ ];
+ $this->assertSame($expected, $rows);
+ }
+
+ public function testSeederPluginLegacyLogEntryIsNotRunAgain(): void
+ {
+ $this->_loadTestPlugin('TestBlog');
+ $this->createTables();
+
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+
+ // Simulate seeds executed before plugin attribution was fixed
+ $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed');
+ $this->assertExitSuccess();
+ $connection->execute('UPDATE cake_seeds SET plugin = NULL');
+
+ $letters = $connection->execute('SELECT COUNT(*) FROM letters');
+ $this->assertEquals(4, $letters->fetchColumn(0));
+
+ $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed');
+ $this->assertExitSuccess();
+ $this->assertOutputNotContains('seeding');
+
+ // No additional rows were inserted by a second run
+ $letters = $connection->execute('SELECT COUNT(*) FROM letters');
+ $this->assertEquals(4, $letters->fetchColumn(0));
+
+ $this->exec('seeds status -c test -p TestBlog --source CallSeeds');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('executed');
+ $this->assertOutputNotContains('pending');
+
+ // Resetting removes the legacy entries as well
+ $this->exec('seeds reset -c test -p TestBlog --source CallSeeds', ['y']);
+ $this->assertExitSuccess();
+
+ $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds');
+ $this->assertEquals(0, $seedLog->fetchColumn(0));
+ }
+
+ public function testSeedStatusCommandWithPlugin(): void
+ {
+ $this->_loadTestPlugin('TestBlog');
+ $this->createTables();
+
+ $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('seeding');
+
+ $this->exec('seeds status -c test -p TestBlog --source CallSeeds');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('TestBlog');
+ $this->assertOutputContains('executed');
+ $this->assertOutputNotContains('pending');
+ }
+
+ public function testSeedResetCommandWithPlugin(): void
+ {
+ $this->_loadTestPlugin('TestBlog');
+ $this->createTables();
+
+ $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('seeding');
+
+ $this->exec('seeds reset -c test -p TestBlog --source CallSeeds', ['y']);
+ $this->assertExitSuccess();
+ $this->assertOutputContains('All seeds will be reset:');
+
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+ $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds');
+ $this->assertEquals(0, $seedLog->fetchColumn(0));
+
+ // Verify the seed can be run again without --force
+ $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('seeding');
+ $this->assertOutputNotContains('already executed');
+ }
+
public function testSeedResetCommand(): void
{
$this->createTables();
@@ -543,7 +642,7 @@ public function testIdempotentSeed(): void
$this->assertEquals(2, $query->fetchColumn(0));
// Verify the seed WAS tracked in cake_seeds table (only one record, updated each run)
- $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'IdempotentTestSeed\'');
+ $seedLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'IdempotentTestSeed'");
$this->assertEquals(1, $seedLog->fetchColumn(0), 'Idempotent seeds should track last execution');
}
@@ -560,7 +659,7 @@ public function testNonIdempotentSeedIsTracked(): void
$connection = ConnectionManager::get('test');
// Verify the seed WAS tracked in cake_seeds table
- $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\'');
+ $seedLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'");
$this->assertEquals(1, $seedLog->fetchColumn(0), 'Regular seeds should be tracked');
// Run again - should be silently skipped
@@ -589,7 +688,7 @@ public function testFakeSeedMarksAsExecuted(): void
$this->assertEquals(0, $query->fetchColumn(0), 'Fake seed should not insert data');
// Verify the seed WAS tracked in cake_seeds table
- $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\'');
+ $seedLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'");
$this->assertEquals(1, $seedLog->fetchColumn(0), 'Fake seeds should be tracked');
// Running again should be silently skipped
@@ -610,7 +709,7 @@ public function testFakeSeedWithForce(): void
$this->assertExitSuccess();
// Verify seed is tracked
- $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\'');
+ $seedLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'");
$this->assertEquals(1, $seedLog->fetchColumn(0));
// Run with --force to actually execute it
@@ -638,10 +737,10 @@ public function testResetSpecificSeed(): void
$this->assertExitSuccess();
// Verify both are tracked
- $numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\'');
+ $numbersLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'");
$this->assertEquals(1, $numbersLog->fetchColumn(0));
- $storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\'');
+ $storesLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'StoresSeed'");
$this->assertEquals(1, $storesLog->fetchColumn(0));
// Reset only Numbers seed
@@ -651,10 +750,10 @@ public function testResetSpecificSeed(): void
$this->assertOutputNotContains('All seeds will be reset:');
// Verify Numbers is reset but Stores is still tracked
- $numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\'');
+ $numbersLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'");
$this->assertEquals(0, $numbersLog->fetchColumn(0), 'Numbers seed should be reset');
- $storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\'');
+ $storesLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'StoresSeed'");
$this->assertEquals(1, $storesLog->fetchColumn(0), 'Stores seed should still be tracked');
}
@@ -674,10 +773,10 @@ public function testResetMultipleSpecificSeeds(): void
$this->assertExitSuccess();
// Verify both are reset
- $numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\'');
+ $numbersLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'");
$this->assertEquals(0, $numbersLog->fetchColumn(0));
- $storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\'');
+ $storesLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'StoresSeed'");
$this->assertEquals(0, $storesLog->fetchColumn(0));
}
@@ -708,7 +807,7 @@ public function testFakeIdempotentSeedIsTracked(): void
$this->assertEquals(0, $query->fetchColumn(0), 'Fake seed should not insert data');
// Verify the seed WAS tracked
- $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'IdempotentTestSeed\'');
+ $seedLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'IdempotentTestSeed'");
$this->assertEquals(1, $seedLog->fetchColumn(0), 'Idempotent seeds should be tracked when faked');
}
}
diff --git a/tests/TestCase/Command/StatusCommandTest.php b/tests/TestCase/Command/StatusCommandTest.php
index 48acaf229..929b07071 100644
--- a/tests/TestCase/Command/StatusCommandTest.php
+++ b/tests/TestCase/Command/StatusCommandTest.php
@@ -3,13 +3,15 @@
namespace Migrations\Test\TestCase\Command;
+use Cake\Console\TestSuite\StubConsoleOutput;
use Cake\Core\Exception\MissingPluginException;
+use Migrations\Command\StatusCommand;
use Migrations\Test\TestCase\TestCase;
use RuntimeException;
class StatusCommandTest extends TestCase
{
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -41,7 +43,7 @@ public function testExecuteSimpleJson(): void
$this->exec('migrations status -c test --format json');
$this->assertExitSuccess();
- assert(isset($this->_out));
+ assert($this->_out instanceof StubConsoleOutput);
$output = $this->_out->messages();
$parsed = json_decode($output[0], true);
$this->assertTrue(is_array($parsed));
@@ -104,4 +106,80 @@ public function testCleanHelp(): void
$this->assertOutputContains('--cleanup');
$this->assertOutputContains('Remove MISSING migrations from the');
}
+
+ public function testAllHelp(): void
+ {
+ $this->exec('migrations status --help');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('--all');
+ $this->assertOutputContains('every loaded plugin');
+ }
+
+ public function testAllAppOnlyExitCodeDownWhenPending(): void
+ {
+ $this->exec('migrations status -c test --all');
+ // App has unmigrated migrations, so the exit code signals pending.
+ $this->assertExitCode(StatusCommand::CODE_STATUS_DOWN);
+ // Default output is summary only — no per-section table.
+ $this->assertOutputContains('Summary:');
+ $this->assertOutputContains('APP:');
+ $this->assertOutputContains('pending');
+ $this->assertOutputNotContains('Migration ID');
+ }
+
+ public function testAllIncludesLoadedPluginWithMigrations(): void
+ {
+ $this->loadPlugins(['Migrator']);
+ $this->exec('migrations status -c test --all');
+ $this->assertExitCode(StatusCommand::CODE_STATUS_DOWN);
+ $this->assertOutputContains('Summary:');
+ $this->assertOutputContains('- APP:');
+ $this->assertOutputContains('- Migrator:');
+ $this->assertOutputNotContains('Plugin:');
+ }
+
+ public function testAllVerboseShowsPerSectionTables(): void
+ {
+ $this->loadPlugins(['Migrator']);
+ $this->exec('migrations status -c test --all -v');
+ $this->assertExitCode(StatusCommand::CODE_STATUS_DOWN);
+ $this->assertOutputContains('Migration ID');
+ // Plugin section header reads as just the plugin name now.
+ $this->assertOutputContains('Migrator');
+ $this->assertOutputNotContains('Plugin: Migrator');
+ // Summary still rendered after the tables.
+ $this->assertOutputContains('Summary:');
+ }
+
+ public function testAllJsonOutput(): void
+ {
+ $this->loadPlugins(['Migrator']);
+ $this->exec('migrations status -c test --all --format json');
+ $this->assertExitCode(StatusCommand::CODE_STATUS_DOWN);
+
+ assert($this->_out instanceof StubConsoleOutput);
+ $messages = $this->_out->messages();
+ $jsonLine = end($messages);
+ $parsed = json_decode((string)$jsonLine, true);
+ $this->assertIsArray($parsed);
+ $this->assertArrayHasKey('app', $parsed);
+ $this->assertArrayHasKey('Migrator', $parsed);
+ $this->assertIsArray($parsed['app']);
+ $this->assertIsArray($parsed['Migrator']);
+ }
+
+ public function testAllRejectsPluginOption(): void
+ {
+ $this->loadPlugins(['Migrator']);
+ $this->exec('migrations status -c test --all -p Migrator');
+ $this->assertExitError();
+ $this->assertErrorContains('cannot be combined with --plugin');
+ }
+
+ public function testAllRejectsCleanupOption(): void
+ {
+ $this->exec('migrations status -c test --all --cleanup');
+ $this->assertExitError();
+ $this->assertErrorContains('cannot be combined with --cleanup');
+ }
}
diff --git a/tests/TestCase/Command/UpgradeCommandTest.php b/tests/TestCase/Command/UpgradeCommandTest.php
index ceddd9537..3a3890229 100644
--- a/tests/TestCase/Command/UpgradeCommandTest.php
+++ b/tests/TestCase/Command/UpgradeCommandTest.php
@@ -12,7 +12,7 @@
class UpgradeCommandTest extends TestCase
{
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -23,7 +23,7 @@ public function setUp(): void
$connection->execute('DROP TABLE IF EXISTS cake_migrations');
}
- public function tearDown(): void
+ protected function tearDown(): void
{
$this->clearMigrationRecords('test');
@@ -48,7 +48,7 @@ protected function getAdapter(): AdapterInterface
public function testHelp(): void
{
- Configure::write('Migrations.legacyTables', null);
+ Configure::write('Migrations.legacyTables');
$this->exec('migrations upgrade --help');
$this->assertExitSuccess();
@@ -61,7 +61,7 @@ public function testExecuteSimpleDryRun(): void
Configure::write('Migrations.legacyTables', true);
try {
$this->getAdapter()->createSchemaTable();
- } catch (Exception $e) {
+ } catch (Exception) {
// Table probably exists
}
@@ -85,7 +85,7 @@ public function testExecuteSimpleExecute(): void
$adapter = $environment->getAdapter();
try {
$adapter->createSchemaTable();
- } catch (Exception $e) {
+ } catch (Exception) {
// Table probably exists
}
@@ -113,7 +113,7 @@ public function testExecuteSimpleExecuteDropTables(): void
$adapter = $environment->getAdapter();
try {
$adapter->createSchemaTable();
- } catch (Exception $e) {
+ } catch (Exception) {
// Table probably exists
}
@@ -135,7 +135,7 @@ public function testExecuteWithMigrations(): void
Configure::write('Migrations.legacyTables', true);
try {
$this->getAdapter()->createSchemaTable();
- } catch (Exception $e) {
+ } catch (Exception) {
// Table probably exists
}
@@ -166,4 +166,65 @@ public function testExecuteWithMigrations(): void
$this->assertCount(1, $rows);
}
+
+ /**
+ * Test that plugins with slashes (like CakeDC/Users) are correctly identified
+ * during upgrade from legacy phinxlog tables.
+ */
+ public function testExecuteWithSlashInPluginName(): void
+ {
+ Configure::write('Migrations.legacyTables', true);
+
+ // Create the plugin's phinxlog table using the adapter for cross-database compatibility
+ $config = ConnectionManager::getConfig('test');
+ $environment = new Environment('default', [
+ 'connection' => 'test',
+ 'database' => $config['database'],
+ 'migration_table' => 'cake_d_c_users_phinxlog',
+ ]);
+ $adapter = $environment->getAdapter();
+ try {
+ $adapter->createSchemaTable();
+ } catch (Exception) {
+ // Table probably exists
+ }
+
+ // Insert a migration record
+ $adapter->getInsertBuilder()
+ ->insert(['version', 'migration_name', 'breakpoint'])
+ ->into('cake_d_c_users_phinxlog')
+ ->values([
+ 'version' => '20250118143003',
+ 'migration_name' => 'SlashPluginMigration',
+ 'breakpoint' => 0,
+ ])
+ ->execute();
+
+ // Load a fake plugin with a slash in the name using loadPlugins
+ // which properly integrates with the console application
+ $this->loadPlugins(['CakeDC/Users' => ['path' => TMP]]);
+
+ try {
+ $this->exec('migrations upgrade -c test');
+ $this->assertExitSuccess();
+
+ $this->assertOutputContains('cake_d_c_users_phinxlog (CakeDC/Users)');
+
+ // Verify the plugin column has the correct value with slash
+ $rows = $this->getAdapter()->getSelectBuilder()
+ ->select(['version', 'migration_name', 'plugin'])
+ ->from('cake_migrations')
+ ->where(['migration_name' => 'SlashPluginMigration'])
+ ->all();
+
+ $this->assertCount(1, $rows);
+ $this->assertSame('CakeDC/Users', $rows[0]['plugin']);
+ } finally {
+ // Cleanup
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+ $connection->execute('DROP TABLE ' . $connection->getDriver()->quoteIdentifier('cake_d_c_users_phinxlog'));
+ $this->removePlugins(['CakeDC/Users']);
+ }
+ }
}
diff --git a/tests/TestCase/Config/ConfigMigrationPathsTest.php b/tests/TestCase/Config/ConfigMigrationPathsTest.php
index 8a1165ba8..a849eb7c9 100644
--- a/tests/TestCase/Config/ConfigMigrationPathsTest.php
+++ b/tests/TestCase/Config/ConfigMigrationPathsTest.php
@@ -10,7 +10,7 @@
*/
class ConfigMigrationPathsTest extends AbstractConfigTestCase
{
- public function testGetMigrationPathsThrowsExceptionForNoPath()
+ public function testGetMigrationPathsThrowsExceptionForNoPath(): void
{
$config = new Config([]);
@@ -22,7 +22,7 @@ public function testGetMigrationPathsThrowsExceptionForNoPath()
/**
* Normal behavior
*/
- public function testGetMigrationPaths()
+ public function testGetMigrationPaths(): void
{
$config = new Config($this->getConfigArray());
$this->assertEquals($this->getMigrationPath(), $config->getMigrationPath());
diff --git a/tests/TestCase/Config/ConfigSeedPathsTest.php b/tests/TestCase/Config/ConfigSeedPathsTest.php
index 8697979bf..164c8404b 100644
--- a/tests/TestCase/Config/ConfigSeedPathsTest.php
+++ b/tests/TestCase/Config/ConfigSeedPathsTest.php
@@ -10,7 +10,7 @@
*/
class ConfigSeedPathsTest extends AbstractConfigTestCase
{
- public function testGetSeedPathsThrowsExceptionForNoPath()
+ public function testGetSeedPathsThrowsExceptionForNoPath(): void
{
$config = new Config([]);
@@ -22,13 +22,13 @@ public function testGetSeedPathsThrowsExceptionForNoPath()
/**
* Normal behavior
*/
- public function testGetSeedPaths()
+ public function testGetSeedPaths(): void
{
$config = new Config($this->getConfigArray());
$this->assertEquals($this->getSeedPath(), $config->getSeedPath());
}
- public function testGetSeedPathConvertsStringToArray()
+ public function testGetSeedPathConvertsStringToArray(): void
{
$values = [
'paths' => [
diff --git a/tests/TestCase/Config/ConfigTest.php b/tests/TestCase/Config/ConfigTest.php
index 9197956be..5bd0ba4c7 100644
--- a/tests/TestCase/Config/ConfigTest.php
+++ b/tests/TestCase/Config/ConfigTest.php
@@ -12,14 +12,14 @@
*/
class ConfigTest extends AbstractConfigTestCase
{
- public function testGetEnvironmentMethod()
+ public function testGetEnvironmentMethod(): void
{
$config = new Config($this->getConfigArray());
$db = $config->getEnvironment();
$this->assertArrayHasKey('adapter', $db);
}
- public function testEnvironmentHasMigrationTable()
+ public function testEnvironmentHasMigrationTable(): void
{
$configArray = $this->getConfigArray();
$configArray['environment']['migration_table'] = 'test_table';
@@ -28,7 +28,7 @@ public function testEnvironmentHasMigrationTable()
$this->assertSame('test_table', $config->getEnvironment()['migration_table']);
}
- public function testArrayAccessMethods()
+ public function testArrayAccessMethods(): void
{
$config = new Config([]);
$config['foo'] = 'bar';
@@ -38,7 +38,7 @@ public function testArrayAccessMethods()
$this->assertArrayNotHasKey('foo', $config);
}
- public function testUndefinedArrayAccess()
+ public function testUndefinedArrayAccess(): void
{
$config = new Config([]);
@@ -48,7 +48,7 @@ public function testUndefinedArrayAccess()
$config['foo'];
}
- public function testGetSeedPath()
+ public function testGetSeedPath(): void
{
$config = new Config(['paths' => ['seeds' => 'db/seeds']]);
$this->assertEquals('db/seeds', $config->getSeedPath());
@@ -57,7 +57,7 @@ public function testGetSeedPath()
$this->assertEquals('db/seeds1', $config->getSeedPath());
}
- public function testGetSeedPathThrowsException()
+ public function testGetSeedPathThrowsException(): void
{
$config = new Config([]);
@@ -67,7 +67,7 @@ public function testGetSeedPathThrowsException()
$config->getSeedPath();
}
- public function testGetVersionOrder()
+ public function testGetVersionOrder(): void
{
$config = new Config([]);
$config['version_order'] = Config::VERSION_ORDER_EXECUTION_TIME;
@@ -75,7 +75,7 @@ public function testGetVersionOrder()
}
#[DataProvider('isVersionOrderCreationTimeDataProvider')]
- public function testIsVersionOrderCreationTime($versionOrder, $expected)
+ public function testIsVersionOrderCreationTime(string $versionOrder, bool $expected): void
{
// get config stub
$configStub = $this->getMockBuilder(Config::class)
@@ -90,7 +90,7 @@ public function testIsVersionOrderCreationTime($versionOrder, $expected)
$this->assertEquals($expected, $configStub->isVersionOrderCreationTime());
}
- public static function isVersionOrderCreationTimeDataProvider()
+ public static function isVersionOrderCreationTimeDataProvider(): array
{
return [
'With Creation Time Version Order' =>
diff --git a/tests/TestCase/Db/Adapter/AbstractAdapterTest.php b/tests/TestCase/Db/Adapter/AbstractAdapterTest.php
index 425d5281d..5279b0705 100644
--- a/tests/TestCase/Db/Adapter/AbstractAdapterTest.php
+++ b/tests/TestCase/Db/Adapter/AbstractAdapterTest.php
@@ -18,10 +18,7 @@
class AbstractAdapterTest extends TestCase
{
- /**
- * @var \Migrations\Db\Adapter\AbstractAdapter|\PHPUnit\Framework\MockObject\MockObject
- */
- private $adapter;
+ private AbstractAdapter $adapter;
protected function setUp(): void
{
@@ -35,14 +32,14 @@ protected function tearDown(): void
unset($this->adapter);
}
- public function testOptions()
+ public function testOptions(): void
{
$options = $this->adapter->getOptions();
$this->assertArrayHasKey('foo', $options);
$this->assertEquals('bar', $options['foo']);
}
- public function testOptionsSetSchemaTableName()
+ public function testOptionsSetSchemaTableName(): void
{
// When unified table mode is enabled, getSchemaTableName() returns cake_migrations
$expectedDefault = Configure::read('Migrations.legacyTables') === false
@@ -58,7 +55,7 @@ public function testOptionsSetSchemaTableName()
$this->assertEquals($expectedAfterSet, $this->adapter->getSchemaTableName());
}
- public function testSchemaTableName()
+ public function testSchemaTableName(): void
{
$expectedDefault = Configure::read('Migrations.legacyTables') === false
? UnifiedMigrationsTableStorage::TABLE_NAME
@@ -71,7 +68,7 @@ public function testSchemaTableName()
$this->assertEquals($expectedAfterSet, $this->adapter->getSchemaTableName());
}
- public function testGetVersionLogInvalidVersionOrderKO()
+ public function testGetVersionLogInvalidVersionOrderKO(): void
{
$this->expectExceptionMessage('Invalid version_order configuration option');
$adapter = new class (['version_order' => 'invalid']) extends AbstractAdapter {
@@ -83,7 +80,7 @@ public function testGetVersionLogInvalidVersionOrderKO()
$adapter->getVersionLog();
}
- public function testGetVersionLongDryRun()
+ public function testGetVersionLongDryRun(): void
{
$adapter = new class (['version_order' => Config::VERSION_ORDER_CREATION_TIME]) extends AbstractAdapter {
use DefaultAdapterTrait;
@@ -122,7 +119,7 @@ public function fetchAll(string $sql): array
* @see https://github.com/cakephp/phinx/issues/1891
*/
#[DataProvider('currentTimestampDefaultValueProvider')]
- public function testCurrentTimestampDefaultValueQuoting($default, $columnType, $expected)
+ public function testCurrentTimestampDefaultValueQuoting(string|Literal|bool $default, ?string $columnType, string $expected): void
{
$adapter = new class (['version_order' => Config::VERSION_ORDER_CREATION_TIME]) extends AbstractAdapter {
use DefaultAdapterTrait;
diff --git a/tests/TestCase/Db/Adapter/AdapterFactoryTest.php b/tests/TestCase/Db/Adapter/AdapterFactoryTest.php
index 8db24f3bf..e1fd15594 100644
--- a/tests/TestCase/Db/Adapter/AdapterFactoryTest.php
+++ b/tests/TestCase/Db/Adapter/AdapterFactoryTest.php
@@ -5,17 +5,18 @@
use Migrations\Db\Adapter\AbstractAdapter;
use Migrations\Db\Adapter\AdapterFactory;
+use Migrations\Db\Adapter\AdapterInterface;
+use Migrations\Db\Adapter\MysqlAdapter;
+use Migrations\Db\Adapter\TimedOutputAdapter;
use Migrations\Test\TestCase\Db\Adapter\DefaultAdapterTrait;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use RuntimeException;
class AdapterFactoryTest extends TestCase
{
- /**
- * @var \Migrations\Db\Adapter\AdapterFactory
- */
- private $factory;
+ private AdapterFactory $factory;
protected function setUp(): void
{
@@ -27,17 +28,17 @@ protected function tearDown(): void
unset($this->factory);
}
- public function testInstanceIsFactory()
+ public function testInstanceIsFactory(): void
{
$this->assertInstanceOf(AdapterFactory::class, $this->factory);
}
- public function testRegisterAdapter()
+ public function testRegisterAdapter(): void
{
$pdo = new class (['foo' => 'bar']) extends AbstractAdapter {
use DefaultAdapterTrait;
};
- $this->factory->registerAdapter('test', function (array $options) use ($pdo) {
+ $this->factory->registerAdapter('test', function (array $options) use ($pdo): object {
$this->assertEquals('value', $options['key']);
return $pdo;
@@ -46,7 +47,7 @@ public function testRegisterAdapter()
$this->assertEquals($pdo, $this->factory->getAdapter('test', ['key' => 'value']));
}
- public function testRegisterAdapterFailure()
+ public function testRegisterAdapterFailure(): void
{
$adapter = static::class;
@@ -56,14 +57,14 @@ public function testRegisterAdapterFailure()
$this->factory->registerAdapter('test', $adapter);
}
- public function testGetAdapter()
+ public function testGetAdapter(): void
{
$adapter = $this->factory->getAdapter('mysql', []);
- $this->assertInstanceOf('Migrations\Db\Adapter\MysqlAdapter', $adapter);
+ $this->assertInstanceOf(MysqlAdapter::class, $adapter);
}
- public function testGetAdapterFailure()
+ public function testGetAdapterFailure(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Adapter "bad" has not been registered');
@@ -71,12 +72,11 @@ public function testGetAdapterFailure()
$this->factory->getAdapter('bad', []);
}
- public function testRegisterWrapper()
+ public function testRegisterWrapper(): void
{
// WrapperFactory::getClass is protected, work around it to avoid
// creating unnecessary instances and making the test more complex.
- $method = new ReflectionMethod(get_class($this->factory), 'getWrapperClass');
- $method->setAccessible(true);
+ $method = new ReflectionMethod($this->factory::class, 'getWrapperClass');
$wrapper = $method->invoke($this->factory, 'record');
$this->factory->registerWrapper('test', $wrapper);
@@ -84,7 +84,7 @@ public function testRegisterWrapper()
$this->assertEquals($wrapper, $method->invoke($this->factory, 'test'));
}
- public function testRegisterWrapperFailure()
+ public function testRegisterWrapperFailure(): void
{
$wrapper = static::class;
@@ -94,19 +94,19 @@ public function testRegisterWrapperFailure()
$this->factory->registerWrapper('test', $wrapper);
}
- private function getAdapterMock()
+ private function getAdapterMock(): MockObject
{
- return $this->getMockBuilder('Migrations\Db\Adapter\AdapterInterface')->getMock();
+ return $this->getMockBuilder(AdapterInterface::class)->getMock();
}
- public function testGetWrapper()
+ public function testGetWrapper(): void
{
$wrapper = $this->factory->getWrapper('timed', $this->getAdapterMock());
- $this->assertInstanceOf('Migrations\Db\Adapter\TimedOutputAdapter', $wrapper);
+ $this->assertInstanceOf(TimedOutputAdapter::class, $wrapper);
}
- public function testGetWrapperFailure()
+ public function testGetWrapperFailure(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Wrapper "nope" has not been registered');
diff --git a/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php b/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php
index 61543f28b..59ecb01f4 100644
--- a/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php
+++ b/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php
@@ -9,6 +9,8 @@
use Migrations\Db\Table\ForeignKey;
use Migrations\Db\Table\Index;
use Migrations\Db\Table\TableMetadata;
+use Migrations\Db\Table\Trigger;
+use Migrations\Db\Table\View;
trait DefaultAdapterTrait
{
@@ -187,4 +189,32 @@ protected function getDropCheckConstraintInstructions(string $tableName, string
{
return new AlterInstructions();
}
+
+ protected function getCreateViewInstructions(View $view): AlterInstructions
+ {
+ return new AlterInstructions();
+ }
+
+ protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions
+ {
+ return new AlterInstructions();
+ }
+
+ protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions
+ {
+ return new AlterInstructions();
+ }
+
+ protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions
+ {
+ return new AlterInstructions();
+ }
+
+ public function disableForeignKeyConstraints(): void
+ {
+ }
+
+ public function enableForeignKeyConstraints(): void
+ {
+ }
}
diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php
index 91cf6833f..30a2c8044 100644
--- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php
+++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php
@@ -9,6 +9,7 @@
use Cake\Core\Configure;
use Cake\Database\Connection;
use Cake\Database\Driver\Mysql;
+use Cake\Database\Schema\TableSchema;
use Cake\Datasource\ConnectionManager;
use InvalidArgumentException;
use Migrations\Db\Adapter\MysqlAdapter;
@@ -28,16 +29,12 @@
class MysqlAdapterTest extends TestCase
{
- /**
- * @var \Migrations\Db\Adapter\MysqlAdapter
- */
- private $adapter;
+ private MysqlAdapter $adapter;
+
+ private array $config;
- /**
- * @var array
- */
- private $config;
private StubConsoleOutput $out;
+
private ConsoleIo $io;
protected function setUp(): void
@@ -101,12 +98,12 @@ private function usingMariaDbWithUuid(): bool
return version_compare($version, '10.7.0', '>=');
}
- public function testConnection()
+ public function testConnection(): void
{
$this->assertInstanceOf(Connection::class, $this->adapter->getConnection());
}
- public function testCreatingTheSchemaTableOnConnect()
+ public function testCreatingTheSchemaTableOnConnect(): void
{
$this->adapter->connect();
$this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName()));
@@ -117,7 +114,7 @@ public function testCreatingTheSchemaTableOnConnect()
$this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName()));
}
- public function testSchemaTableIsCreatedWithPrimaryKey()
+ public function testSchemaTableIsCreatedWithPrimaryKey(): void
{
// Skip for unified table mode since schema structure is different
if (Configure::read('Migrations.legacyTables') === false) {
@@ -129,7 +126,7 @@ public function testSchemaTableIsCreatedWithPrimaryKey()
$this->assertTrue($this->adapter->hasIndex($this->adapter->getSchemaTableName(), ['version']));
}
- public function testDatabaseNameWithEscapedCharacter()
+ public function testDatabaseNameWithEscapedCharacter(): void
{
$this->adapter->dropDatabase($this->config['database'] . '-test');
$this->adapter->createDatabase($this->config['database'] . '-test', ['charset' => 'utf8mb4']);
@@ -137,24 +134,24 @@ public function testDatabaseNameWithEscapedCharacter()
$this->adapter->dropDatabase($this->config['database'] . '-test');
}
- public function testQuoteTableName()
+ public function testQuoteTableName(): void
{
$this->assertEquals('`test_table`', $this->adapter->quoteTableName('test_table'));
}
- public function testQuoteColumnName()
+ public function testQuoteColumnName(): void
{
$this->assertEquals('`test_column`', $this->adapter->quoteColumnName('test_column'));
}
- public function testHasTableUnderstandsSchemaNotation()
+ public function testHasTableUnderstandsSchemaNotation(): void
{
$this->assertTrue($this->adapter->hasTable('performance_schema.threads'), 'Failed asserting hasTable understands tables in another schema.');
$this->assertFalse($this->adapter->hasTable('performance_schema.unknown_table'));
$this->assertFalse($this->adapter->hasTable('unknown_schema.phinxlog'));
}
- public function testCreateTable()
+ public function testCreateTable(): void
{
$table = new Table('ntable', [], $this->adapter);
$table->addColumn('realname', 'string')
@@ -172,7 +169,7 @@ public function testCreateTable()
$this->assertFalse($columns[0]->isSigned());
}
- public function testCreateTableWithComment()
+ public function testCreateTableWithComment(): void
{
$tableComment = 'Table comment';
$table = new Table('ntable', ['comment' => $tableComment], $this->adapter);
@@ -192,7 +189,7 @@ public function testCreateTableWithComment()
$this->assertEquals($tableComment, $comment['TABLE_COMMENT'], 'Dont set table comment correctly');
}
- public function testCreateTableWithForeignKeys()
+ public function testCreateTableWithForeignKeys(): void
{
$tag_table = new Table('ntable_tag', [], $this->adapter);
$tag_table->addColumn('realname', 'string')
@@ -223,7 +220,7 @@ public function testCreateTableWithForeignKeys()
$this->assertEquals($foreignKey['REFERENCED_COLUMN_NAME'], 'id');
}
- public function testCreateTableCustomIdColumn()
+ public function testCreateTableCustomIdColumn(): void
{
$table = new Table('ntable', ['id' => 'custom_id'], $this->adapter);
$table->addColumn('realname', 'string')
@@ -236,7 +233,7 @@ public function testCreateTableCustomIdColumn()
$this->assertFalse($this->adapter->hasColumn('ntable', 'address'));
}
- public function testCreateTableWithNoPrimaryKey()
+ public function testCreateTableWithNoPrimaryKey(): void
{
$options = [
'id' => false,
@@ -247,7 +244,7 @@ public function testCreateTableWithNoPrimaryKey()
$this->assertFalse($this->adapter->hasColumn('atable', 'id'));
}
- public function testCreateTableWithConflictingPrimaryKeys()
+ public function testCreateTableWithConflictingPrimaryKeys(): void
{
$options = [
'primary_key' => 'user_id',
@@ -259,7 +256,7 @@ public function testCreateTableWithConflictingPrimaryKeys()
$table->addColumn('user_id', 'integer')->save();
}
- public function testCreateTableWithPrimaryKeySetToImplicitId()
+ public function testCreateTableWithPrimaryKeySetToImplicitId(): void
{
$options = [
'primary_key' => 'id',
@@ -271,7 +268,7 @@ public function testCreateTableWithPrimaryKeySetToImplicitId()
$this->assertTrue($this->adapter->hasColumn('ztable', 'user_id'));
}
- public function testCreateTableWithPrimaryKeyArraySetToImplicitId()
+ public function testCreateTableWithPrimaryKeyArraySetToImplicitId(): void
{
$options = [
'primary_key' => ['id'],
@@ -283,7 +280,7 @@ public function testCreateTableWithPrimaryKeyArraySetToImplicitId()
$this->assertTrue($this->adapter->hasColumn('ztable', 'user_id'));
}
- public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId()
+ public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId(): void
{
$options = [
'primary_key' => ['id', 'user_id'],
@@ -294,7 +291,7 @@ public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId()
$table->addColumn('user_id', 'integer')->save();
}
- public function testCreateTableWithMultiplePrimaryKeys()
+ public function testCreateTableWithMultiplePrimaryKeys(): void
{
$options = [
'id' => false,
@@ -313,7 +310,7 @@ public function testCreateTableWithMultiplePrimaryKeys()
/**
* @return void
*/
- public function testCreateTableWithPrimaryKeyAsUuid()
+ public function testCreateTableWithPrimaryKeyAsUuid(): void
{
$options = [
'id' => false,
@@ -330,7 +327,7 @@ public function testCreateTableWithPrimaryKeyAsUuid()
/**
* @return void
*/
- public function testCreateTableWithPrimaryKeyAsBinaryUuid()
+ public function testCreateTableWithPrimaryKeyAsBinaryUuid(): void
{
$options = [
'id' => false,
@@ -344,7 +341,7 @@ public function testCreateTableWithPrimaryKeyAsBinaryUuid()
$this->assertTrue($this->adapter->hasColumn('ztable', 'user_id'));
}
- public function testCreateTableBinaryLengthWithIndex()
+ public function testCreateTableBinaryLengthWithIndex(): void
{
$table = new Table('ntable', [], $this->adapter);
$table
@@ -368,7 +365,7 @@ public function testCreateTableBinaryLengthWithIndex()
/**
* @return void
*/
- public function testCreateTableWithPrimaryKeyAsNativeUuid()
+ public function testCreateTableWithPrimaryKeyAsNativeUuid(): void
{
if (!$this->usingMariaDbWithUuid()) {
$this->markTestSkipped('Database does not have a native uuid type');
@@ -386,7 +383,7 @@ public function testCreateTableWithPrimaryKeyAsNativeUuid()
$this->assertTrue($this->adapter->hasColumn('ztable', 'user_id'));
}
- public function testCreateTableWithMultipleIndexes()
+ public function testCreateTableWithMultipleIndexes(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -400,7 +397,7 @@ public function testCreateTableWithMultipleIndexes()
$this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_name']));
}
- public function testCreateTableWithUniqueIndexes()
+ public function testCreateTableWithUniqueIndexes(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string', ['limit' => 191])
@@ -410,7 +407,7 @@ public function testCreateTableWithUniqueIndexes()
$this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_email']));
}
- public function testCreateTableWithFullTextIndex()
+ public function testCreateTableWithFullTextIndex(): void
{
$table = new Table('table1', ['engine' => 'MyISAM'], $this->adapter);
$table->addColumn('email', 'string')
@@ -420,7 +417,7 @@ public function testCreateTableWithFullTextIndex()
$this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_email']));
}
- public function testCreateTableWithNamedIndex()
+ public function testCreateTableWithNamedIndex(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -431,7 +428,7 @@ public function testCreateTableWithNamedIndex()
$this->assertTrue($this->adapter->hasIndexByName('table1', 'myemailindex'));
}
- public function testCreateTableWithMyISAMEngine()
+ public function testCreateTableWithMyISAMEngine(): void
{
$table = new Table('ntable', ['engine' => 'MyISAM'], $this->adapter);
$table->addColumn('realname', 'string')
@@ -441,7 +438,7 @@ public function testCreateTableWithMyISAMEngine()
$this->assertEquals('MyISAM', $row['Engine']);
}
- public function testCreateTableAndInheritDefaultCollation()
+ public function testCreateTableAndInheritDefaultCollation(): void
{
$options = $this->config + [
'charset' => 'utf8mb4',
@@ -457,7 +454,7 @@ public function testCreateTableAndInheritDefaultCollation()
$this->assertContains($row['Collation'], ['utf8mb4_general_ci', 'utf8mb4_0900_ai_ci', 'utf8mb4_uca1400_ai_ci', 'utf8mb4_unicode_ci']);
}
- public function testCreateTableWithLatin1Collate()
+ public function testCreateTableWithLatin1Collate(): void
{
$table = new Table('latin1_table', ['collation' => 'latin1_general_ci'], $this->adapter);
$table->addColumn('name', 'string')
@@ -467,7 +464,7 @@ public function testCreateTableWithLatin1Collate()
$this->assertEquals('latin1_general_ci', $row['Collation']);
}
- public function testCreateTableWithSignedPK()
+ public function testCreateTableWithSignedPK(): void
{
$table = new Table('ntable', ['signed' => true], $this->adapter);
$table->addColumn('realname', 'string')
@@ -486,7 +483,7 @@ public function testCreateTableWithSignedPK()
}
}
- public function testCreateTableWithUnsignedPK()
+ public function testCreateTableWithUnsignedPK(): void
{
$table = new Table('ntable', ['signed' => false], $this->adapter);
$table->addColumn('realname', 'string')
@@ -505,7 +502,7 @@ public function testCreateTableWithUnsignedPK()
}
}
- public function testCreateTableWithUnsignedNamedPK()
+ public function testCreateTableWithUnsignedNamedPK(): void
{
$table = new Table('ntable', ['id' => 'named_id', 'signed' => false], $this->adapter);
$table->addColumn('realname', 'string')
@@ -524,7 +521,7 @@ public function testCreateTableWithUnsignedNamedPK()
$this->assertFalse($this->adapter->hasColumn('ntable', 'address'));
}
- public function testCreateTableWithSetEnumTypes()
+ public function testCreateTableWithSetEnumTypes(): void
{
$table = new Table('enum_test', [], $this->adapter);
$table->addColumn('status', 'enum', ['values' => ['pending', 'active', 'archived']])
@@ -537,7 +534,7 @@ public function testCreateTableWithSetEnumTypes()
}
#[RunInSeparateProcess]
- public function testUnsignedPksFeatureFlag()
+ public function testUnsignedPksFeatureFlag(): void
{
$this->adapter->connect();
@@ -553,7 +550,7 @@ public function testUnsignedPksFeatureFlag()
}
#[RunInSeparateProcess]
- public function testAddTimestampsFeatureFlag()
+ public function testAddTimestampsFeatureFlag(): void
{
Configure::write('Migrations.add_timestamps_use_datetime', true);
$this->adapter->connect();
@@ -580,7 +577,7 @@ public function testAddTimestampsFeatureFlag()
$this->assertContains($columns[2]->getDefault(), ['CURRENT_TIMESTAMP', 'current_timestamp()']);
}
- public function testCreateTableWithSchema()
+ public function testCreateTableWithSchema(): void
{
$table = new Table($this->config['database'] . '.ntable', [], $this->adapter);
$table->addColumn('realname', 'string')
@@ -589,7 +586,7 @@ public function testCreateTableWithSchema()
$this->assertTrue($this->adapter->hasTable('ntable'));
}
- public function testAddPrimarykey()
+ public function testAddPrimarykey(): void
{
$table = new Table('table1', ['id' => false], $this->adapter);
$table
@@ -603,7 +600,7 @@ public function testAddPrimarykey()
$this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column1']));
}
- public function testChangePrimaryKey()
+ public function testChangePrimaryKey(): void
{
$table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter);
$table
@@ -620,7 +617,7 @@ public function testChangePrimaryKey()
$this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column2', 'column3']));
}
- public function testDropPrimaryKey()
+ public function testDropPrimaryKey(): void
{
$table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter);
$table
@@ -634,7 +631,7 @@ public function testDropPrimaryKey()
$this->assertFalse($this->adapter->hasPrimaryKey('table1', ['column1']));
}
- public function testAddComment()
+ public function testAddComment(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -656,7 +653,7 @@ public function testAddComment()
$this->assertEquals('comment1', $rows[0]['TABLE_COMMENT']);
}
- public function testChangeComment()
+ public function testChangeComment(): void
{
$table = new Table('table1', ['comment' => 'comment1'], $this->adapter);
$table->save();
@@ -678,7 +675,7 @@ public function testChangeComment()
$this->assertEquals('comment2', $rows[0]['TABLE_COMMENT']);
}
- public function testDropComment()
+ public function testDropComment(): void
{
$table = new Table('table1', ['comment' => 'comment1'], $this->adapter);
$table->save();
@@ -700,7 +697,7 @@ public function testDropComment()
$this->assertEquals('', $rows[0]['TABLE_COMMENT']);
}
- public function testRenameTable()
+ public function testRenameTable(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -712,7 +709,7 @@ public function testRenameTable()
$this->assertTrue($this->adapter->hasTable('table2'));
}
- public function testAddColumn()
+ public function testAddColumn(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -726,7 +723,7 @@ public function testAddColumn()
$this->assertEquals('realname', $rows[1]['Field']);
}
- public function testAddColumnWithDefaultValue()
+ public function testAddColumnWithDefaultValue(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -736,7 +733,7 @@ public function testAddColumnWithDefaultValue()
$this->assertEquals('test', $rows[1]['Default']);
}
- public function testAddColumnWithDefaultZero()
+ public function testAddColumnWithDefaultZero(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -747,7 +744,7 @@ public function testAddColumnWithDefaultZero()
$this->assertEquals('0', $rows[1]['Default']);
}
- public function testAddColumnWithDefaultEmptyString()
+ public function testAddColumnWithDefaultEmptyString(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -757,7 +754,7 @@ public function testAddColumnWithDefaultEmptyString()
$this->assertEquals('', $rows[1]['Default']);
}
- public function testAddColumnWithDefaultBoolean()
+ public function testAddColumnWithDefaultBoolean(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -771,7 +768,7 @@ public function testAddColumnWithDefaultBoolean()
$this->assertNull($rows[3]['Default']);
}
- public function testAddColumnWithDefaultLiteral()
+ public function testAddColumnWithDefaultLiteral(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -785,7 +782,7 @@ public function testAddColumnWithDefaultLiteral()
$this->assertTrue($rows[2]['Default'] === 'oh hi');
}
- public function testAddColumnFirst()
+ public function testAddColumnFirst(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -795,7 +792,7 @@ public function testAddColumnFirst()
$this->assertSame('new_id', $rows[0]['Field']);
}
- public static function integerDataProvider()
+ public static function integerDataProvider(): array
{
return [
['integer', [], 'int', '11', ''],
@@ -810,7 +807,7 @@ public static function integerDataProvider()
}
#[DataProvider('integerDataProvider')]
- public function testIntegerColumnTypes($phinx_type, $options, $sql_type, $width, $extra)
+ public function testIntegerColumnTypes(string $phinx_type, array $options, string $sql_type, string $width, string $extra): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -896,7 +893,7 @@ public function testRenameColumnWithDefaultGeneratedExtra(): void
$this->assertTrue($this->adapter->hasColumn('t', 'last_changed2'));
}
- public function testRenamingANonExistentColumn()
+ public function testRenamingANonExistentColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -909,13 +906,13 @@ public function testRenamingANonExistentColumn()
$this->assertInstanceOf(
'InvalidArgumentException',
$e,
- 'Expected exception of type InvalidArgumentException, got ' . get_class($e),
+ 'Expected exception of type InvalidArgumentException, got ' . $e::class,
);
- $this->assertEquals('The specified column doesn\'t exist: column2', $e->getMessage());
+ $this->assertEquals("The specified column doesn't exist: column2", $e->getMessage());
}
}
- public function testChangeColumn()
+ public function testChangeColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -932,7 +929,7 @@ public function testChangeColumn()
$this->assertTrue($this->adapter->hasColumn('t', 'column2'));
}
- public function testChangeColumnDefaultValue()
+ public function testChangeColumnDefaultValue(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'test'])
@@ -947,7 +944,7 @@ public function testChangeColumnDefaultValue()
$this->assertEquals('test1', $rows[1]['Default']);
}
- public function testChangeColumnDefaultToZero()
+ public function testChangeColumnDefaultToZero(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'integer')
@@ -962,7 +959,7 @@ public function testChangeColumnDefaultToZero()
$this->assertEquals('0', $rows[1]['Default']);
}
- public function testChangeColumnDefaultToNull()
+ public function testChangeColumnDefaultToNull(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'test'])
@@ -976,7 +973,7 @@ public function testChangeColumnDefaultToNull()
$this->assertNull($rows[1]['Default']);
}
- public function testChangeColumnPreservesDefaultValue()
+ public function testChangeColumnPreservesDefaultValue(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'original_default', 'null' => false, 'limit' => 100])
@@ -991,7 +988,7 @@ public function testChangeColumnPreservesDefaultValue()
$this->assertEquals('varchar(100)', $rows[1]['Type']);
}
- public function testChangeColumnPreservesDefaultValueWithDifferentType()
+ public function testChangeColumnPreservesDefaultValueWithDifferentType(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'integer', ['default' => 42, 'null' => false])
@@ -1005,7 +1002,7 @@ public function testChangeColumnPreservesDefaultValueWithDifferentType()
$this->assertEquals('NO', $rows[1]['Null']);
}
- public function testChangeColumnCanExplicitlyOverrideDefault()
+ public function testChangeColumnCanExplicitlyOverrideDefault(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'original_default'])
@@ -1018,7 +1015,7 @@ public function testChangeColumnCanExplicitlyOverrideDefault()
$this->assertEquals('new_default', $rows[1]['Default']);
}
- public function testChangeColumnCanDisablePreserveUnspecified()
+ public function testChangeColumnCanDisablePreserveUnspecified(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'original_default', 'limit' => 100])
@@ -1031,7 +1028,7 @@ public function testChangeColumnCanDisablePreserveUnspecified()
$this->assertNull($rows[1]['Default']);
}
- public function testChangeColumnWithNullTypePreservesType()
+ public function testChangeColumnWithNullTypePreservesType(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100])
@@ -1046,7 +1043,7 @@ public function testChangeColumnWithNullTypePreservesType()
$this->assertEquals('YES', $rows[1]['Null']);
}
- public function testChangeColumnWithNullTypeOnNonExistentColumnThrows()
+ public function testChangeColumnWithNullTypeOnNonExistentColumnThrows(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage("Cannot preserve column type for 'nonexistent'");
@@ -1058,7 +1055,7 @@ public function testChangeColumnWithNullTypeOnNonExistentColumnThrows()
$table->changeColumn('nonexistent', null, ['null' => true])->save();
}
- public function testUpdateColumnPreservesAttributes()
+ public function testUpdateColumnPreservesAttributes(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100, 'null' => false])
@@ -1073,7 +1070,7 @@ public function testUpdateColumnPreservesAttributes()
$this->assertEquals('YES', $rows[1]['Null']);
}
- public function testChangeColumnDoesNotPreserveByDefault()
+ public function testChangeColumnDoesNotPreserveByDefault(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100])
@@ -1088,7 +1085,7 @@ public function testChangeColumnDoesNotPreserveByDefault()
$this->assertEquals('YES', $rows[1]['Null']);
}
- public function testChangeColumnWithPreserveUnspecifiedTrue()
+ public function testChangeColumnWithPreserveUnspecifiedTrue(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100])
@@ -1103,7 +1100,7 @@ public function testChangeColumnWithPreserveUnspecifiedTrue()
$this->assertEquals('YES', $rows[1]['Null']);
}
- public function testUpdateColumnWithColumnObject()
+ public function testUpdateColumnWithColumnObject(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100, 'null' => false])
@@ -1122,7 +1119,7 @@ public function testUpdateColumnWithColumnObject()
$this->assertEquals('YES', $rows[1]['Null']);
}
- public function testUpdateColumnWithColumnObjectAndOptionsThrows()
+ public function testUpdateColumnWithColumnObjectAndOptionsThrows(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot specify options array when passing a Column object');
@@ -1140,7 +1137,7 @@ public function testUpdateColumnWithColumnObjectAndOptionsThrows()
$table->updateColumn('column1', $newColumn, ['limit' => 500]);
}
- public function testUpdateColumnWithTypeChangeToText()
+ public function testUpdateColumnWithTypeChangeToText(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['limit' => 100, 'default' => 'test'])
@@ -1160,7 +1157,7 @@ public function testUpdateColumnWithTypeChangeToText()
$this->assertStringContainsString('test', $rows[1]['Default']); // Default should be preserved
}
- public function testUpdateColumnCanRemoveLengthConstraintWithoutChangingType()
+ public function testUpdateColumnCanRemoveLengthConstraintWithoutChangingType(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['limit' => 100, 'default' => 'test'])
@@ -1180,7 +1177,7 @@ public function testUpdateColumnCanRemoveLengthConstraintWithoutChangingType()
$this->assertEquals('test', $rows[1]['Default']); // Default should be preserved
}
- public function testUpdateColumnCanRemoveScaleAndPrecision()
+ public function testUpdateColumnCanRemoveScaleAndPrecision(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'decimal', ['precision' => 10, 'scale' => 2, 'default' => '123.45'])
@@ -1199,7 +1196,7 @@ public function testUpdateColumnCanRemoveScaleAndPrecision()
$this->assertEquals('123', $rows[1]['Default']); // Default should be preserved (truncated to integer)
}
- public function testUpdateColumnCanRemoveComment()
+ public function testUpdateColumnCanRemoveComment(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['limit' => 100, 'comment' => 'Original comment', 'default' => 'test'])
@@ -1219,7 +1216,7 @@ public function testUpdateColumnCanRemoveComment()
$this->assertEquals('test', $rows[1]['Default']);
}
- public function testChangeColumnEnum()
+ public function testChangeColumnEnum(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -1234,7 +1231,7 @@ public function testChangeColumnEnum()
$this->assertEquals("enum('a','b')", $rows[1]['Type']);
}
- public static function binaryToBlobAutomaticConversionData()
+ public static function binaryToBlobAutomaticConversionData(): array
{
return [
// When creating binary with limit > 255, MySQL auto-converts to BLOB
@@ -1254,7 +1251,7 @@ public static function binaryToBlobAutomaticConversionData()
}
#[DataProvider('binaryToBlobAutomaticConversionData')]
- public function testBinaryToBlobAutomaticConversion(?int $limit, string $expectedType, ?int $expectedLimit)
+ public function testBinaryToBlobAutomaticConversion(?int $limit, string $expectedType, ?int $expectedLimit): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'binary', ['limit' => $limit])
@@ -1264,7 +1261,7 @@ public function testBinaryToBlobAutomaticConversion(?int $limit, string $expecte
$this->assertSame($expectedLimit, $columns[1]->getLimit());
}
- public static function varbinaryToBlobAutomaticConversionData()
+ public static function varbinaryToBlobAutomaticConversionData(): array
{
return [
// When creating varbinary with limit > 255, MySQL auto-converts to BLOB
@@ -1284,7 +1281,7 @@ public static function varbinaryToBlobAutomaticConversionData()
}
#[DataProvider('varbinaryToBlobAutomaticConversionData')]
- public function testVarbinaryToBlobAutomaticConversion(?int $limit, string $expectedType, ?int $expectedLimit)
+ public function testVarbinaryToBlobAutomaticConversion(?int $limit, string $expectedType, ?int $expectedLimit): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'varbinary', ['limit' => $limit])
@@ -1294,7 +1291,7 @@ public function testVarbinaryToBlobAutomaticConversion(?int $limit, string $expe
$this->assertSame($expectedLimit, $columns[1]->getLimit());
}
- public static function blobColumnsData()
+ public static function blobColumnsData(): array
{
return [
// BLOB columns with various limits - MySQL auto-selects appropriate BLOB subtype
@@ -1327,7 +1324,7 @@ public static function blobColumnsData()
}
#[DataProvider('blobColumnsData')]
- public function testblobColumns(string $type, string $expectedType, ?int $limit, ?int $expectedLimit)
+ public function testblobColumns(string $type, string $expectedType, ?int $limit, ?int $expectedLimit): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('blob_col', $type, ['limit' => $limit])
@@ -1337,7 +1334,7 @@ public function testblobColumns(string $type, string $expectedType, ?int $limit,
$this->assertSame($expectedLimit, $columns[1]->getLimit());
}
- public static function blobRoundTripData()
+ public static function blobRoundTripData(): array
{
return [
// type, limit, expected type after round-trip, expected limit after round-trip
@@ -1350,7 +1347,7 @@ public static function blobRoundTripData()
}
#[DataProvider('blobRoundTripData')]
- public function testBlobRoundTrip(string $type, ?int $limit, string $expectedType, int $expectedLimit)
+ public function testBlobRoundTrip(string $type, ?int $limit, string $expectedType, int $expectedLimit): void
{
// Create a table with a BLOB column
$table = new Table('blob_round_trip_test', [], $this->adapter);
@@ -1369,7 +1366,7 @@ public function testBlobRoundTrip(string $type, ?int $limit, string $expectedTyp
$this->adapter->dropTable('blob_round_trip_test');
}
- public static function textRoundTripData()
+ public static function textRoundTripData(): array
{
return [
// type, limit, expected type after round-trip, expected limit after round-trip
@@ -1377,11 +1374,14 @@ public static function textRoundTripData()
['text', MysqlAdapter::TEXT_TINY, 'text', MysqlAdapter::TEXT_TINY],
['text', MysqlAdapter::TEXT_MEDIUM, 'text', MysqlAdapter::TEXT_MEDIUM],
['text', MysqlAdapter::TEXT_LONG, 'text', MysqlAdapter::TEXT_LONG],
+ // Test backward compatibility: CakePHP's LENGTH_LONG (4294967295) should also work
+ // This ensures migrations generated before the fix still create LONGTEXT correctly
+ ['text', TableSchema::LENGTH_LONG, 'text', MysqlAdapter::TEXT_LONG],
];
}
#[DataProvider('textRoundTripData')]
- public function testTextRoundTrip(string $type, ?int $limit, string $expectedType, ?int $expectedLimit)
+ public function testTextRoundTrip(string $type, ?int $limit, string $expectedType, ?int $expectedLimit): void
{
// Create a table with a TEXT column
$table = new Table('text_round_trip_test', [], $this->adapter);
@@ -1400,7 +1400,7 @@ public function testTextRoundTrip(string $type, ?int $limit, string $expectedTyp
$this->adapter->dropTable('text_round_trip_test');
}
- public function testTimestampInvalidLimit()
+ public function testTimestampInvalidLimit(): void
{
$this->adapter->connect();
$version = $this->adapter->getConnection()->getDriver()->version();
@@ -1414,7 +1414,7 @@ public function testTimestampInvalidLimit()
$table->addColumn('column1', 'timestamp', ['limit' => 7])->save();
}
- public function testDropColumn()
+ public function testDropColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -1425,7 +1425,7 @@ public function testDropColumn()
$this->assertFalse($this->adapter->hasColumn('t', 'column1'));
}
- public static function columnsProvider()
+ public static function columnsProvider(): array
{
return [
['column1', 'string', []],
@@ -1455,7 +1455,7 @@ public static function columnsProvider()
}
#[DataProvider('columnsProvider')]
- public function testGetColumns($colName, $type, $options)
+ public function testGetColumns(string $colName, string $type, array $options): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn($colName, $type, $options)->save();
@@ -1486,7 +1486,7 @@ public function testGetColumns($colName, $type, $options)
}
}
- public function testGetColumnsInteger()
+ public function testGetColumnsInteger(): void
{
$colName = 'column15';
$type = 'integer';
@@ -1502,7 +1502,7 @@ public function testGetColumnsInteger()
$this->assertNull($columns[1]->getLimit());
}
- public function testGetColumnsReservedTableName()
+ public function testGetColumnsReservedTableName(): void
{
$table = new Table('group', [], $this->adapter);
$table->addColumn('column1', 'string')->save();
@@ -1510,7 +1510,7 @@ public function testGetColumnsReservedTableName()
$this->assertCount(2, $columns);
}
- public function testAddIndex()
+ public function testAddIndex(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -1521,7 +1521,7 @@ public function testAddIndex()
$this->assertTrue($table->hasIndex('email'));
}
- public function testAddIndexWithSort()
+ public function testAddIndexWithSort(): void
{
$this->adapter->connect();
if (!$this->usingMysql8()) {
@@ -1544,7 +1544,7 @@ public function testAddIndexWithSort()
$this->assertEquals($emailOrder, 'A');
}
- public function testAddMultipleFulltextIndex()
+ public function testAddMultipleFulltextIndex(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -1565,7 +1565,7 @@ public function testAddMultipleFulltextIndex()
$this->assertTrue($table->hasIndex(['email', 'bio']));
}
- public function testAddIndexWithLimit()
+ public function testAddIndexWithLimit(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -1582,7 +1582,7 @@ public function testAddIndexWithLimit()
$this->assertEquals($expected_limit, 50);
}
- public function testAddMultiIndexesWithLimitSpecifier()
+ public function testAddMultiIndexesWithLimitSpecifier(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -1606,7 +1606,7 @@ public function testAddMultiIndexesWithLimitSpecifier()
$this->assertEquals($expected_limit, 2);
}
- public function testAddSingleIndexesWithLimitSpecifier()
+ public function testAddSingleIndexesWithLimitSpecifier(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -1624,7 +1624,7 @@ public function testAddSingleIndexesWithLimitSpecifier()
$this->assertEquals($expected_limit, 3);
}
- public function testDropIndex()
+ public function testDropIndex(): void
{
// single column index
$table = new Table('table1', [], $this->adapter);
@@ -1674,7 +1674,7 @@ public function testDropIndex()
try {
$table2->removeIndex(['fname'])->save();
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
}
$this->assertTrue($table2->hasIndex(['fname', 'lname']));
@@ -1689,13 +1689,13 @@ public function testDropIndex()
try {
$table4->removeIndex(['fname'])->save();
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
}
$this->assertTrue($table4->hasIndex(['fname', 'lname']));
}
- public function testDropIndexByName()
+ public function testDropIndexByName(): void
{
// single column index
$table = new Table('table1', [], $this->adapter);
@@ -1717,7 +1717,7 @@ public function testDropIndexByName()
$this->assertFalse($table2->hasIndex(['fname', 'lname']));
}
- public function testAddForeignKey()
+ public function testAddForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -1731,7 +1731,7 @@ public function testAddForeignKey()
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testAddForeignKeyForTableWithSignedPK()
+ public function testAddForeignKeyForTableWithSignedPK(): void
{
$refTable = new Table('ref_table', ['signed' => true], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -1745,7 +1745,7 @@ public function testAddForeignKeyForTableWithSignedPK()
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testDropForeignKey()
+ public function testDropForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -1760,7 +1760,7 @@ public function testDropForeignKey()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testDropForeignKeyWithMultipleColumns()
+ public function testDropForeignKeyWithMultipleColumns(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable
@@ -1810,7 +1810,7 @@ public function testDropForeignKeyWithMultipleColumns()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_field1', 'ref_table_id']));
}
- public function testDropForeignKeyWithIdenticalMultipleColumns()
+ public function testDropForeignKeyWithIdenticalMultipleColumns(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable
@@ -1848,6 +1848,32 @@ public function testDropForeignKeyWithIdenticalMultipleColumns()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), [], 'ref_table_fk_2'));
}
+ public function testDropForeignKeyWithNameContainingWhitespace(): void
+ {
+ $refTable = new Table('ref_table', [], $this->adapter);
+ $refTable->addColumn('field1', 'string')->save();
+
+ $table = new Table('table', [], $this->adapter);
+ $key = (new ForeignKey())
+ ->setName('ref table fk')
+ ->setColumns(['ref_table_id'])
+ ->setReferencedTable('ref_table')
+ ->setReferencedColumns(['id']);
+ $table
+ ->addColumn('ref_table_id', 'integer', ['signed' => false])
+ ->addForeignKey($key)
+ ->save();
+
+ $this->assertTrue($this->adapter->hasForeignKey($table->getName(), [], 'ref table fk'));
+
+ // The constraint name must be quoted in the DROP statement, otherwise
+ // names with whitespace (or numeric names auto-assigned by MariaDB 12)
+ // produce invalid SQL.
+ $this->adapter->dropForeignKey($table->getName(), [], 'ref table fk');
+
+ $this->assertFalse($this->adapter->hasForeignKey($table->getName(), [], 'ref table fk'));
+ }
+
public static function nonExistentForeignKeyColumnsProvider(): array
{
return [
@@ -1858,11 +1884,8 @@ public static function nonExistentForeignKeyColumnsProvider(): array
];
}
- /**
- * @param array $columns
- */
#[DataProvider('nonExistentForeignKeyColumnsProvider')]
- public function testDropForeignKeyByNonExistentKeyColumns(array $columns)
+ public function testDropForeignKeyByNonExistentKeyColumns(array $columns): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable
@@ -1890,7 +1913,7 @@ public function testDropForeignKeyByNonExistentKeyColumns(array $columns)
$this->adapter->dropForeignKey($table->getName(), $columns);
}
- public function testDropForeignKeyCaseInsensitivity()
+ public function testDropForeignKeyCaseInsensitivity(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->save();
@@ -1905,7 +1928,7 @@ public function testDropForeignKeyCaseInsensitivity()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testDropForeignKeyByName()
+ public function testDropForeignKeyByName(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->save();
@@ -1925,7 +1948,7 @@ public function testDropForeignKeyByName()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testDropForeignKeyForTableWithSignedPK()
+ public function testDropForeignKeyForTableWithSignedPK(): void
{
$refTable = new Table('ref_table', ['signed' => true], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -1940,7 +1963,7 @@ public function testDropForeignKeyForTableWithSignedPK()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testDropForeignKeyAsString()
+ public function testDropForeignKeyAsString(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -1956,7 +1979,7 @@ public function testDropForeignKeyAsString()
}
#[DataProvider('provideForeignKeysToCheck')]
- public function testHasForeignKey($tableDef, $key, $exp)
+ public function testHasForeignKey(string $tableDef, string|array $key, bool $exp): void
{
$conn = $this->adapter->getConnection();
$conn->execute('CREATE TABLE other(a int, b int, c int, key(a), key(b), key(a,b), key(a,b,c));');
@@ -1964,7 +1987,7 @@ public function testHasForeignKey($tableDef, $key, $exp)
$this->assertSame($exp, $this->adapter->hasForeignKey('t', $key));
}
- public static function provideForeignKeysToCheck()
+ public static function provideForeignKeysToCheck(): array
{
return [
['create table t(a int)', 'a', false],
@@ -1990,7 +2013,7 @@ public static function provideForeignKeysToCheck()
];
}
- public function testHasForeignKeyAsString()
+ public function testHasForeignKeyAsString(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -2005,7 +2028,7 @@ public function testHasForeignKeyAsString()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), 'ref_table_id2'));
}
- public function testHasNamedForeignKey()
+ public function testHasNamedForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -2028,7 +2051,7 @@ public function testHasNamedForeignKey()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), [], 'my_constraint2'));
}
- public function testHasForeignKeyWithConstraintForTableWithSignedPK()
+ public function testHasForeignKeyWithConstraintForTableWithSignedPK(): void
{
$refTable = new Table('ref_table', ['signed' => true], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -2048,7 +2071,7 @@ public function testHasForeignKeyWithConstraintForTableWithSignedPK()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'], 'my_constraint2'));
}
- public function testsHasForeignKeyWithSchemaDotTableName()
+ public function testsHasForeignKeyWithSchemaDotTableName(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -2063,13 +2086,13 @@ public function testsHasForeignKeyWithSchemaDotTableName()
$this->assertFalse($this->adapter->hasForeignKey($this->config['database'] . '.' . $table->getName(), ['ref_table_id2']));
}
- public function testHasDatabase()
+ public function testHasDatabase(): void
{
$this->assertFalse($this->adapter->hasDatabase('fake_database_name'));
$this->assertTrue($this->adapter->hasDatabase($this->config['database']));
}
- public function testDropDatabase()
+ public function testDropDatabase(): void
{
$this->assertFalse($this->adapter->hasDatabase('phinx_temp_database'));
$this->adapter->createDatabase('phinx_temp_database');
@@ -2077,7 +2100,7 @@ public function testDropDatabase()
$this->adapter->dropDatabase('phinx_temp_database');
}
- public function testAddColumnWithComment()
+ public function testAddColumnWithComment(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'string', ['comment' => $comment = 'Comments from "column1"'])
@@ -2096,7 +2119,7 @@ public function testAddColumnWithComment()
$this->assertEquals($comment, $columnWithComment['COLUMN_COMMENT'], "Didn't set column comment correctly");
}
- public function testAddColumnEnum()
+ public function testAddColumnEnum(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -2130,7 +2153,7 @@ public function testAddColumnEnum()
$this->assertEquals($comment, $columnWithComment['COLUMN_COMMENT'], "Didn't set column comment correctly");
}
- public function testAddGeoSpatialColumns()
+ public function testAddGeoSpatialColumns(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -2141,7 +2164,7 @@ public function testAddGeoSpatialColumns()
$this->assertEquals('geometry', $rows[1]['Type']);
}
- public function testHasColumn()
+ public function testHasColumn(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -2151,7 +2174,7 @@ public function testHasColumn()
$this->assertTrue($table->hasColumn('column1'));
}
- public function testHasColumnReservedName()
+ public function testHasColumnReservedName(): void
{
$tableQuoted = new Table('group', [], $this->adapter);
$tableQuoted->addColumn('value', 'string')
@@ -2161,7 +2184,7 @@ public function testHasColumnReservedName()
$this->assertTrue($tableQuoted->hasColumn('value'));
}
- public function testBulkInsertData()
+ public function testBulkInsertData(): void
{
$data = [
[
@@ -2195,7 +2218,7 @@ public function testBulkInsertData()
$this->assertEquals('test', $rows[2]['column3']);
}
- public function testBulkInsertLiteral()
+ public function testBulkInsertLiteral(): void
{
$data = [
[
@@ -2221,12 +2244,12 @@ public function testBulkInsertLiteral()
$this->assertEquals('value1', $rows[0]['column1']);
$this->assertEquals('value2', $rows[1]['column1']);
$this->assertEquals('value3', $rows[2]['column1']);
- $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']);
+ $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column2']);
$this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']);
$this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']);
}
- public function testInsertData()
+ public function testInsertData(): void
{
$data = [
[
@@ -2261,7 +2284,7 @@ public function testInsertData()
$this->assertEquals('foo', $rows[2]['column3']);
}
- public function testInsertLiteral()
+ public function testInsertLiteral(): void
{
$data = [
[
@@ -2292,12 +2315,12 @@ public function testInsertLiteral()
$this->assertEquals('test', $rows[0]['column2']);
$this->assertEquals('test', $rows[1]['column2']);
$this->assertEquals('foo', $rows[2]['column2']);
- $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']);
+ $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column3']);
$this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']);
$this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']);
}
- public function testDumpCreateTable()
+ public function testDumpCreateTable(): void
{
$options = $this->adapter->getOptions();
$options['dryrun'] = true;
@@ -2310,7 +2333,7 @@ public function testDumpCreateTable()
->addColumn('column3', 'string', ['default' => 'test', 'null' => false])
->save();
- $actualOutput = join("\n", $this->out->messages());
+ $actualOutput = implode("\n", $this->out->messages());
// MySQL version affects default collation (8.0.0+ uses utf8mb4_0900_ai_ci, older uses utf8mb4_general_ci)
// MariaDB 11.8 uses: utf8mb4_uca1400_ai_ci
$this->assertMatchesRegularExpression(
@@ -2325,7 +2348,7 @@ public function testDumpCreateTable()
* Then enables dry run mode and inserts a record.
* Asserts that the insert statement is output and doesn't insert a record.
*/
- public function testDumpInsert()
+ public function testDumpInsert(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -2350,13 +2373,13 @@ public function testDumpInsert()
INSERT INTO `table1` (`string_col`) VALUES (null);
INSERT INTO `table1` (`int_col`) VALUES (23);
OUTPUT;
- $actualOutput = join("\n", $this->out->messages());
+ $actualOutput = implode("\n", $this->out->messages());
// Add this to be LF - CR/LF systems independent
$expectedOutput = preg_replace('~\R~u', '', $expectedOutput);
$actualOutput = preg_replace('~\R~u', '', $actualOutput);
- $this->assertStringContainsString($expectedOutput, trim($actualOutput), 'Passing the --dry-run option doesn\'t dump the insert to the output');
+ $this->assertStringContainsString($expectedOutput, trim((string)$actualOutput), "Passing the --dry-run option doesn't dump the insert to the output");
$countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1');
$this->assertTrue($countQuery->execute());
@@ -2369,7 +2392,7 @@ public function testDumpInsert()
* Then enables dry run mode and inserts some records.
* Asserts that output contains the insert statement and doesn't insert any record.
*/
- public function testDumpBulkinsert()
+ public function testDumpBulkinsert(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -2391,8 +2414,8 @@ public function testDumpBulkinsert()
$expectedOutput = <<<'OUTPUT'
INSERT INTO `table1` (`string_col`, `int_col`) VALUES ('test_data1', 23), (null, 42);
OUTPUT;
- $actualOutput = join("\n", $this->out->messages());
- $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option doesn\'t dump the bulkinsert to the output');
+ $actualOutput = implode("\n", $this->out->messages());
+ $this->assertStringContainsString($expectedOutput, $actualOutput, "Passing the --dry-run option doesn't dump the bulkinsert to the output");
$countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1');
$this->assertTrue($countQuery->execute());
@@ -2400,7 +2423,7 @@ public function testDumpBulkinsert()
$this->assertEquals(0, $res[0]['COUNT(*)']);
}
- public function testDumpCreateTableAndThenInsert()
+ public function testDumpCreateTableAndThenInsert(): void
{
$options = $this->adapter->getOptions();
$options['dryrun'] = true;
@@ -2418,7 +2441,7 @@ public function testDumpCreateTableAndThenInsert()
'column2' => 1,
])->save();
- $actualOutput = join("\n", $this->out->messages());
+ $actualOutput = implode("\n", $this->out->messages());
// Add this to be LF - CR/LF systems independent
$actualOutput = preg_replace('~\R~u', '', $actualOutput);
// MySQL version affects default collation (8.0.0+ uses utf8mb4_0900_ai_ci, older uses utf8mb4_general_ci)
@@ -2432,7 +2455,7 @@ public function testDumpCreateTableAndThenInsert()
/**
* Tests interaction with the query builder
*/
- public function testQueryBuilder()
+ public function testQueryBuilder(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -2471,7 +2494,7 @@ public function testQueryBuilder()
$this->assertEquals(1, $stm->rowCount());
}
- public function testQueryWithParams()
+ public function testQueryWithParams(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -2502,7 +2525,7 @@ public function testQueryWithParams()
$this->assertEquals(3, $res[0]['c']);
}
- public static function geometryTypeProvider()
+ public static function geometryTypeProvider(): array
{
return [
[MysqlAdapter::TYPE_GEOMETRY, 'POINT(0 0)'],
@@ -2512,12 +2535,8 @@ public static function geometryTypeProvider()
];
}
- /**
- * @param string $type
- * @param string $geom
- */
#[DataProvider('geometryTypeProvider')]
- public function testGeometrySridSupport($type, $geom)
+ public function testGeometrySridSupport(string $type, string $geom): void
{
$this->adapter->connect();
if (!$this->usingMysql8()) {
@@ -2529,19 +2548,15 @@ public function testGeometrySridSupport($type, $geom)
->addColumn('geom', $type, ['srid' => 4326])
->save();
- $this->adapter->execute("INSERT INTO table1 (`geom`) VALUES (ST_GeomFromText('{$geom}', 4326))");
+ $this->adapter->execute(sprintf("INSERT INTO table1 (`geom`) VALUES (ST_GeomFromText('%s', 4326))", $geom));
$rows = $this->adapter->fetchAll('SELECT ST_AsWKT(geom) as wkt, ST_SRID(geom) as srid FROM table1');
$this->assertCount(1, $rows);
$this->assertSame($geom, $rows[0]['wkt']);
$this->assertSame(4326, (int)$rows[0]['srid']);
}
- /**
- * @param string $type
- * @param string $geom
- */
#[DataProvider('geometryTypeProvider')]
- public function testGeometrySridThrowsInsertDifferentSrid($type, $geom)
+ public function testGeometrySridThrowsInsertDifferentSrid(string $type, string $geom): void
{
$this->adapter->connect();
if (!$this->usingMysql8()) {
@@ -2555,10 +2570,10 @@ public function testGeometrySridThrowsInsertDifferentSrid($type, $geom)
$this->expectException(PDOException::class);
$this->expectExceptionMessage("SQLSTATE[HY000]: General error: 3643 The SRID of the geometry does not match the SRID of the column 'geom'. The SRID of the geometry is 4322, but the SRID of the column is 4326. Consider changing the SRID of the geometry or the SRID property of the column.");
- $this->adapter->execute("INSERT INTO table1 (`geom`) VALUES (ST_GeomFromText('{$geom}', 4322))");
+ $this->adapter->execute(sprintf("INSERT INTO table1 (`geom`) VALUES (ST_GeomFromText('%s', 4322))", $geom));
}
- public static function defaultsCastAsExpressions()
+ public static function defaultsCastAsExpressions(): array
{
return [
[MysqlAdapter::TYPE_JSON, '{"a": true}'],
@@ -2573,20 +2588,21 @@ public static function defaultsCastAsExpressions()
/**
* MySQL 8 added support for specifying defaults for the BLOB, TEXT, GEOMETRY, and JSON data types,
* however requiring that they be wrapped in expressions.
- *
- * @param string $type
- * @param string $default
*/
#[DataProvider('defaultsCastAsExpressions')]
public function testDefaultsCastAsExpressionsForCertainTypes(string $type, string $default): void
{
if (
- $this->usingMariaDb() && in_array($type, [
- MysqlAdapter::TYPE_GEOMETRY,
- MysqlAdapter::TYPE_POINT,
- MysqlAdapter::TYPE_LINESTRING,
- MysqlAdapter::TYPE_POLYGON,
- ])
+ $this->usingMariaDb() && in_array(
+ $type,
+ [
+ MysqlAdapter::TYPE_GEOMETRY,
+ MysqlAdapter::TYPE_POINT,
+ MysqlAdapter::TYPE_LINESTRING,
+ MysqlAdapter::TYPE_POLYGON,
+ ],
+ true,
+ )
) {
$this->markTestSkipped('GIS is broken with MariaDB');
}
@@ -2608,13 +2624,13 @@ public function testDefaultsCastAsExpressionsForCertainTypes(string $type, strin
$actualDefault = $columns[0]->getDefault();
// Normalize quote handling - both MariaDB and MySQL 8.0.13+ may return defaults with quotes
- if (str_starts_with($actualDefault, "'") && str_ends_with($actualDefault, "'")) {
- $actualDefault = substr($actualDefault, 1, -1);
+ if (str_starts_with((string)$actualDefault, "'") && str_ends_with((string)$actualDefault, "'")) {
+ $actualDefault = substr((string)$actualDefault, 1, -1);
}
$this->assertSame($default, $actualDefault);
}
- public function testCreateTableWithPrecisionCurrentTimestamp()
+ public function testCreateTableWithPrecisionCurrentTimestamp(): void
{
$this->adapter->connect();
(new Table('exampleCurrentTimestamp3', ['id' => false], $this->adapter))
@@ -2633,7 +2649,7 @@ public function testCreateTableWithPrecisionCurrentTimestamp()
$this->assertEqualsIgnoringCase('CURRENT_TIMESTAMP(3)', $colDef['COLUMN_DEFAULT']);
}
- public function testAddCheckConstraint()
+ public function testAddCheckConstraint(): void
{
$table = new Table('check_table', [], $this->adapter);
$table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2])
@@ -2645,7 +2661,7 @@ public function testAddCheckConstraint()
$this->assertTrue($this->adapter->hasCheckConstraint('check_table', 'price_positive'));
}
- public function testAddCheckConstraintWithAutoGeneratedName()
+ public function testAddCheckConstraintWithAutoGeneratedName(): void
{
$table = new Table('check_table2', [], $this->adapter);
$table->addColumn('age', 'integer')
@@ -2661,11 +2677,13 @@ public function testAddCheckConstraintWithAutoGeneratedName()
$dialect = $driver->schemaDialect();
$constraints = $dialect->describeCheckConstraints('check_table2');
$this->assertCount(1, $constraints);
- $expected = $driver->isMariaDb() ? 'CONSTRAINT_1' : 'check_table2_chk_';
- $this->assertStringContainsString($expected, $constraints[0]['name']);
+ // The name is generated by migrations (not left to the DB), so it is
+ // identical on MySQL and MariaDB instead of the DB fallback CONSTRAINT_1.
+ $expectedName = 'check_table2_chk_' . substr(md5('age >= 18'), 0, 8);
+ $this->assertSame($expectedName, $constraints[0]['name']);
}
- public function testHasCheckConstraint()
+ public function testHasCheckConstraint(): void
{
$table = new Table('check_table3', [], $this->adapter);
$table->addColumn('quantity', 'integer')
@@ -2679,7 +2697,7 @@ public function testHasCheckConstraint()
$this->assertTrue($this->adapter->hasCheckConstraint('check_table3', 'quantity_positive'));
}
- public function testDropCheckConstraint()
+ public function testDropCheckConstraint(): void
{
$table = new Table('check_table4', [], $this->adapter);
$table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2])
@@ -2693,7 +2711,7 @@ public function testDropCheckConstraint()
$this->assertFalse($this->adapter->hasCheckConstraint('check_table4', 'price_check'));
}
- public function testCheckConstraintWithComplexExpression()
+ public function testCheckConstraintWithComplexExpression(): void
{
$table = new Table('check_table5', [], $this->adapter);
$table->addColumn('email', 'string', ['limit' => 255])
@@ -2710,7 +2728,7 @@ public function testCheckConstraintWithComplexExpression()
// Verify the constraint is actually enforced
$quotedTableName = $this->adapter->getConnection()->getDriver()->quoteIdentifier('check_table5');
$this->expectException(PDOException::class);
- $this->adapter->execute("INSERT INTO {$quotedTableName} (email, status) VALUES ('test@example.com', 'invalid')");
+ $this->adapter->execute(sprintf("INSERT INTO %s (email, status) VALUES ('test@example.com', 'invalid')", $quotedTableName));
}
/**
@@ -2723,7 +2741,7 @@ public function testCheckConstraintWithComplexExpression()
* The 5.x branch uses CakePHP's database layer instead of phinx,
* so we need to verify it handles scale=0 correctly.
*/
- public function testDecimalWithScaleZero()
+ public function testDecimalWithScaleZero(): void
{
// Create table with DECIMAL(65,0)
$table = new Table('decimal_scale_zero_test', [], $this->adapter);
@@ -2757,7 +2775,7 @@ public function testDecimalWithScaleZero()
);
}
- public function testInsertOrSkipWithDuplicates()
+ public function testInsertOrSkipWithDuplicates(): void
{
$table = new Table('users', [], $this->adapter);
$table->addColumn('email', 'string', ['limit' => 255])
@@ -2780,7 +2798,7 @@ public function testInsertOrSkipWithDuplicates()
$this->assertEquals('John', $rows[0]['name']);
}
- public function testInsertModeResetsAfterInsertOrSkip()
+ public function testInsertModeResetsAfterInsertOrSkip(): void
{
$table = new Table('users', [], $this->adapter);
$table->addColumn('email', 'string', ['limit' => 255])
@@ -2800,7 +2818,7 @@ public function testInsertModeResetsAfterInsertOrSkip()
])->save();
}
- public function testBulkinsertOrSkipWithDuplicates()
+ public function testBulkinsertOrSkipWithDuplicates(): void
{
$table = new Table('products', [], $this->adapter);
$table->addColumn('sku', 'string', ['limit' => 50])
@@ -2827,7 +2845,7 @@ public function testBulkinsertOrSkipWithDuplicates()
$this->assertEquals('30.00', $rows[2]['price']);
}
- public function testInsertOrSkipWithoutDuplicates()
+ public function testInsertOrSkipWithoutDuplicates(): void
{
$table = new Table('categories', [], $this->adapter);
$table->addColumn('name', 'string')
@@ -2843,7 +2861,111 @@ public function testInsertOrSkipWithoutDuplicates()
$this->assertCount(2, $rows);
}
- public function testAddColumnWithAlgorithmInstant()
+ public function testCreateView(): void
+ {
+ // Create a base table
+ $table = new Table('users', [], $this->adapter);
+ $table->addColumn('name', 'string')
+ ->addColumn('email', 'string')
+ ->create();
+
+ // Insert some data
+ $table->insert([
+ ['name' => 'Alice', 'email' => 'alice@example.com'],
+ ['name' => 'Bob', 'email' => 'bob@example.com'],
+ ])->save();
+
+ // Create a view
+ $viewTable = new Table('user_emails', [], $this->adapter);
+ $viewTable->createView('user_emails', 'SELECT name, email FROM users')
+ ->save();
+
+ // Query the view
+ $rows = $this->adapter->fetchAll('SELECT * FROM user_emails');
+ $this->assertCount(2, $rows);
+ $this->assertEquals('Alice', $rows[0]['name']);
+ $this->assertEquals('alice@example.com', $rows[0]['email']);
+ }
+
+ public function testDropView(): void
+ {
+ // Create a base table
+ $table = new Table('users', [], $this->adapter);
+ $table->addColumn('name', 'string')->create();
+
+ // Create a view
+ $viewTable = new Table('user_names', [], $this->adapter);
+ $viewTable->createView('user_names', 'SELECT name FROM users')->save();
+
+ // Verify view exists
+ $rows = $this->adapter->fetchAll('SELECT * FROM user_names');
+ $this->assertIsArray($rows);
+
+ // Drop the view
+ $viewTable->dropView('user_names')->save();
+
+ // Verify view is dropped
+ $this->expectException(PDOException::class);
+ $this->adapter->fetchAll('SELECT * FROM user_names');
+ }
+
+ public function testCreateTrigger(): void
+ {
+ // Create tables
+ $table = new Table('users', [], $this->adapter);
+ $table->addColumn('name', 'string')
+ ->addColumn('created_count', 'integer', ['default' => 0])
+ ->create();
+
+ $logTable = new Table('user_log', [], $this->adapter);
+ $logTable->addColumn('action', 'string')->create();
+
+ // Create a trigger
+ $table->createTrigger(
+ 'log_user_insert',
+ 'INSERT',
+ "INSERT INTO user_log (action) VALUES ('user_created')",
+ ['timing' => 'AFTER'],
+ )->save();
+
+ // Insert data to trigger the trigger
+ $table->insert(['name' => 'Alice', 'created_count' => 0])->save();
+
+ // Verify trigger fired
+ $rows = $this->adapter->fetchAll('SELECT * FROM user_log');
+ $this->assertCount(1, $rows);
+ $this->assertEquals('user_created', $rows[0]['action']);
+ }
+
+ public function testDropTrigger(): void
+ {
+ // Create table
+ $table = new Table('users', [], $this->adapter);
+ $table->addColumn('name', 'string')->create();
+
+ $logTable = new Table('user_log', [], $this->adapter);
+ $logTable->addColumn('action', 'string')->create();
+
+ // Create a trigger
+ $table->createTrigger(
+ 'log_user_insert',
+ 'INSERT',
+ "INSERT INTO user_log (action) VALUES ('user_created')",
+ ['timing' => 'AFTER'],
+ )->save();
+
+ // Drop the trigger
+ $table->dropTrigger('log_user_insert')->save();
+
+ // Insert data - trigger should not fire
+ $table->insert(['name' => 'Bob'])->save();
+
+ // Verify trigger did not fire
+ $rows = $this->adapter->fetchAll('SELECT * FROM user_log');
+ $this->assertCount(0, $rows);
+ }
+
+ public function testAddColumnWithAlgorithmInstant(): void
{
$table = new Table('users', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -2857,7 +2979,7 @@ public function testAddColumnWithAlgorithmInstant()
$this->assertTrue($this->adapter->hasColumn('users', 'status'));
}
- public function testAddColumnWithAlgorithmAndLock()
+ public function testAddColumnWithAlgorithmAndLock(): void
{
$table = new Table('products', [], $this->adapter);
$table->addColumn('name', 'string')
@@ -2875,7 +2997,7 @@ public function testAddColumnWithAlgorithmAndLock()
$this->assertTrue($this->adapter->hasColumn('products', 'price'));
}
- public function testChangeColumnWithAlgorithm()
+ public function testChangeColumnWithAlgorithm(): void
{
$table = new Table('items', [], $this->adapter);
$table->addColumn('description', 'string', ['limit' => 100])
@@ -2895,7 +3017,7 @@ public function testChangeColumnWithAlgorithm()
}
}
- public function testBatchedOperationsWithSameAlgorithm()
+ public function testBatchedOperationsWithSameAlgorithm(): void
{
$table = new Table('batch_test', [], $this->adapter);
$table->addColumn('col1', 'string')
@@ -2915,7 +3037,7 @@ public function testBatchedOperationsWithSameAlgorithm()
$this->assertTrue($this->adapter->hasColumn('batch_test', 'col3'));
}
- public function testBatchedOperationsWithConflictingAlgorithmsThrowsException()
+ public function testBatchedOperationsWithConflictingAlgorithmsThrowsException(): void
{
$table = new Table('conflict_test', [], $this->adapter);
$table->addColumn('col1', 'string')
@@ -2935,7 +3057,7 @@ public function testBatchedOperationsWithConflictingAlgorithmsThrowsException()
->update();
}
- public function testBatchedOperationsWithConflictingLocksThrowsException()
+ public function testBatchedOperationsWithConflictingLocksThrowsException(): void
{
$table = new Table('lock_conflict_test', [], $this->adapter);
$table->addColumn('col1', 'string')
@@ -2957,7 +3079,7 @@ public function testBatchedOperationsWithConflictingLocksThrowsException()
->update();
}
- public function testInvalidAlgorithmThrowsException()
+ public function testInvalidAlgorithmThrowsException(): void
{
$table = new Table('invalid_algo', [], $this->adapter);
$table->addColumn('col1', 'string')
@@ -2971,7 +3093,7 @@ public function testInvalidAlgorithmThrowsException()
])->update();
}
- public function testInvalidLockThrowsException()
+ public function testInvalidLockThrowsException(): void
{
$table = new Table('invalid_lock', [], $this->adapter);
$table->addColumn('col1', 'string')
@@ -2985,7 +3107,7 @@ public function testInvalidLockThrowsException()
])->update();
}
- public function testAlgorithmInstantWithExplicitLockThrowsException()
+ public function testAlgorithmInstantWithExplicitLockThrowsException(): void
{
$table = new Table('instant_lock_test', [], $this->adapter);
$table->addColumn('col1', 'string')
@@ -3001,7 +3123,7 @@ public function testAlgorithmInstantWithExplicitLockThrowsException()
])->update();
}
- public function testAlgorithmConstantsAreDefined()
+ public function testAlgorithmConstantsAreDefined(): void
{
$this->assertEquals('DEFAULT', MysqlAdapter::ALGORITHM_DEFAULT);
$this->assertEquals('INSTANT', MysqlAdapter::ALGORITHM_INSTANT);
@@ -3009,7 +3131,7 @@ public function testAlgorithmConstantsAreDefined()
$this->assertEquals('COPY', MysqlAdapter::ALGORITHM_COPY);
}
- public function testLockConstantsAreDefined()
+ public function testLockConstantsAreDefined(): void
{
$this->assertEquals('DEFAULT', MysqlAdapter::LOCK_DEFAULT);
$this->assertEquals('NONE', MysqlAdapter::LOCK_NONE);
@@ -3017,7 +3139,7 @@ public function testLockConstantsAreDefined()
$this->assertEquals('EXCLUSIVE', MysqlAdapter::LOCK_EXCLUSIVE);
}
- public function testAlgorithmWithMixedCase()
+ public function testAlgorithmWithMixedCase(): void
{
$table = new Table('mixed_case', [], $this->adapter);
$table->addColumn('col1', 'string')
@@ -3033,7 +3155,283 @@ public function testAlgorithmWithMixedCase()
$this->assertTrue($this->adapter->hasColumn('mixed_case', 'col2'));
}
- public function testInsertOrUpdateWithDuplicates()
+ public function testAddColumnWithAlgorithmAndLockSqlContainsClause(): void
+ {
+ $table = new Table('col_sql_verify', [], $this->adapter);
+ $table->addColumn('col1', 'string')
+ ->create();
+
+ $this->io->level(ConsoleIo::VERBOSE);
+ $this->out->clear();
+
+ $table->addColumn('col2', 'string', [
+ 'null' => true,
+ 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
+ 'lock' => MysqlAdapter::LOCK_NONE,
+ ])->update();
+
+ $output = $this->out->output();
+ $this->assertStringContainsString('ALGORITHM=INPLACE', $output);
+ $this->assertStringContainsString('LOCK=NONE', $output);
+ $this->assertTrue($this->adapter->hasColumn('col_sql_verify', 'col2'));
+ }
+
+ public function testAddColumnWithFluentColumnBuilder(): void
+ {
+ $table = new Table('col_fluent', [], $this->adapter);
+ $table->addColumn('col1', 'string')
+ ->create();
+
+ $column = new Column();
+ $column->setName('col2')
+ ->setType('string')
+ ->setNull(true)
+ ->setAlgorithm(MysqlAdapter::ALGORITHM_INPLACE)
+ ->setLock(MysqlAdapter::LOCK_NONE);
+
+ $this->io->level(ConsoleIo::VERBOSE);
+ $this->out->clear();
+
+ $table->addColumn($column)->update();
+
+ $output = $this->out->output();
+ $this->assertStringContainsString('ALGORITHM=INPLACE', $output);
+ $this->assertStringContainsString('LOCK=NONE', $output);
+ $this->assertTrue($this->adapter->hasColumn('col_fluent', 'col2'));
+ }
+
+ public function testAddIndexWithAlgorithm(): void
+ {
+ $table = new Table('index_algo', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->create();
+
+ $table->addIndex('email', [
+ 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
+ ])->update();
+
+ $this->assertTrue($this->adapter->hasIndex('index_algo', ['email']));
+ }
+
+ public function testAddIndexWithAlgorithmAndLock(): void
+ {
+ $table = new Table('index_algo_lock', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->create();
+
+ $table->addIndex('email', [
+ 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
+ 'lock' => MysqlAdapter::LOCK_NONE,
+ ])->update();
+
+ $this->assertTrue($this->adapter->hasIndex('index_algo_lock', ['email']));
+ }
+
+ public function testAddIndexWithAlgorithmCopy(): void
+ {
+ $table = new Table('index_copy', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->create();
+
+ $table->addIndex('email', [
+ 'algorithm' => MysqlAdapter::ALGORITHM_COPY,
+ ])->update();
+
+ $this->assertTrue($this->adapter->hasIndex('index_copy', ['email']));
+ }
+
+ public function testAddIndexWithAlgorithmMixedCase(): void
+ {
+ $table = new Table('index_case', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->create();
+
+ $table->addIndex('email', [
+ 'algorithm' => 'inplace',
+ 'lock' => 'none',
+ ])->update();
+
+ $this->assertTrue($this->adapter->hasIndex('index_case', ['email']));
+ }
+
+ public function testAddIndexWithInvalidAlgorithmThrowsException(): void
+ {
+ $table = new Table('index_invalid_algo', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->create();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Invalid algorithm');
+
+ $table->addIndex('email', [
+ 'algorithm' => 'INVALID',
+ ])->update();
+ }
+
+ public function testAddIndexWithInvalidLockThrowsException(): void
+ {
+ $table = new Table('index_invalid_lock', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->create();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Invalid lock');
+
+ $table->addIndex('email', [
+ 'lock' => 'INVALID',
+ ])->update();
+ }
+
+ public function testAddIndexWithAlgorithmInstantAndExplicitLockThrowsException(): void
+ {
+ $table = new Table('index_instant_lock', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->create();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('ALGORITHM=INSTANT cannot be combined with LOCK=NONE');
+
+ $table->addIndex('email', [
+ 'algorithm' => MysqlAdapter::ALGORITHM_INSTANT,
+ 'lock' => MysqlAdapter::LOCK_NONE,
+ ])->update();
+ }
+
+ public function testBatchedIndexesWithSameAlgorithm(): void
+ {
+ $table = new Table('index_batch', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->addColumn('name', 'string')
+ ->create();
+
+ $table->addIndex('email', [
+ 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
+ 'lock' => MysqlAdapter::LOCK_NONE,
+ ])
+ ->addIndex('name', [
+ 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
+ 'lock' => MysqlAdapter::LOCK_NONE,
+ ])
+ ->update();
+
+ $this->assertTrue($this->adapter->hasIndex('index_batch', ['email']));
+ $this->assertTrue($this->adapter->hasIndex('index_batch', ['name']));
+ }
+
+ public function testBatchedIndexesWithConflictingAlgorithmsThrowsException(): void
+ {
+ $table = new Table('index_batch_conflict', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->addColumn('name', 'string')
+ ->create();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Conflicting algorithm specifications');
+
+ $table->addIndex('email', [
+ 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
+ ])
+ ->addIndex('name', [
+ 'algorithm' => MysqlAdapter::ALGORITHM_COPY,
+ ])
+ ->update();
+ }
+
+ public function testBatchedIndexesWithConflictingLocksThrowsException(): void
+ {
+ $table = new Table('index_lock_conflict', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->addColumn('name', 'string')
+ ->create();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Conflicting lock specifications');
+
+ $table->addIndex('email', [
+ 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
+ 'lock' => MysqlAdapter::LOCK_NONE,
+ ])
+ ->addIndex('name', [
+ 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
+ 'lock' => MysqlAdapter::LOCK_SHARED,
+ ])
+ ->update();
+ }
+
+ public function testAddFulltextIndexWithAlgorithmAndLock(): void
+ {
+ $table = new Table('index_fulltext_algo', [], $this->adapter);
+ $table->addColumn('content', 'text')
+ ->create();
+
+ $table->addIndex('content', [
+ 'type' => 'fulltext',
+ 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
+ 'lock' => MysqlAdapter::LOCK_SHARED,
+ ])->update();
+
+ $this->assertTrue($this->adapter->hasIndex('index_fulltext_algo', ['content']));
+ }
+
+ public function testAddFulltextIndexWithInstantAndLockThrowsException(): void
+ {
+ $table = new Table('index_fulltext_instant', [], $this->adapter);
+ $table->addColumn('content', 'text')
+ ->create();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('ALGORITHM=INSTANT cannot be combined with LOCK=NONE');
+
+ $table->addIndex('content', [
+ 'type' => 'fulltext',
+ 'algorithm' => MysqlAdapter::ALGORITHM_INSTANT,
+ 'lock' => MysqlAdapter::LOCK_NONE,
+ ])->update();
+ }
+
+ public function testAddIndexWithAlgorithmAndLockSqlContainsClause(): void
+ {
+ $table = new Table('idx_sql_verify', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->create();
+
+ $this->io->level(ConsoleIo::VERBOSE);
+ $this->out->clear();
+
+ $table->addIndex('email', [
+ 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE,
+ 'lock' => MysqlAdapter::LOCK_NONE,
+ ])->update();
+
+ $output = $this->out->output();
+ $this->assertStringContainsString('ALGORITHM=INPLACE', $output);
+ $this->assertStringContainsString('LOCK=NONE', $output);
+ $this->assertTrue($this->adapter->hasIndex('idx_sql_verify', ['email']));
+ }
+
+ public function testAddIndexWithFluentIndexBuilder(): void
+ {
+ $table = new Table('idx_fluent', [], $this->adapter);
+ $table->addColumn('email', 'string')
+ ->create();
+
+ $index = new Index();
+ $index->setColumns('email')
+ ->setAlgorithm(MysqlAdapter::ALGORITHM_INPLACE)
+ ->setLock(MysqlAdapter::LOCK_NONE);
+
+ $this->io->level(ConsoleIo::VERBOSE);
+ $this->out->clear();
+
+ $table->addIndex($index)->update();
+
+ $output = $this->out->output();
+ $this->assertStringContainsString('ALGORITHM=INPLACE', $output);
+ $this->assertStringContainsString('LOCK=NONE', $output);
+ $this->assertTrue($this->adapter->hasIndex('idx_fluent', ['email']));
+ }
+
+ public function testInsertOrUpdateWithDuplicates(): void
{
$table = new Table('currencies', [], $this->adapter);
$table->addColumn('code', 'string', ['limit' => 3])
@@ -3066,7 +3464,7 @@ public function testInsertOrUpdateWithDuplicates()
$this->assertEquals('1.0500', $rows[2]['rate']); // USD updated
}
- public function testInsertOrUpdateWithMultipleUpdateColumns()
+ public function testInsertOrUpdateWithMultipleUpdateColumns(): void
{
$table = new Table('products', [], $this->adapter);
$table->addColumn('sku', 'string', ['limit' => 50])
@@ -3091,7 +3489,7 @@ public function testInsertOrUpdateWithMultipleUpdateColumns()
$this->assertEquals(50, $rows[0]['stock']);
}
- public function testInsertOrUpdateModeResetsAfterSave()
+ public function testInsertOrUpdateModeResetsAfterSave(): void
{
$table = new Table('items', [], $this->adapter);
$table->addColumn('code', 'string', ['limit' => 10])
@@ -3111,7 +3509,7 @@ public function testInsertOrUpdateModeResetsAfterSave()
])->save();
}
- public function testInsertOrUpdateWithEmptyConflictColumnsDoesNotWarn()
+ public function testInsertOrUpdateWithEmptyConflictColumnsDoesNotWarn(): void
{
$table = new Table('currencies', [], $this->adapter);
$table->addColumn('code', 'string', ['limit' => 3])
@@ -3120,7 +3518,7 @@ public function testInsertOrUpdateWithEmptyConflictColumnsDoesNotWarn()
->create();
$warning = null;
- set_error_handler(function (int $errno, string $errstr) use (&$warning) {
+ set_error_handler(function (int $errno, string $errstr) use (&$warning): true {
$warning = $errstr;
return true;
@@ -3143,7 +3541,7 @@ public function testInsertOrUpdateWithEmptyConflictColumnsDoesNotWarn()
$this->assertEquals('1.0000', $rows[1]['rate']);
}
- public function testCreateTableWithRangeColumnsPartitioning()
+ public function testCreateTableWithRangeColumnsPartitioning(): void
{
// MySQL requires RANGE COLUMNS for DATE columns
$table = new Table('partitioned_orders', ['id' => false, 'primary_key' => ['id', 'order_date']], $this->adapter);
@@ -3161,7 +3559,7 @@ public function testCreateTableWithRangeColumnsPartitioning()
$this->assertTrue($this->adapter->hasColumn('partitioned_orders', 'order_date'));
}
- public function testCreateTableWithListColumnsPartitioning()
+ public function testCreateTableWithListColumnsPartitioning(): void
{
// MySQL requires LIST COLUMNS for STRING columns
$table = new Table('partitioned_customers', ['id' => false, 'primary_key' => ['id', 'region']], $this->adapter);
@@ -3176,7 +3574,7 @@ public function testCreateTableWithListColumnsPartitioning()
$this->assertTrue($this->adapter->hasTable('partitioned_customers'));
}
- public function testCreateTableWithHashPartitioning()
+ public function testCreateTableWithHashPartitioning(): void
{
// MySQL requires partition column in primary key
$table = new Table('partitioned_sessions', ['id' => false, 'primary_key' => ['id', 'user_id']], $this->adapter);
@@ -3189,7 +3587,7 @@ public function testCreateTableWithHashPartitioning()
$this->assertTrue($this->adapter->hasTable('partitioned_sessions'));
}
- public function testCreateTableWithKeyPartitioning()
+ public function testCreateTableWithKeyPartitioning(): void
{
$table = new Table('partitioned_cache', ['id' => false, 'primary_key' => ['cache_key']], $this->adapter);
$table->addColumn('cache_key', 'string', ['limit' => 255])
@@ -3200,7 +3598,7 @@ public function testCreateTableWithKeyPartitioning()
$this->assertTrue($this->adapter->hasTable('partitioned_cache'));
}
- public function testCreateTableWithRangePartitioningByInteger()
+ public function testCreateTableWithRangePartitioningByInteger(): void
{
$table = new Table('partitioned_logs', ['id' => false, 'primary_key' => ['id']], $this->adapter);
$table->addColumn('id', 'biginteger')
@@ -3214,7 +3612,7 @@ public function testCreateTableWithRangePartitioningByInteger()
$this->assertTrue($this->adapter->hasTable('partitioned_logs'));
}
- public function testCreateTableWithExpressionPartitioning()
+ public function testCreateTableWithExpressionPartitioning(): void
{
$table = new Table('partitioned_events', ['id' => false, 'primary_key' => ['id', 'created_at']], $this->adapter);
$table->addColumn('id', 'integer')
@@ -3228,7 +3626,7 @@ public function testCreateTableWithExpressionPartitioning()
$this->assertTrue($this->adapter->hasTable('partitioned_events'));
}
- public function testAddSinglePartitionToExistingTable()
+ public function testAddSinglePartitionToExistingTable(): void
{
// Create a partitioned table with room to add more partitions
$table = new Table('partitioned_orders', ['id' => false, 'primary_key' => ['id', 'order_date']], $this->adapter);
@@ -3256,7 +3654,7 @@ public function testAddSinglePartitionToExistingTable()
$this->assertCount(1, $rows);
}
- public function testAddMultiplePartitionsToExistingTable()
+ public function testAddMultiplePartitionsToExistingTable(): void
{
// Create a partitioned table with room to add more partitions
$table = new Table('partitioned_sales', ['id' => false, 'primary_key' => ['id', 'sale_date']], $this->adapter);
@@ -3293,7 +3691,7 @@ public function testAddMultiplePartitionsToExistingTable()
$this->assertCount(3, $rows);
}
- public function testDropSinglePartitionFromExistingTable()
+ public function testDropSinglePartitionFromExistingTable(): void
{
// Create a partitioned table with multiple partitions
$table = new Table('partitioned_logs', ['id' => false, 'primary_key' => ['id']], $this->adapter);
@@ -3330,7 +3728,7 @@ public function testDropSinglePartitionFromExistingTable()
$this->assertCount(1, $rows);
}
- public function testDropMultiplePartitionsFromExistingTable()
+ public function testDropMultiplePartitionsFromExistingTable(): void
{
// Create a partitioned table with multiple partitions
$table = new Table('partitioned_archive', ['id' => false, 'primary_key' => ['id']], $this->adapter);
@@ -3373,7 +3771,7 @@ public function testDropMultiplePartitionsFromExistingTable()
$this->assertCount(1, $rows);
}
- public function testAddMultipleListPartitionsToExistingTable()
+ public function testAddMultipleListPartitionsToExistingTable(): void
{
// Create a LIST partitioned table
$table = new Table('partitioned_regions', ['id' => false, 'primary_key' => ['id', 'region_id']], $this->adapter);
@@ -3405,7 +3803,7 @@ public function testAddMultipleListPartitionsToExistingTable()
$this->assertCount(2, $rows);
}
- public function testAddPartitionsWithMaxvalue()
+ public function testAddPartitionsWithMaxvalue(): void
{
// Create a partitioned table without MAXVALUE partition
$table = new Table('partitioned_data', ['id' => false, 'primary_key' => ['id']], $this->adapter);
@@ -3541,4 +3939,53 @@ public function testCombinedPartitionAndColumnOperations(): void
$this->assertCount(1, $rows);
$this->assertEquals('A description', $rows[0]['description']);
}
+
+ public function testBinaryColumnWithFixedOption(): void
+ {
+ $table = new Table('binary_fixed_test', [], $this->adapter);
+ $table->addColumn('hash', 'binary', ['limit' => 20, 'fixed' => true])
+ ->addColumn('data', 'binary', ['limit' => 20])
+ ->save();
+
+ $this->assertTrue($this->adapter->hasColumn('binary_fixed_test', 'hash'));
+ $this->assertTrue($this->adapter->hasColumn('binary_fixed_test', 'data'));
+
+ // Check that the fixed column is created as BINARY and the non-fixed as VARBINARY
+ $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM binary_fixed_test');
+ $hashColumn = null;
+ $dataColumn = null;
+ foreach ($rows as $row) {
+ if ($row['Field'] === 'hash') {
+ $hashColumn = $row;
+ }
+ if ($row['Field'] === 'data') {
+ $dataColumn = $row;
+ }
+ }
+
+ $this->assertNotNull($hashColumn);
+ $this->assertNotNull($dataColumn);
+ $this->assertSame('binary(20)', $hashColumn['Type']);
+ $this->assertSame('varbinary(20)', $dataColumn['Type']);
+
+ // Verify the fixed attribute is reflected back
+ $columns = $this->adapter->getColumns('binary_fixed_test');
+ $hashCol = null;
+ $dataCol = null;
+ foreach ($columns as $col) {
+ if ($col->getName() === 'hash') {
+ $hashCol = $col;
+ }
+ if ($col->getName() === 'data') {
+ $dataCol = $col;
+ }
+ }
+
+ $this->assertNotNull($hashCol);
+ $this->assertNotNull($dataCol);
+ $this->assertSame('binary', $hashCol->getType());
+ $this->assertSame('binary', $dataCol->getType());
+ $this->assertTrue($hashCol->getFixed());
+ $this->assertNull($dataCol->getFixed());
+ }
}
diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php
index 58c03871e..831386949 100644
--- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php
+++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php
@@ -8,6 +8,7 @@
use Cake\Console\TestSuite\StubConsoleOutput;
use Cake\Database\Connection;
use Cake\Datasource\ConnectionManager;
+use Exception;
use InvalidArgumentException;
use Migrations\Db\Adapter\AdapterInterface;
use Migrations\Db\Adapter\PostgresAdapter;
@@ -27,13 +28,12 @@
class PostgresAdapterTest extends TestCase
{
- /**
- * @var \Migrations\Db\Adapter\PostgresAdapter
- */
- private $adapter;
+ private PostgresAdapter $adapter;
private array $config;
+
private StubConsoleOutput $out;
+
private ConsoleIo $io;
/**
@@ -41,7 +41,7 @@ class PostgresAdapterTest extends TestCase
*
* @return bool
*/
- private static function isPostgresAvailable()
+ private function isPostgresAvailable()
{
static $available;
@@ -67,7 +67,7 @@ protected function setUp(): void
'database' => $config['database'],
];
- if (!self::isPostgresAvailable()) {
+ if (!$this->isPostgresAvailable()) {
$this->markTestSkipped('Postgres is not available. Please install php-pdo-pgsql or equivalent package.');
}
@@ -110,17 +110,17 @@ private function usingPostgres10(): bool
return version_compare($version, '10.0.0', '>=');
}
- public function testAdapterType()
+ public function testAdapterType(): void
{
$this->assertEquals('pgsql', $this->adapter->getAdapterType());
}
- public function testConnection()
+ public function testConnection(): void
{
$this->assertInstanceOf(Connection::class, $this->adapter->getConnection());
}
- public function testCreatingTheSchemaTableOnConnect()
+ public function testCreatingTheSchemaTableOnConnect(): void
{
$this->adapter->connect();
$this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName()));
@@ -131,21 +131,21 @@ public function testCreatingTheSchemaTableOnConnect()
$this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName()));
}
- public function testSchemaTableIsCreatedWithPrimaryKey()
+ public function testSchemaTableIsCreatedWithPrimaryKey(): void
{
$this->adapter->connect();
new Table($this->adapter->getSchemaTableName(), [], $this->adapter);
$this->assertTrue($this->adapter->hasIndex($this->adapter->getSchemaTableName(), ['version']));
}
- public function testQuoteSchemaName()
+ public function testQuoteSchemaName(): void
{
$this->assertEquals('"schema"', $this->adapter->quoteSchemaName('schema'));
// No . is supported in schema name.
$this->assertEquals('"schema"."schema"', $this->adapter->quoteSchemaName('schema.schema'));
}
- public function testGetGlobalSchemaName()
+ public function testGetGlobalSchemaName(): void
{
$config = ConnectionManager::getConfig('test');
$config['schema'] = 'test_schema';
@@ -169,20 +169,20 @@ public function testGetGlobalSchemaName()
ConnectionManager::drop('test-schema');
}
- public function testQuoteTableName()
+ public function testQuoteTableName(): void
{
$this->assertEquals('"public"."table"', $this->adapter->quoteTableName('table'));
$this->assertEquals('"table"."table"', $this->adapter->quoteTableName('table.table'));
}
- public function testQuoteColumnName()
+ public function testQuoteColumnName(): void
{
$this->assertEquals('"string"', $this->adapter->quoteColumnName('string'));
// No . is supported in column name.
$this->assertEquals('"string"."string"', $this->adapter->quoteColumnName('string.string'));
}
- public function testCreateTable()
+ public function testCreateTable(): void
{
$table = new Table('ntable', [], $this->adapter);
$table->addColumn('realname', 'string')
@@ -195,7 +195,7 @@ public function testCreateTable()
$this->assertFalse($this->adapter->hasColumn('ntable', 'address'));
}
- public function testCreateTableWithSchema()
+ public function testCreateTableWithSchema(): void
{
$this->adapter->createSchema('nschema');
@@ -212,7 +212,7 @@ public function testCreateTableWithSchema()
$this->adapter->dropSchema('nschema');
}
- public function testCreateTableCustomIdColumn()
+ public function testCreateTableCustomIdColumn(): void
{
$table = new Table('ntable', ['id' => 'custom_id'], $this->adapter);
$table->addColumn('realname', 'string')
@@ -225,7 +225,7 @@ public function testCreateTableCustomIdColumn()
$this->assertFalse($this->adapter->hasColumn('ntable', 'address'));
}
- public function testCreateTableWithNoPrimaryKey()
+ public function testCreateTableWithNoPrimaryKey(): void
{
$options = [
'id' => false,
@@ -236,7 +236,7 @@ public function testCreateTableWithNoPrimaryKey()
$this->assertFalse($this->adapter->hasColumn('atable', 'id'));
}
- public function testCreateTableWithConflictingPrimaryKeys()
+ public function testCreateTableWithConflictingPrimaryKeys(): void
{
$options = [
'primary_key' => 'user_id',
@@ -248,7 +248,7 @@ public function testCreateTableWithConflictingPrimaryKeys()
$table->addColumn('user_id', 'integer')->save();
}
- public function testCreateTableWithPrimaryKeySetToImplicitId()
+ public function testCreateTableWithPrimaryKeySetToImplicitId(): void
{
$options = [
'primary_key' => 'id',
@@ -260,7 +260,7 @@ public function testCreateTableWithPrimaryKeySetToImplicitId()
$this->assertTrue($this->adapter->hasColumn('ztable', 'user_id'));
}
- public function testCreateTableWithPrimaryKeyArraySetToImplicitId()
+ public function testCreateTableWithPrimaryKeyArraySetToImplicitId(): void
{
$options = [
'primary_key' => ['id'],
@@ -272,7 +272,7 @@ public function testCreateTableWithPrimaryKeyArraySetToImplicitId()
$this->assertTrue($this->adapter->hasColumn('ztable', 'user_id'));
}
- public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId()
+ public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId(): void
{
$options = [
'primary_key' => ['id', 'user_id'],
@@ -283,7 +283,7 @@ public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId()
$table->addColumn('user_id', 'integer')->save();
}
- public function testCreateTableWithMultiplePrimaryKeys()
+ public function testCreateTableWithMultiplePrimaryKeys(): void
{
$options = [
'id' => false,
@@ -298,7 +298,7 @@ public function testCreateTableWithMultiplePrimaryKeys()
$this->assertFalse($this->adapter->hasIndex('table1', ['tag_id', 'user_email']));
}
- public function testCreateTableWithMultiplePrimaryKeysWithSchema()
+ public function testCreateTableWithMultiplePrimaryKeysWithSchema(): void
{
$this->adapter->createSchema('schema1');
@@ -320,7 +320,7 @@ public function testCreateTableWithMultiplePrimaryKeysWithSchema()
/**
* @return void
*/
- public function testCreateTableWithPrimaryKeyAsUuid()
+ public function testCreateTableWithPrimaryKeyAsUuid(): void
{
$options = [
'id' => false,
@@ -337,7 +337,7 @@ public function testCreateTableWithPrimaryKeyAsUuid()
/**
* @return void
*/
- public function testCreateTableWithPrimaryKeyAsBinaryUuid()
+ public function testCreateTableWithPrimaryKeyAsBinaryUuid(): void
{
$options = [
'id' => false,
@@ -354,7 +354,7 @@ public function testCreateTableWithPrimaryKeyAsBinaryUuid()
/**
* @return void
*/
- public function testCreateTableWithPrimaryKeyAsNativeUuid()
+ public function testCreateTableWithPrimaryKeyAsNativeUuid(): void
{
$options = [
'id' => false,
@@ -368,7 +368,7 @@ public function testCreateTableWithPrimaryKeyAsNativeUuid()
$this->assertTrue($this->adapter->hasColumn('ztable', 'user_id'));
}
- public function testCreateTableWithMultipleIndexes()
+ public function testCreateTableWithMultipleIndexes(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -382,7 +382,7 @@ public function testCreateTableWithMultipleIndexes()
$this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_name']));
}
- public function testCreateTableWithUniqueIndexes()
+ public function testCreateTableWithUniqueIndexes(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -392,7 +392,7 @@ public function testCreateTableWithUniqueIndexes()
$this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_email']));
}
- public function testCreateTableWithNamedIndexes()
+ public function testCreateTableWithNamedIndexes(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -465,7 +465,7 @@ public function testCreateTableIndexWithWhere(): void
$this->assertStringContainsString('("email") WHERE is_verified = true', $indexQuery);
}
- public function testAddPrimaryKey()
+ public function testAddPrimaryKey(): void
{
$table = new Table('table1', ['id' => false], $this->adapter);
$table
@@ -479,7 +479,7 @@ public function testAddPrimaryKey()
$this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column1']));
}
- public function testChangePrimaryKey()
+ public function testChangePrimaryKey(): void
{
$table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter);
$table
@@ -496,7 +496,7 @@ public function testChangePrimaryKey()
$this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column2', 'column3']));
}
- public function testDropPrimaryKey()
+ public function testDropPrimaryKey(): void
{
$table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter);
$table
@@ -510,7 +510,7 @@ public function testDropPrimaryKey()
$this->assertFalse($this->adapter->hasPrimaryKey('table1', ['column1']));
}
- public function testAddComment()
+ public function testAddComment(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -531,7 +531,7 @@ public function testAddComment()
$this->assertEquals('comment1', $rows[0]['description']);
}
- public function testChangeComment()
+ public function testChangeComment(): void
{
$table = new Table('table1', ['comment' => 'comment1'], $this->adapter);
$table->save();
@@ -552,7 +552,7 @@ public function testChangeComment()
$this->assertEquals('comment2', $rows[0]['description']);
}
- public function testDropComment()
+ public function testDropComment(): void
{
$table = new Table('table1', ['comment' => 'comment1'], $this->adapter);
$table->save();
@@ -573,7 +573,7 @@ public function testDropComment()
$this->assertEmpty($rows);
}
- public function testRenameTable()
+ public function testRenameTable(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -585,7 +585,7 @@ public function testRenameTable()
$this->assertTrue($this->adapter->hasTable('table2'));
}
- public function testRenameTableWithSchema()
+ public function testRenameTableWithSchema(): void
{
$this->adapter->createSchema('schema1');
@@ -600,7 +600,7 @@ public function testRenameTableWithSchema()
$this->adapter->dropSchema('schema1');
}
- public function testAddColumn()
+ public function testAddColumn(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -610,7 +610,7 @@ public function testAddColumn()
$this->assertTrue($table->hasColumn('email'));
}
- public function testAddColumnWithDefaultValue()
+ public function testAddColumnWithDefaultValue(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -624,7 +624,7 @@ public function testAddColumnWithDefaultValue()
}
}
- public function testAddColumnWithDefaultZero()
+ public function testAddColumnWithDefaultZero(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -639,13 +639,14 @@ public function testAddColumnWithDefaultZero()
}
}
- public function testAddColumnWithAutoIdentity()
+ public function testAddColumnWithAutoIdentity(): void
{
if (!$this->usingPostgres10()) {
$this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0');
}
$table = new Table('table1', [], $this->adapter);
$table->save();
+
$columns = $this->adapter->getColumns('table1');
foreach ($columns as $column) {
if ($column->getName() === 'id') {
@@ -658,7 +659,7 @@ public function testAddColumnWithAutoIdentity()
/**
* Test that shims from PHINX_TYPE_JSONB to 'json' type work.
*/
- public function testAddColumnJsonbCompat()
+ public function testAddColumnJsonbCompat(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -668,6 +669,34 @@ public function testAddColumnJsonbCompat()
$this->assertTrue($table->hasColumn('config'));
}
+ /**
+ * Test that adding a column with the citext type works.
+ *
+ * Requires the `citext` extension to be enabled on the database; the setUp
+ * above creates the extension when missing.
+ */
+ public function testAddColumnCitext(): void
+ {
+ $table = new Table('table1', [], $this->adapter);
+ $table->save();
+ $this->assertFalse($table->hasColumn('nickname'));
+ $table->addColumn('nickname', 'citext', ['null' => true])
+ ->save();
+ $this->assertTrue($table->hasColumn('nickname'));
+
+ $columns = $this->adapter->getColumns('table1');
+ $nickname = null;
+ foreach ($columns as $column) {
+ if ($column->getName() === 'nickname') {
+ $nickname = $column;
+ break;
+ }
+ }
+ $this->assertNotNull($nickname);
+ $this->assertSame('citext', $nickname->getType());
+ $this->assertTrue($nickname->isNull());
+ }
+
public static function providerAddColumnIdentity(): array
{
return [
@@ -678,15 +707,16 @@ public static function providerAddColumnIdentity(): array
}
#[DataProvider('providerAddColumnIdentity')]
- public function testAddColumnIdentity($generated, $addToColumn)
+ public function testAddColumnIdentity(string $generated, bool $addToColumn): void
{
if (!$this->usingPostgres10()) {
$this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0');
}
$table = new Table('table1', ['id' => false], $this->adapter);
$table->save();
+
$options = ['identity' => true];
- if ($addToColumn !== false) {
+ if ($addToColumn) {
$options['generated'] = $generated;
}
$table->addColumn('id', 'integer', $options)
@@ -700,7 +730,7 @@ public function testAddColumnIdentity($generated, $addToColumn)
}
}
- public function testAddColumnWithDefaultBoolean()
+ public function testAddColumnWithDefaultBoolean(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -724,7 +754,7 @@ public function testAddColumnWithDefaultBoolean()
}
}
- public function testAddColumnWithBooleanIgnoreLimitCastDefault()
+ public function testAddColumnWithBooleanIgnoreLimitCastDefault(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -758,7 +788,7 @@ public function testAddColumnWithBooleanIgnoreLimitCastDefault()
$this->assertNull($column->getLimit());
}
- public function testAddColumnWithComment()
+ public function testAddColumnWithComment(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -788,7 +818,7 @@ public function testAddColumnWithComment()
);
}
- public function testAddStringWithLimit()
+ public function testAddStringWithLimit(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -807,7 +837,7 @@ public function testAddStringWithLimit()
}
}
- public function testAddDecimalWithPrecisionAndScale()
+ public function testAddDecimalWithPrecisionAndScale(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -829,7 +859,7 @@ public function testAddDecimalWithPrecisionAndScale()
}
}
- public function testAddTimestampWithPrecision()
+ public function testAddTimestampWithPrecision(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -853,7 +883,7 @@ public function testAddTimestampWithPrecision()
}
}
- public function testRenameColumn()
+ public function testRenameColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -865,7 +895,7 @@ public function testRenameColumn()
$this->assertTrue($this->adapter->hasColumn('t', 'column2'));
}
- public function testRenameColumnIsCaseSensitive()
+ public function testRenameColumnIsCaseSensitive(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('columnOne', 'string')
@@ -877,7 +907,7 @@ public function testRenameColumnIsCaseSensitive()
$this->assertTrue($this->adapter->hasColumn('t', 'columnTwo'));
}
- public function testRenamingANonExistentColumn()
+ public function testRenamingANonExistentColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -890,13 +920,13 @@ public function testRenamingANonExistentColumn()
$this->assertInstanceOf(
'InvalidArgumentException',
$e,
- 'Expected exception of type InvalidArgumentException, got ' . get_class($e),
+ 'Expected exception of type InvalidArgumentException, got ' . $e::class,
);
$this->assertEquals('The specified column does not exist: column2', $e->getMessage());
}
}
- public function testChangeColumn()
+ public function testChangeColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -922,7 +952,7 @@ public static function providerChangeColumnIdentity(): array
}
#[DataProvider('providerChangeColumnIdentity')]
- public function testChangeColumnIdentity($generated)
+ public function testChangeColumnIdentity(string $generated): void
{
if (!$this->usingPostgres10()) {
$this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0');
@@ -933,6 +963,7 @@ public function testChangeColumnIdentity($generated)
$table->changeColumn('column1', 'integer', ['identity' => true, 'generated' => PostgresAdapter::GENERATED_ALWAYS]);
$table->save();
+
$columns = $this->adapter->getColumns('table1');
foreach ($columns as $column) {
if ($column->getName() === 'column1') {
@@ -942,7 +973,7 @@ public function testChangeColumnIdentity($generated)
}
}
- public function testChangeColumnDropIdentity()
+ public function testChangeColumnDropIdentity(): void
{
if (!$this->usingPostgres10()) {
$this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0');
@@ -951,6 +982,7 @@ public function testChangeColumnDropIdentity()
$table->save();
$table->changeColumn('id', 'integer', ['identity' => false]);
$table->save();
+
$columns = $this->adapter->getColumns('table1');
foreach ($columns as $column) {
if ($column->getName() === 'id') {
@@ -959,7 +991,7 @@ public function testChangeColumnDropIdentity()
}
}
- public function testChangeColumnChangeIdentity()
+ public function testChangeColumnChangeIdentity(): void
{
if (!$this->usingPostgres10()) {
$this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0');
@@ -968,6 +1000,7 @@ public function testChangeColumnChangeIdentity()
$table->save();
$table->changeColumn('id', 'integer', ['identity' => true, 'generated' => PostgresAdapter::GENERATED_BY_DEFAULT]);
$table->save();
+
$columns = $this->adapter->getColumns('table1');
foreach ($columns as $column) {
if ($column->getName() === 'id') {
@@ -977,7 +1010,7 @@ public function testChangeColumnChangeIdentity()
}
}
- public static function integersProvider()
+ public static function integersProvider(): array
{
return [
['smallinteger', 32767],
@@ -987,7 +1020,7 @@ public static function integersProvider()
}
#[DataProvider('integersProvider')]
- public function testChangeColumnFromTextToInteger($type, $value)
+ public function testChangeColumnFromTextToInteger(string $type, int $value): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'text')
@@ -1002,7 +1035,7 @@ public function testChangeColumnFromTextToInteger($type, $value)
$this->assertSame($value, $row['column1']);
}
- public function testChangeBooleanOptions()
+ public function testChangeBooleanOptions(): void
{
$table = new Table('t', ['id' => false], $this->adapter);
$table->addColumn('my_bool', 'boolean', ['default' => true, 'null' => true])
@@ -1020,12 +1053,12 @@ public function testChangeBooleanOptions()
$rows = $this->adapter->fetchAll('SELECT * FROM t');
$this->assertCount(3, $rows);
- $this->assertSame([true, false, null], array_map(function ($row) {
+ $this->assertSame([true, false, null], array_map(function (array $row) {
return $row['my_bool'];
}, $rows));
}
- public function testChangeColumnFromIntegerToBoolean()
+ public function testChangeColumnFromIntegerToBoolean(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'integer', ['default' => 0])
@@ -1041,7 +1074,7 @@ public function testChangeColumnFromIntegerToBoolean()
}
}
- public function testChangeColumnCharToUuid()
+ public function testChangeColumnCharToUuid(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'char', ['default' => null, 'limit' => 36])
@@ -1059,7 +1092,7 @@ public function testChangeColumnCharToUuid()
}
}
- public function testChangeColumnCharToNativeUuid()
+ public function testChangeColumnCharToNativeUuid(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'char', ['default' => null, 'limit' => 36])
@@ -1077,7 +1110,7 @@ public function testChangeColumnCharToNativeUuid()
}
}
- public function testChangeColumnWithDefault()
+ public function testChangeColumnWithDefault(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -1100,7 +1133,7 @@ public function testChangeColumnWithDefault()
}
}
- public function testChangeColumnWithDropDefault()
+ public function testChangeColumnWithDropDefault(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'Test'])
@@ -1127,7 +1160,7 @@ public function testChangeColumnWithDropDefault()
}
}
- public function testDropColumn()
+ public function testDropColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -1138,7 +1171,7 @@ public function testDropColumn()
$this->assertFalse($this->adapter->hasColumn('t', 'column1'));
}
- public static function columnsProvider()
+ public static function columnsProvider(): array
{
return [
['column1', 'string', []],
@@ -1161,7 +1194,7 @@ public static function columnsProvider()
}
#[DataProvider('columnsProvider')]
- public function testGetColumns($colName, $type, $options, $actualType = null)
+ public function testGetColumns(string $colName, string $type, array $options, ?string $actualType = null): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn($colName, $type, $options)->save();
@@ -1182,7 +1215,7 @@ public function testGetColumns($colName, $type, $options, $actualType = null)
}
#[DataProvider('columnsProvider')]
- public function testGetColumnsWithSchema($colName, $type, $options, $actualType = null)
+ public function testGetColumnsWithSchema(string $colName, string $type, array $options, ?string $actualType = null): void
{
$this->adapter->createSchema('tschema');
@@ -1206,7 +1239,7 @@ public function testGetColumnsWithSchema($colName, $type, $options, $actualType
$this->adapter->dropSchema('tschema');
}
- public function testAddIndex()
+ public function testAddIndex(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -1217,7 +1250,7 @@ public function testAddIndex()
$this->assertTrue($table->hasIndex('email'));
}
- public function testAddIndexWithSort()
+ public function testAddIndexWithSort(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -1259,7 +1292,7 @@ public function testAddIndexWithSort()
$this->assertEquals($emailOrder['sort_order'], 'ASC');
}
- public function testAddIndexWithIncludeColumns()
+ public function testAddIndexWithIncludeColumns(): void
{
if (!version_compare($this->adapter->fetchAll('SHOW server_version;')[0]['server_version'], '11.0.0', '>=')) {
$this->markTestSkipped('Cannot test index include collumns (non-key columns) on postgresql versions less than 11');
@@ -1312,7 +1345,7 @@ public function testAddIndexWithIncludeColumns()
$this->assertEquals($indexColumn['index_column'], 'INCLUDED');
}
- public function testAddIndexWithSchema()
+ public function testAddIndexWithSchema(): void
{
$this->adapter->createSchema('schema1');
@@ -1327,7 +1360,7 @@ public function testAddIndexWithSchema()
$this->adapter->dropSchema('schema1');
}
- public function testAddIndexWithNameWithSchema()
+ public function testAddIndexWithNameWithSchema(): void
{
$this->adapter->createSchema('schema1');
@@ -1342,7 +1375,7 @@ public function testAddIndexWithNameWithSchema()
$this->adapter->dropSchema('schema1');
}
- public function testAddIndexIsCaseSensitive()
+ public function testAddIndexIsCaseSensitive(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('theEmail', 'string')
@@ -1353,7 +1386,7 @@ public function testAddIndexIsCaseSensitive()
$this->assertTrue($table->hasIndex('theEmail'));
}
- public function testDropIndex()
+ public function testDropIndex(): void
{
// single column index
$table = new Table('table1', [], $this->adapter);
@@ -1394,7 +1427,7 @@ public function testDropIndex()
$this->assertFalse($table4->hasIndex(['fname', 'lname']));
}
- public function testDropIndexWithSchema()
+ public function testDropIndexWithSchema(): void
{
$this->adapter->createSchema('schema1');
@@ -1439,7 +1472,7 @@ public function testDropIndexWithSchema()
$this->adapter->dropSchema('schema1');
}
- public function testDropIndexByName()
+ public function testDropIndexByName(): void
{
// single column index
$table = new Table('table1', [], $this->adapter);
@@ -1464,7 +1497,7 @@ public function testDropIndexByName()
$this->assertFalse($table2->hasIndex(['fname', 'lname']));
}
- public function testDropIndexByNameWithSchema()
+ public function testDropIndexByNameWithSchema(): void
{
$this->adapter->createSchema('schema1');
@@ -1493,7 +1526,163 @@ public function testDropIndexByNameWithSchema()
$this->adapter->dropSchema('schema1');
}
- public function testAddForeignKey()
+ public function testAddGistIndex(): void
+ {
+ // GiST indexes require specific data types with GiST support.
+ // We use int4range which has built-in GiST support in PostgreSQL.
+ $this->adapter->execute('CREATE TABLE table1 (id SERIAL PRIMARY KEY, int_range int4range)');
+
+ $table = new Table('table1', [], $this->adapter);
+ $table->addIndex('int_range', ['type' => 'gist'])
+ ->save();
+
+ $this->assertTrue($table->hasIndex('int_range'));
+
+ // Verify the index uses the GIST access method
+ $rows = $this->adapter->fetchAll(
+ "SELECT am.amname as access_method
+ FROM pg_index i
+ JOIN pg_class c ON c.oid = i.indexrelid
+ JOIN pg_am am ON am.oid = c.relam
+ JOIN pg_class t ON t.oid = i.indrelid
+ WHERE t.relname = 'table1' AND c.relname = 'table1_int_range'",
+ );
+ $this->assertCount(1, $rows);
+ $this->assertEquals('gist', $rows[0]['access_method']);
+ }
+
+ public function testAddGinIndex(): void
+ {
+ $table = new Table('table1', [], $this->adapter);
+ $table->addColumn('tags', 'jsonb')
+ ->save();
+
+ $table->addIndex('tags', ['type' => 'gin'])
+ ->save();
+
+ $this->assertTrue($table->hasIndex('tags'));
+
+ // Verify the index uses the GIN access method
+ $rows = $this->adapter->fetchAll(
+ "SELECT am.amname as access_method
+ FROM pg_index i
+ JOIN pg_class c ON c.oid = i.indexrelid
+ JOIN pg_am am ON am.oid = c.relam
+ JOIN pg_class t ON t.oid = i.indrelid
+ WHERE t.relname = 'table1' AND c.relname = 'table1_tags'",
+ );
+ $this->assertCount(1, $rows);
+ $this->assertEquals('gin', $rows[0]['access_method']);
+ }
+
+ public function testAddBrinIndex(): void
+ {
+ $table = new Table('table1', [], $this->adapter);
+ $table->addColumn('created_at', 'timestamp')
+ ->save();
+
+ $table->addIndex('created_at', ['type' => 'brin'])
+ ->save();
+
+ $this->assertTrue($table->hasIndex('created_at'));
+
+ // Verify the index uses the BRIN access method
+ $rows = $this->adapter->fetchAll(
+ "SELECT am.amname as access_method
+ FROM pg_index i
+ JOIN pg_class c ON c.oid = i.indexrelid
+ JOIN pg_am am ON am.oid = c.relam
+ JOIN pg_class t ON t.oid = i.indrelid
+ WHERE t.relname = 'table1' AND c.relname = 'table1_created_at'",
+ );
+ $this->assertCount(1, $rows);
+ $this->assertEquals('brin', $rows[0]['access_method']);
+ }
+
+ public function testAddHashIndex(): void
+ {
+ $table = new Table('table1', [], $this->adapter);
+ $table->addColumn('session_id', 'string', ['limit' => 64])
+ ->save();
+
+ $table->addIndex('session_id', ['type' => 'hash'])
+ ->save();
+
+ $this->assertTrue($table->hasIndex('session_id'));
+
+ // Verify the index uses the HASH access method
+ $rows = $this->adapter->fetchAll(
+ "SELECT am.amname as access_method
+ FROM pg_index i
+ JOIN pg_class c ON c.oid = i.indexrelid
+ JOIN pg_am am ON am.oid = c.relam
+ JOIN pg_class t ON t.oid = i.indrelid
+ WHERE t.relname = 'table1' AND c.relname = 'table1_session_id'",
+ );
+ $this->assertCount(1, $rows);
+ $this->assertEquals('hash', $rows[0]['access_method']);
+ }
+
+ public function testAddSpgistIndex(): void
+ {
+ // SP-GiST indexes on text require the text_ops operator class
+ $table = new Table('table1', [], $this->adapter);
+ $table->addColumn('data', 'text')
+ ->save();
+
+ $table->addIndex('data', ['type' => 'spgist', 'opclass' => ['data' => 'text_ops']])
+ ->save();
+
+ $this->assertTrue($table->hasIndex('data'));
+
+ // Verify the index uses the SP-GIST access method
+ $rows = $this->adapter->fetchAll(
+ "SELECT am.amname as access_method
+ FROM pg_index i
+ JOIN pg_class c ON c.oid = i.indexrelid
+ JOIN pg_am am ON am.oid = c.relam
+ JOIN pg_class t ON t.oid = i.indrelid
+ WHERE t.relname = 'table1' AND c.relname = 'table1_data'",
+ );
+ $this->assertCount(1, $rows);
+ $this->assertEquals('spgist', $rows[0]['access_method']);
+ }
+
+ public function testAddIndexWithOpclass(): void
+ {
+ // Test opclass with GiST using pg_trgm extension
+ // Skip if extension is not available
+ try {
+ $this->adapter->execute('CREATE EXTENSION IF NOT EXISTS pg_trgm');
+ } catch (Exception) {
+ $this->markTestSkipped('pg_trgm extension is not available');
+ }
+
+ $table = new Table('table1', [], $this->adapter);
+ $table->addColumn('name', 'string')
+ ->save();
+
+ $table->addIndex('name', [
+ 'type' => 'gist',
+ 'opclass' => ['name' => 'gist_trgm_ops'],
+ ])->save();
+
+ $this->assertTrue($table->hasIndex('name'));
+
+ // Verify the index was created with the correct access method
+ $rows = $this->adapter->fetchAll(
+ "SELECT am.amname as access_method
+ FROM pg_index i
+ JOIN pg_class c ON c.oid = i.indexrelid
+ JOIN pg_am am ON am.oid = c.relam
+ JOIN pg_class t ON t.oid = i.indrelid
+ WHERE t.relname = 'table1' AND c.relname = 'table1_name'",
+ );
+ $this->assertCount(1, $rows);
+ $this->assertEquals('gist', $rows[0]['access_method']);
+ }
+
+ public function testAddForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -1507,7 +1696,7 @@ public function testAddForeignKey()
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testAddForeignKeyWithSchema()
+ public function testAddForeignKeyWithSchema(): void
{
$this->adapter->createSchema('schema1');
$this->adapter->createSchema('schema2');
@@ -1527,7 +1716,7 @@ public function testAddForeignKeyWithSchema()
$this->adapter->dropSchema('schema2');
}
- public function testAddForeignKeyDeferrable()
+ public function testAddForeignKeyDeferrable(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -1548,7 +1737,7 @@ public function testAddForeignKeyDeferrable()
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testDropForeignKey()
+ public function testDropForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -1563,7 +1752,7 @@ public function testDropForeignKey()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testDropForeignKeyWithMultipleColumns()
+ public function testDropForeignKeyWithMultipleColumns(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable
@@ -1614,7 +1803,7 @@ public function testDropForeignKeyWithMultipleColumns()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_field1', 'ref_table_id']));
}
- public function testDropForeignKeyWithIdenticalMultipleColumns()
+ public function testDropForeignKeyWithIdenticalMultipleColumns(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable
@@ -1662,11 +1851,8 @@ public static function nonExistentForeignKeyColumnsProvider(): array
];
}
- /**
- * @param array $columns
- */
#[DataProvider('nonExistentForeignKeyColumnsProvider')]
- public function testDropForeignKeyByNonExistentKeyColumns(array $columns)
+ public function testDropForeignKeyByNonExistentKeyColumns(array $columns): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable
@@ -1694,7 +1880,7 @@ public function testDropForeignKeyByNonExistentKeyColumns(array $columns)
$this->adapter->dropForeignKey($table->getName(), $columns);
}
- public function testDropForeignKeyCaseSensitivity()
+ public function testDropForeignKeyCaseSensitivity(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->save();
@@ -1716,7 +1902,7 @@ public function testDropForeignKeyCaseSensitivity()
$this->adapter->dropForeignKey($table->getName(), ['ref_table_id']);
}
- public function testDropForeignKeyByName()
+ public function testDropForeignKeyByName(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->save();
@@ -1737,7 +1923,7 @@ public function testDropForeignKeyByName()
}
#[DataProvider('provideForeignKeysToCheck')]
- public function testHasForeignKey($tableDef, $key, $exp)
+ public function testHasForeignKey(string $tableDef, string|array $key, bool $exp): void
{
$conn = $this->adapter->getConnection();
$conn->execute('CREATE TABLE other(a int, b int, c int, unique(a), unique(b), unique(a,b), unique(a,b,c));');
@@ -1745,7 +1931,7 @@ public function testHasForeignKey($tableDef, $key, $exp)
$this->assertSame($exp, $this->adapter->hasForeignKey('t', $key));
}
- public static function provideForeignKeysToCheck()
+ public static function provideForeignKeysToCheck(): array
{
return [
['create table t(a int)', 'a', false],
@@ -1771,7 +1957,7 @@ public static function provideForeignKeysToCheck()
];
}
- public function testHasNamedForeignKey()
+ public function testHasNamedForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->save();
@@ -1794,7 +1980,7 @@ public function testHasNamedForeignKey()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), [], 'my_constraint2'));
}
- public function testDropForeignKeyWithSchema()
+ public function testDropForeignKeyWithSchema(): void
{
$this->adapter->createSchema('schema1');
$this->adapter->createSchema('schema2');
@@ -1815,7 +2001,7 @@ public function testDropForeignKeyWithSchema()
$this->adapter->dropSchema('schema2');
}
- public function testDropForeignKeyNotDroppingPrimaryKey()
+ public function testDropForeignKeyNotDroppingPrimaryKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -1833,13 +2019,13 @@ public function testDropForeignKeyNotDroppingPrimaryKey()
$this->assertTrue($this->adapter->hasIndexByName('table', 'table_pkey'));
}
- public function testHasDatabase()
+ public function testHasDatabase(): void
{
$this->assertFalse($this->adapter->hasDatabase('fake_database_name'));
$this->assertTrue($this->adapter->hasDatabase($this->config['database']));
}
- public function testDropDatabase()
+ public function testDropDatabase(): void
{
$this->assertFalse($this->adapter->hasDatabase('phinx_temp_database'));
$this->adapter->createDatabase('phinx_temp_database');
@@ -1847,13 +2033,13 @@ public function testDropDatabase()
$this->adapter->dropDatabase('phinx_temp_database');
}
- public function testCreateSchema()
+ public function testCreateSchema(): void
{
$this->adapter->createSchema('foo');
$this->assertTrue($this->adapter->hasSchema('foo'));
}
- public function testDropSchema()
+ public function testDropSchema(): void
{
$this->adapter->createSchema('foo');
$this->assertTrue($this->adapter->hasSchema('foo'));
@@ -1861,7 +2047,7 @@ public function testDropSchema()
$this->assertFalse($this->adapter->hasSchema('foo'));
}
- public function testDropAllSchemas()
+ public function testDropAllSchemas(): void
{
$this->adapter->createSchema('foo');
$this->adapter->createSchema('bar');
@@ -1873,7 +2059,7 @@ public function testDropAllSchemas()
$this->assertFalse($this->adapter->hasSchema('bar'));
}
- public function testCreateTableWithComment()
+ public function testCreateTableWithComment(): void
{
$tableComment = 'Table comment';
$table = new Table('ntable', ['comment' => $tableComment], $this->adapter);
@@ -1895,7 +2081,7 @@ public function testCreateTableWithComment()
$this->assertEquals($tableComment, $rows[0]['description'], 'Dont set table comment correctly');
}
- public function testCanAddColumnComment()
+ public function testCanAddColumnComment(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn(
@@ -1918,7 +2104,7 @@ public function testCanAddColumnComment()
$this->assertEquals($comment, $row['column_comment'], 'Dont set column comment correctly');
}
- public function testCanAddCommentForColumnWithReservedName()
+ public function testCanAddCommentForColumnWithReservedName(): void
{
$table = new Table('user', [], $this->adapter);
$table->addColumn('index', 'string', ['comment' => $comment = 'Comments from column "index"'])
@@ -1943,7 +2129,7 @@ public function testCanAddCommentForColumnWithReservedName()
}
#[Depends('testCanAddColumnComment')]
- public function testCanChangeColumnComment()
+ public function testCanChangeColumnComment(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('field1', 'string', ['comment' => 'Comments from column "field1"'])
@@ -1970,7 +2156,7 @@ public function testCanChangeColumnComment()
}
#[Depends('testCanAddColumnComment')]
- public function testCanRemoveColumnComment()
+ public function testCanRemoveColumnComment(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('field1', 'string', ['comment' => 'Comments from column "field1"'])
@@ -1994,7 +2180,7 @@ public function testCanRemoveColumnComment()
}
#[Depends('testCanAddColumnComment')]
- public function testCanAddMultipleCommentsToOneTable()
+ public function testCanAddMultipleCommentsToOneTable(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('comment1', 'string', [
@@ -2033,7 +2219,7 @@ public function testCanAddMultipleCommentsToOneTable()
}
#[Depends('testCanAddColumnComment')]
- public function testColumnsAreResetBetweenTables()
+ public function testColumnsAreResetBetweenTables(): void
{
$table = new Table('widgets', [], $this->adapter);
$table->addColumn('transport', 'string', [
@@ -2062,7 +2248,7 @@ public function testColumnsAreResetBetweenTables()
/**
* Test that column names are properly escaped when creating Foreign Keys
*/
- public function testForeignKeysAreProperlyEscaped()
+ public function testForeignKeysAreProperlyEscaped(): void
{
$userId = 'user';
$sessionId = 'session';
@@ -2082,7 +2268,7 @@ public function testForeignKeysAreProperlyEscaped()
$this->assertTrue($foreign->hasForeignKey('user'));
}
- public function testForeignKeysAreProperlyEscapedWithSchema()
+ public function testForeignKeysAreProperlyEscapedWithSchema(): void
{
$this->adapter->createSchema('schema_users');
@@ -2110,7 +2296,7 @@ public function testForeignKeysAreProperlyEscapedWithSchema()
$this->adapter->dropSchema('schema_users');
}
- public function testForeignKeysAreProperlyEscapedWithSchema2()
+ public function testForeignKeysAreProperlyEscapedWithSchema2(): void
{
$this->adapter->createSchema('schema_users');
$this->adapter->createSchema('schema_sessions');
@@ -2140,7 +2326,7 @@ public function testForeignKeysAreProperlyEscapedWithSchema2()
$this->adapter->dropSchema('schema_sessions');
}
- public function testTimestampWithTimezone()
+ public function testTimestampWithTimezone(): void
{
$table = new Table('tztable', ['id' => false], $this->adapter);
$table
@@ -2154,7 +2340,7 @@ public function testTimestampWithTimezone()
$columns = $this->adapter->getColumns('tztable');
foreach ($columns as $column) {
- if (substr($column->getName(), -4) === 'notz') {
+ if (str_ends_with((string)$column->getName(), 'notz')) {
$this->assertFalse($column->isTimezone(), 'column: ' . $column->getName());
} else {
$this->assertTrue($column->isTimezone(), 'column: ' . $column->getName());
@@ -2162,7 +2348,7 @@ public function testTimestampWithTimezone()
}
}
- public function testTimestampWithTimezoneWithSchema()
+ public function testTimestampWithTimezoneWithSchema(): void
{
$this->adapter->createSchema('tzschema');
@@ -2178,7 +2364,7 @@ public function testTimestampWithTimezoneWithSchema()
$columns = $this->adapter->getColumns('tzschema.tztable');
foreach ($columns as $column) {
- if (substr($column->getName(), -4) === 'notz') {
+ if (str_ends_with((string)$column->getName(), 'notz')) {
$this->assertFalse($column->isTimezone(), 'column: ' . $column->getName());
} else {
$this->assertTrue($column->isTimezone(), 'column: ' . $column->getName());
@@ -2188,7 +2374,7 @@ public function testTimestampWithTimezoneWithSchema()
$this->adapter->dropSchema('tzschema');
}
- public function testBulkInsertData()
+ public function testBulkInsertData(): void
{
$data = [
[
@@ -2222,7 +2408,7 @@ public function testBulkInsertData()
$this->assertEquals('test', $rows[2]['column3']);
}
- public function testBulkInsertBoolean()
+ public function testBulkInsertBoolean(): void
{
$data = [
[
@@ -2246,7 +2432,7 @@ public function testBulkInsertBoolean()
$this->assertNull($rows[2]['column1']);
}
- public function testBulkInsertLiteral()
+ public function testBulkInsertLiteral(): void
{
$data = [
[
@@ -2272,12 +2458,12 @@ public function testBulkInsertLiteral()
$this->assertEquals('value1', $rows[0]['column1']);
$this->assertEquals('value2', $rows[1]['column1']);
$this->assertEquals('value3', $rows[2]['column1']);
- $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']);
+ $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column2']);
$this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']);
$this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']);
}
- public function testInsertData()
+ public function testInsertData(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -2301,7 +2487,7 @@ public function testInsertData()
$this->assertEquals(2, $rows[1]['column2']);
}
- public function testInsertLiteral()
+ public function testInsertLiteral(): void
{
$data = [
[
@@ -2332,12 +2518,12 @@ public function testInsertLiteral()
$this->assertEquals('test', $rows[0]['column2']);
$this->assertEquals('test', $rows[1]['column2']);
$this->assertEquals('foo', $rows[2]['column2']);
- $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']);
+ $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column3']);
$this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']);
$this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']);
}
- public function testInsertBoolean()
+ public function testInsertBoolean(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'boolean', ['null' => true])
@@ -2362,7 +2548,7 @@ public function testInsertBoolean()
$this->assertNull($rows[2]['column1']);
}
- public function testInsertDataWithSchema()
+ public function testInsertDataWithSchema(): void
{
$this->adapter->createSchema('schema1');
@@ -2390,7 +2576,7 @@ public function testInsertDataWithSchema()
$this->adapter->dropSchema('schema1');
}
- public function testTruncateTable()
+ public function testTruncateTable(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -2414,7 +2600,7 @@ public function testTruncateTable()
$this->assertCount(0, $rows);
}
- public function testTruncateTableWithSchema()
+ public function testTruncateTableWithSchema(): void
{
$this->adapter->createSchema('schema1');
@@ -2442,7 +2628,7 @@ public function testTruncateTableWithSchema()
$this->adapter->dropSchema('schema1');
}
- public function testDumpCreateTable()
+ public function testDumpCreateTable(): void
{
$options = $this->adapter->getOptions();
$options['dryrun'] = true;
@@ -2455,7 +2641,7 @@ public function testDumpCreateTable()
->addColumn('column3', 'string', ['default' => 'test', 'null' => false])
->save();
- $actualOutput = join("\n", $this->out->messages());
+ $actualOutput = implode("\n", $this->out->messages());
// Check for key parts of the CREATE TABLE statement
// The identity column syntax varies between CakePHP/database versions
$this->assertStringContainsString(
@@ -2469,7 +2655,7 @@ public function testDumpCreateTable()
$this->assertStringContainsString('CONSTRAINT "table1_pkey" PRIMARY KEY ("id")', $actualOutput);
}
- public function testDumpCreateTableWithSchema()
+ public function testDumpCreateTableWithSchema(): void
{
$options = $this->adapter->getOptions();
$options['dryrun'] = true;
@@ -2482,7 +2668,7 @@ public function testDumpCreateTableWithSchema()
->addColumn('column3', 'string', ['default' => 'test', 'null' => false])
->save();
- $actualOutput = join("\n", $this->out->messages());
+ $actualOutput = implode("\n", $this->out->messages());
// Check for key parts of the CREATE TABLE statement
// The identity column syntax varies between CakePHP/database versions
$this->assertStringContainsString(
@@ -2501,7 +2687,7 @@ public function testDumpCreateTableWithSchema()
* Then enables dry run mode and inserts a record.
* Asserts that output contains the insert statement and doesn't insert a record.
*/
- public function testDumpInsert()
+ public function testDumpInsert(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -2538,11 +2724,11 @@ public function testDumpInsert()
OUTPUT;
}
- $actualOutput = join("\n", $this->out->messages());
+ $actualOutput = implode("\n", $this->out->messages());
$this->assertStringContainsString(
$expectedOutput,
$actualOutput,
- 'Passing the --dry-run option doesn\'t dump the insert to the output',
+ "Passing the --dry-run option doesn't dump the insert to the output",
);
$countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1');
@@ -2556,7 +2742,7 @@ public function testDumpInsert()
* Then enables dry run mode and inserts some records.
* Asserts that output contains the insert statement and doesn't insert any record.
*/
- public function testDumpBulkinsert()
+ public function testDumpBulkinsert(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -2588,11 +2774,11 @@ public function testDumpBulkinsert()
OUTPUT;
}
- $actualOutput = join("\n", $this->out->messages());
+ $actualOutput = implode("\n", $this->out->messages());
$this->assertStringContainsString(
$expectedOutput,
$actualOutput,
- 'Passing the --dry-run option doesn\'t dump the bulkinsert to the output',
+ "Passing the --dry-run option doesn't dump the bulkinsert to the output",
);
$countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1');
@@ -2601,7 +2787,7 @@ public function testDumpBulkinsert()
$this->assertEquals(0, $res[0]['count']);
}
- public function testDumpCreateTableAndThenInsert()
+ public function testDumpCreateTableAndThenInsert(): void
{
$options = $this->adapter->getOptions();
$options['dryrun'] = true;
@@ -2630,14 +2816,14 @@ public function testDumpCreateTableAndThenInsert()
OUTPUT;
}
- $actualOutput = join("\n", $this->out->messages());
+ $actualOutput = implode("\n", $this->out->messages());
$this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option does not dump create and then insert table queries to the output');
}
/**
* Tests interaction with the query builder
*/
- public function testQueryBuilder()
+ public function testQueryBuilder(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -2676,7 +2862,7 @@ public function testQueryBuilder()
$this->assertEquals(1, $stm->rowCount());
}
- public function testQueryWithParams()
+ public function testQueryWithParams(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -2707,7 +2893,7 @@ public function testQueryWithParams()
$this->assertEquals(3, $res[0]['c']);
}
- public function testRenameMixedCaseTableAndColumns()
+ public function testRenameMixedCaseTableAndColumns(): void
{
$table = new Table('OrganizationSettings', [], $this->adapter);
$table->addColumn('SettingType', 'string')
@@ -2752,7 +2938,7 @@ public function testSerialAliases(string $columnType): void
$this->assertFalse($column->isNull());
}
- public function testAddCheckConstraint()
+ public function testAddCheckConstraint(): void
{
$table = new Table('check_table', [], $this->adapter);
$table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2])
@@ -2764,7 +2950,22 @@ public function testAddCheckConstraint()
$this->assertTrue($this->adapter->hasCheckConstraint('check_table', 'price_positive'));
}
- public function testHasCheckConstraint()
+ public function testAddCheckConstraintAutoGeneratesName(): void
+ {
+ $table = new Table('check_table_auto', [], $this->adapter);
+ $table->addColumn('quantity', 'integer')
+ ->create();
+
+ $expression = 'quantity >= 0';
+ // An empty name must trigger auto-generation, not emit invalid `ADD CONSTRAINT CHECK`.
+ $checkConstraint = new CheckConstraint('', $expression);
+ $this->adapter->addCheckConstraint($table->getTable(), $checkConstraint);
+
+ $expectedName = 'check_table_auto_chk_' . substr(md5($expression), 0, 8);
+ $this->assertTrue($this->adapter->hasCheckConstraint('check_table_auto', $expectedName));
+ }
+
+ public function testHasCheckConstraint(): void
{
$table = new Table('check_table3', [], $this->adapter);
$table->addColumn('quantity', 'integer')
@@ -2778,7 +2979,7 @@ public function testHasCheckConstraint()
$this->assertTrue($this->adapter->hasCheckConstraint('check_table3', 'quantity_positive'));
}
- public function testDropCheckConstraint()
+ public function testDropCheckConstraint(): void
{
$table = new Table('check_table4', [], $this->adapter);
$table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2])
@@ -2792,7 +2993,7 @@ public function testDropCheckConstraint()
$this->assertFalse($this->adapter->hasCheckConstraint('check_table4', 'price_check'));
}
- public function testCheckConstraintWithComplexExpression()
+ public function testCheckConstraintWithComplexExpression(): void
{
$table = new Table('check_table5', [], $this->adapter);
$table->addColumn('email', 'string', ['limit' => 255])
@@ -2809,10 +3010,10 @@ public function testCheckConstraintWithComplexExpression()
// Verify the constraint is actually enforced
$quotedTableName = $this->adapter->getConnection()->getDriver()->quoteIdentifier('check_table5');
$this->expectException(PDOException::class);
- $this->adapter->execute("INSERT INTO {$quotedTableName} (email, status) VALUES ('test@example.com', 'invalid')");
+ $this->adapter->execute(sprintf("INSERT INTO %s (email, status) VALUES ('test@example.com', 'invalid')", $quotedTableName));
}
- public function testInsertOrSkipWithDuplicates()
+ public function testInsertOrSkipWithDuplicates(): void
{
$table = new Table('users', [], $this->adapter);
$table->addColumn('email', 'string', ['limit' => 255])
@@ -2835,7 +3036,7 @@ public function testInsertOrSkipWithDuplicates()
$this->assertEquals('John', $rows[0]['name']);
}
- public function testInsertModeResetsAfterInsertOrSkip()
+ public function testInsertModeResetsAfterInsertOrSkip(): void
{
$table = new Table('users', [], $this->adapter);
$table->addColumn('email', 'string', ['limit' => 255])
@@ -2855,7 +3056,7 @@ public function testInsertModeResetsAfterInsertOrSkip()
])->save();
}
- public function testBulkinsertOrSkipWithDuplicates()
+ public function testBulkinsertOrSkipWithDuplicates(): void
{
$table = new Table('products', [], $this->adapter);
$table->addColumn('sku', 'string', ['limit' => 50])
@@ -2882,7 +3083,7 @@ public function testBulkinsertOrSkipWithDuplicates()
$this->assertEquals('30.00', $rows[2]['price']);
}
- public function testInsertOrSkipWithoutDuplicates()
+ public function testInsertOrSkipWithoutDuplicates(): void
{
$table = new Table('categories', [], $this->adapter);
$table->addColumn('name', 'string')
@@ -2898,7 +3099,7 @@ public function testInsertOrSkipWithoutDuplicates()
$this->assertCount(2, $rows);
}
- public function testInsertOrUpdateWithDuplicates()
+ public function testInsertOrUpdateWithDuplicates(): void
{
$table = new Table('currencies', [], $this->adapter);
$table->addColumn('code', 'string', ['limit' => 3])
@@ -2931,7 +3132,7 @@ public function testInsertOrUpdateWithDuplicates()
$this->assertEquals('1.0500', $rows[2]['rate']); // USD updated
}
- public function testInsertOrUpdateWithMultipleUpdateColumns()
+ public function testInsertOrUpdateWithMultipleUpdateColumns(): void
{
$table = new Table('products', [], $this->adapter);
$table->addColumn('sku', 'string', ['limit' => 50])
@@ -2956,7 +3157,7 @@ public function testInsertOrUpdateWithMultipleUpdateColumns()
$this->assertEquals(50, $rows[0]['stock']);
}
- public function testInsertOrUpdateModeResetsAfterSave()
+ public function testInsertOrUpdateModeResetsAfterSave(): void
{
$table = new Table('items', [], $this->adapter);
$table->addColumn('code', 'string', ['limit' => 10])
@@ -2976,7 +3177,7 @@ public function testInsertOrUpdateModeResetsAfterSave()
])->save();
}
- public function testInsertOrUpdateRequiresConflictColumns()
+ public function testInsertOrUpdateRequiresConflictColumns(): void
{
$table = new Table('currencies', [], $this->adapter);
$table->addColumn('code', 'string', ['limit' => 3])
@@ -2992,7 +3193,7 @@ public function testInsertOrUpdateRequiresConflictColumns()
], ['rate'], [])->save();
}
- public function testAddSinglePartitionToExistingTable()
+ public function testAddSinglePartitionToExistingTable(): void
{
// Create a partitioned table with room to add more partitions
$table = new Table('partitioned_orders', ['id' => false, 'primary_key' => ['id', 'order_date']], $this->adapter);
@@ -3023,7 +3224,7 @@ public function testAddSinglePartitionToExistingTable()
$this->adapter->dropTable('partitioned_orders');
}
- public function testAddMultiplePartitionsToExistingTable()
+ public function testAddMultiplePartitionsToExistingTable(): void
{
// Create a partitioned table
$table = new Table('partitioned_sales', ['id' => false, 'primary_key' => ['id', 'sale_date']], $this->adapter);
@@ -3061,7 +3262,7 @@ public function testAddMultiplePartitionsToExistingTable()
$this->adapter->dropTable('partitioned_sales');
}
- public function testDropSinglePartitionFromExistingTable()
+ public function testDropSinglePartitionFromExistingTable(): void
{
// Create a partitioned table with multiple partitions
$table = new Table('partitioned_logs', ['id' => false, 'primary_key' => ['id']], $this->adapter);
@@ -3103,7 +3304,7 @@ public function testDropSinglePartitionFromExistingTable()
$this->adapter->dropTable('partitioned_logs');
}
- public function testDropMultiplePartitionsFromExistingTable()
+ public function testDropMultiplePartitionsFromExistingTable(): void
{
// Create a partitioned table with multiple partitions
$table = new Table('partitioned_archive', ['id' => false, 'primary_key' => ['id']], $this->adapter);
diff --git a/tests/TestCase/Db/Adapter/RecordingAdapterTest.php b/tests/TestCase/Db/Adapter/RecordingAdapterTest.php
index c5a684323..260979e9e 100644
--- a/tests/TestCase/Db/Adapter/RecordingAdapterTest.php
+++ b/tests/TestCase/Db/Adapter/RecordingAdapterTest.php
@@ -3,6 +3,12 @@
namespace Migrations\Test\Db\Adapter;
+use Migrations\Db\Action\DropForeignKey;
+use Migrations\Db\Action\DropIndex;
+use Migrations\Db\Action\DropTable;
+use Migrations\Db\Action\RemoveColumn;
+use Migrations\Db\Action\RenameColumn;
+use Migrations\Db\Action\RenameTable;
use Migrations\Db\Adapter\AbstractAdapter;
use Migrations\Db\Adapter\RecordingAdapter;
use Migrations\Db\Table;
@@ -12,10 +18,7 @@
class RecordingAdapterTest extends TestCase
{
- /**
- * @var \Migrations\Db\Adapter\RecordingAdapter
- */
- private $adapter;
+ private RecordingAdapter $adapter;
protected function setUp(): void
{
@@ -35,30 +38,30 @@ protected function tearDown(): void
unset($this->adapter);
}
- public function testRecordingAdapterCanInvertCreateTable()
+ public function testRecordingAdapterCanInvertCreateTable(): void
{
$table = new Table('atable', [], $this->adapter);
$table->addColumn('column1', 'string')
->save();
$commands = $this->adapter->getInvertedCommands()->getActions();
- $this->assertInstanceOf('Migrations\Db\Action\DropTable', $commands[0]);
+ $this->assertInstanceOf(DropTable::class, $commands[0]);
$this->assertEquals('atable', $commands[0]->getTable()->getName());
}
- public function testRecordingAdapterCanInvertRenameTable()
+ public function testRecordingAdapterCanInvertRenameTable(): void
{
$table = new Table('oldname', [], $this->adapter);
$table->rename('newname')
->save();
$commands = $this->adapter->getInvertedCommands()->getActions();
- $this->assertInstanceOf('Migrations\Db\Action\RenameTable', $commands[0]);
+ $this->assertInstanceOf(RenameTable::class, $commands[0]);
$this->assertEquals('newname', $commands[0]->getTable()->getName());
$this->assertEquals('oldname', $commands[0]->getNewName());
}
- public function testRecordingAdapterCanInvertAddColumn()
+ public function testRecordingAdapterCanInvertAddColumn(): void
{
$this->adapter
->getAdapter()
@@ -82,12 +85,12 @@ public function testRecordingAdapterCanInvertAddColumn()
->save();
$commands = $this->adapter->getInvertedCommands()->getActions();
- $this->assertInstanceOf('Migrations\Db\Action\RemoveColumn', $commands[0]);
+ $this->assertInstanceOf(RemoveColumn::class, $commands[0]);
$this->assertEquals('atable', $commands[0]->getTable()->getName());
$this->assertEquals('acolumn', $commands[0]->getColumn()->getName());
}
- public function testRecordingAdapterCanInvertRenameColumn()
+ public function testRecordingAdapterCanInvertRenameColumn(): void
{
$this->adapter
->getAdapter()
@@ -100,12 +103,12 @@ public function testRecordingAdapterCanInvertRenameColumn()
->save();
$commands = $this->adapter->getInvertedCommands()->getActions();
- $this->assertInstanceOf('Migrations\Db\Action\RenameColumn', $commands[0]);
+ $this->assertInstanceOf(RenameColumn::class, $commands[0]);
$this->assertEquals('newname', $commands[0]->getColumn()->getName());
$this->assertEquals('oldname', $commands[0]->getNewName());
}
- public function testRecordingAdapterCanInvertAddIndex()
+ public function testRecordingAdapterCanInvertAddIndex(): void
{
$this->adapter
->getAdapter()
@@ -118,12 +121,12 @@ public function testRecordingAdapterCanInvertAddIndex()
->save();
$commands = $this->adapter->getInvertedCommands()->getActions();
- $this->assertInstanceOf('Migrations\Db\Action\DropIndex', $commands[0]);
+ $this->assertInstanceOf(DropIndex::class, $commands[0]);
$this->assertEquals('atable', $commands[0]->getTable()->getName());
$this->assertEquals(['email'], $commands[0]->getIndex()->getColumns());
}
- public function testRecordingAdapterCanInvertAddForeignKey()
+ public function testRecordingAdapterCanInvertAddForeignKey(): void
{
$this->adapter
->getAdapter()
@@ -136,12 +139,12 @@ public function testRecordingAdapterCanInvertAddForeignKey()
->save();
$commands = $this->adapter->getInvertedCommands()->getActions();
- $this->assertInstanceOf('Migrations\Db\Action\DropForeignKey', $commands[0]);
+ $this->assertInstanceOf(DropForeignKey::class, $commands[0]);
$this->assertEquals('atable', $commands[0]->getTable()->getName());
$this->assertEquals(['ref_table_id'], $commands[0]->getForeignKey()->getColumns());
}
- public function testGetInvertedCommandsThrowsExceptionForIrreversibleCommand()
+ public function testGetInvertedCommandsThrowsExceptionForIrreversibleCommand(): void
{
$this->adapter
->getAdapter()
diff --git a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php
index 5b3c80fae..7471012de 100644
--- a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php
+++ b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php
@@ -27,13 +27,12 @@
class SqliteAdapterTest extends TestCase
{
private array $config;
+
private StubConsoleOutput $out;
+
private ConsoleIo $io;
- /**
- * @var \Migrations\Db\Adapter\SqliteAdapter
- */
- private $adapter;
+ private SqliteAdapter $adapter;
protected function setUp(): void
{
@@ -78,17 +77,17 @@ protected function tearDown(): void
unset($this->adapter, $this->out, $this->io);
}
- public function testGetConnection()
+ public function testGetConnection(): void
{
error_reporting(E_ALL);
- $this->deprecated(function () {
+ $this->deprecated(function (): void {
$connection = $this->adapter->getConnection();
$this->assertInstanceOf(Connection::class, $connection);
$this->assertSame($connection, $this->adapter->getDecoratedConnection());
});
}
- public function testBeginTransaction()
+ public function testBeginTransaction(): void
{
$this->adapter->beginTransaction();
@@ -98,7 +97,7 @@ public function testBeginTransaction()
);
}
- public function testRollbackTransaction()
+ public function testRollbackTransaction(): void
{
$this->adapter->beginTransaction();
$this->adapter->rollbackTransaction();
@@ -109,7 +108,7 @@ public function testRollbackTransaction()
);
}
- public function testCommitTransactionTransaction()
+ public function testCommitTransactionTransaction(): void
{
$this->adapter->beginTransaction();
$this->adapter->commitTransaction();
@@ -120,7 +119,7 @@ public function testCommitTransactionTransaction()
);
}
- public function testCreatingTheSchemaTableOnConnect()
+ public function testCreatingTheSchemaTableOnConnect(): void
{
$this->adapter->connect();
$this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName()));
@@ -131,24 +130,24 @@ public function testCreatingTheSchemaTableOnConnect()
$this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName()));
}
- public function testSchemaTableIsCreatedWithPrimaryKey()
+ public function testSchemaTableIsCreatedWithPrimaryKey(): void
{
$this->adapter->connect();
new Table($this->adapter->getSchemaTableName(), [], $this->adapter);
$this->assertTrue($this->adapter->hasIndex($this->adapter->getSchemaTableName(), ['version']));
}
- public function testQuoteTableName()
+ public function testQuoteTableName(): void
{
$this->assertEquals('"test_table"', $this->adapter->quoteTableName('test_table'));
}
- public function testQuoteColumnName()
+ public function testQuoteColumnName(): void
{
$this->assertEquals('"test_column"', $this->adapter->quoteColumnName('test_column'));
}
- public function testCreateTable()
+ public function testCreateTable(): void
{
$table = new Table('ntable', [], $this->adapter);
$table->addColumn('realname', 'string')
@@ -161,7 +160,7 @@ public function testCreateTable()
$this->assertFalse($this->adapter->hasColumn('ntable', 'address'));
}
- public function testCreateTableCustomIdColumn()
+ public function testCreateTableCustomIdColumn(): void
{
$table = new Table('ntable', ['id' => 'custom_id'], $this->adapter);
$table->addColumn('realname', 'string')
@@ -181,7 +180,7 @@ public function testCreateTableCustomIdColumn()
$this->assertFalse($idColumn->isNull());
}
- public function testCreateTableIdentityIdColumn()
+ public function testCreateTableIdentityIdColumn(): void
{
$table = new Table('ntable', ['id' => false, 'primary_key' => ['custom_id']], $this->adapter);
$table->addColumn('custom_id', 'integer', ['identity' => true])
@@ -195,7 +194,7 @@ public function testCreateTableIdentityIdColumn()
$this->assertTrue($idColumn->getIdentity());
}
- public function testCreateTableWithNoPrimaryKey()
+ public function testCreateTableWithNoPrimaryKey(): void
{
$options = [
'id' => false,
@@ -206,7 +205,7 @@ public function testCreateTableWithNoPrimaryKey()
$this->assertFalse($this->adapter->hasColumn('atable', 'id'));
}
- public function testCreateTableWithMultiplePrimaryKeys()
+ public function testCreateTableWithMultiplePrimaryKeys(): void
{
$options = [
'id' => false,
@@ -226,7 +225,7 @@ public function testCreateTableWithMultiplePrimaryKeys()
/**
* @return void
*/
- public function testCreateTableWithPrimaryKeyAsUuid()
+ public function testCreateTableWithPrimaryKeyAsUuid(): void
{
$options = [
'id' => false,
@@ -241,7 +240,7 @@ public function testCreateTableWithPrimaryKeyAsUuid()
/**
* @return void
*/
- public function testCreateTableWithPrimaryKeyAsBinaryUuid()
+ public function testCreateTableWithPrimaryKeyAsBinaryUuid(): void
{
$options = [
'id' => false,
@@ -253,7 +252,7 @@ public function testCreateTableWithPrimaryKeyAsBinaryUuid()
$this->assertTrue($this->adapter->hasIndex('ztable', 'id'));
}
- public function testCreateTableWithMultipleIndexes()
+ public function testCreateTableWithMultipleIndexes(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -267,7 +266,7 @@ public function testCreateTableWithMultipleIndexes()
$this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_name']));
}
- public function testCreateTableWithUniqueIndexes()
+ public function testCreateTableWithUniqueIndexes(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -277,7 +276,7 @@ public function testCreateTableWithUniqueIndexes()
$this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_email']));
}
- public function testCreateTableWithNamedIndexes()
+ public function testCreateTableWithNamedIndexes(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -288,7 +287,7 @@ public function testCreateTableWithNamedIndexes()
$this->assertTrue($this->adapter->hasIndexByName('table1', 'myemailindex'));
}
- public function testCreateTableWithForeignKey()
+ public function testCreateTableWithForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -302,7 +301,7 @@ public function testCreateTableWithForeignKey()
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testCreateTableWithIndexesAndForeignKey()
+ public function testCreateTableWithIndexesAndForeignKey(): void
{
$refTable = new Table('tbl_master', [], $this->adapter);
$refTable->create();
@@ -335,7 +334,7 @@ public function testCreateTableWithIndexesAndForeignKey()
);
}
- public function testCreateTableWithoutAutoIncrementingPrimaryKeyAndWithForeignKey()
+ public function testCreateTableWithoutAutoIncrementingPrimaryKeyAndWithForeignKey(): void
{
$refTable = (new Table('tbl_master', ['id' => false, 'primary_key' => 'id'], $this->adapter))
->addColumn('id', 'text');
@@ -379,12 +378,18 @@ public function testCreateTableIndexWithWhere(): void
->addIndex($index)
->save();
$queries = $this->out->messages();
- $indexQuery = $queries[2];
+ $indexQuery = '';
+ foreach ($queries as $query) {
+ if (str_contains($query, 'CREATE UNIQUE INDEX "table1_email_index"')) {
+ $indexQuery = $query;
+ break;
+ }
+ }
$this->assertStringContainsString('CREATE UNIQUE INDEX "table1_email_index"', $indexQuery);
$this->assertStringContainsString('("email" ASC) WHERE is_verified = true', $indexQuery);
}
- public function testAddPrimaryKey()
+ public function testAddPrimaryKey(): void
{
$table = new Table('table1', [], $this->adapter);
$table
@@ -396,7 +401,7 @@ public function testAddPrimaryKey()
$this->assertTrue($this->adapter->hasPrimaryKey('table1', ['id']));
}
- public function testChangePrimaryKey()
+ public function testChangePrimaryKey(): void
{
$table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter);
$table
@@ -412,7 +417,7 @@ public function testChangePrimaryKey()
$this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column2']));
}
- public function testChangePrimaryKeyNonInteger()
+ public function testChangePrimaryKeyNonInteger(): void
{
$table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter);
$table
@@ -428,7 +433,7 @@ public function testChangePrimaryKeyNonInteger()
$this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column2']));
}
- public function testChangePrimaryKeyWithoutAutoIncrement()
+ public function testChangePrimaryKeyWithoutAutoIncrement(): void
{
// Create table with id_1 as PK without AUTOINCREMENT keyword
$this->adapter->execute('CREATE TABLE table1 (id_1 INTEGER NOT NULL PRIMARY KEY, id_2 INTEGER NOT NULL)');
@@ -450,7 +455,7 @@ public function testChangePrimaryKeyWithoutAutoIncrement()
$this->assertStringNotContainsString('AUTOINCREMENT', $result['sql'], 'AUTOINCREMENT should not be added when changing PK to a column that did not have it');
}
- public function testChangePrimaryKeyFromAutoIncrementColumn()
+ public function testChangePrimaryKeyFromAutoIncrementColumn(): void
{
// Create table with id_1 as PK with AUTOINCREMENT
$this->adapter->execute('CREATE TABLE table1 (id_1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, id_2 INTEGER NOT NULL)');
@@ -473,7 +478,7 @@ public function testChangePrimaryKeyFromAutoIncrementColumn()
$this->assertStringNotContainsString('AUTOINCREMENT', $result['sql'], 'AUTOINCREMENT should not be added when changing PK to a column that never had it');
}
- public function testDropPrimaryKey()
+ public function testDropPrimaryKey(): void
{
$table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter);
$table
@@ -488,7 +493,7 @@ public function testDropPrimaryKey()
$this->assertFalse($this->adapter->hasPrimaryKey('table1', ['column1']));
}
- public function testAddMultipleColumnPrimaryKeyFails()
+ public function testAddMultipleColumnPrimaryKeyFails(): void
{
$table = new Table('table1', [], $this->adapter);
$table
@@ -503,7 +508,7 @@ public function testAddMultipleColumnPrimaryKeyFails()
->save();
}
- public function testChangeCommentFails()
+ public function testChangeCommentFails(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -515,7 +520,7 @@ public function testChangeCommentFails()
->save();
}
- public function testRenameTable()
+ public function testRenameTable(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -526,7 +531,7 @@ public function testRenameTable()
$this->assertTrue($this->adapter->hasTable('table2'));
}
- public function testAddColumn()
+ public function testAddColumn(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -541,7 +546,7 @@ public function testAddColumn()
// $this->assertEquals('realname', $rows[1]['Field']);
}
- public function testAddColumnWithDefaultValue()
+ public function testAddColumnWithDefaultValue(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -551,7 +556,7 @@ public function testAddColumnWithDefaultValue()
$this->assertEquals("'test'", $rows[1]['dflt_value']);
}
- public function testAddColumnWithDefaultZero()
+ public function testAddColumnWithDefaultZero(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -562,7 +567,7 @@ public function testAddColumnWithDefaultZero()
$this->assertEquals('0', $rows[1]['dflt_value']);
}
- public function testAddColumnWithDefaultEmptyString()
+ public function testAddColumnWithDefaultEmptyString(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -572,7 +577,7 @@ public function testAddColumnWithDefaultEmptyString()
$this->assertEquals("''", $rows[1]['dflt_value']);
}
- public function testAddDecimalWithPrecisionAndScale()
+ public function testAddDecimalWithPrecisionAndScale(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -620,7 +625,7 @@ public function testAddColumnToIrregularCreateTableStatements(string $createTabl
}
}
- public function testRenameColumn()
+ public function testRenameColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -631,7 +636,7 @@ public function testRenameColumn()
$this->assertTrue($this->adapter->hasColumn('t', 'column2'));
}
- public function testRenamingANonExistentColumn()
+ public function testRenamingANonExistentColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -642,7 +647,7 @@ public function testRenamingANonExistentColumn()
$this->adapter->renameColumn('t', 'column2', 'column1');
}
- public function testRenameColumnWithIndex()
+ public function testRenameColumnWithIndex(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -659,7 +664,7 @@ public function testRenameColumnWithIndex()
$this->assertTrue($this->adapter->hasIndex($table->getName(), 'newindexcol'));
}
- public function testRenameColumnWithUniqueIndex()
+ public function testRenameColumnWithUniqueIndex(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -676,7 +681,7 @@ public function testRenameColumnWithUniqueIndex()
$this->assertTrue($this->adapter->hasIndex($table->getName(), 'newindexcol'));
}
- public function testRenameColumnWithCompositeIndex()
+ public function testRenameColumnWithCompositeIndex(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -698,7 +703,7 @@ public function testRenameColumnWithCompositeIndex()
* Tests that rewriting the index SQL does not accidentally change
* the table name in case it matches the column name.
*/
- public function testRenameColumnWithIndexMatchingTheTableName()
+ public function testRenameColumnWithIndexMatchingTheTableName(): void
{
$table = new Table('indexcol', [], $this->adapter);
$table
@@ -719,7 +724,7 @@ public function testRenameColumnWithIndexMatchingTheTableName()
* Tests that rewriting the index SQL does not accidentally change
* column names that partially match the column to rename.
*/
- public function testRenameColumnWithIndexColumnPartialMatch()
+ public function testRenameColumnWithIndexColumnPartialMatch(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -738,7 +743,7 @@ public function testRenameColumnWithIndexColumnPartialMatch()
$this->assertTrue($this->adapter->hasIndex($table->getName(), ['indexcolumn', 'newindexcol']));
}
- public function testRenameColumnWithIndexColumnRequiringQuoting()
+ public function testRenameColumnWithIndexColumnRequiringQuoting(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -758,7 +763,7 @@ public function testRenameColumnWithIndexColumnRequiringQuoting()
/**
* Indices that are using expressions are not being updated.
*/
- public function testRenameColumnWithExpressionIndex()
+ public function testRenameColumnWithExpressionIndex(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -847,7 +852,7 @@ public static function customIndexSQLDataProvider(): array
* @param string $newIndexSQL Expected new index creation SQL
*/
#[DataProvider('customIndexSQLDataProvider')]
- public function testRenameColumnWithCustomIndex(string $indexSQL, string $newIndexSQL)
+ public function testRenameColumnWithCustomIndex(string $indexSQL, string $newIndexSQL): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -946,7 +951,7 @@ public static function customCompositeIndexSQLDataProvider(): array
* @param string $newIndexSQL Expected new index creation SQL
*/
#[DataProvider('customCompositeIndexSQLDataProvider')]
- public function testRenameColumnWithCustomCompositeIndex(string $indexSQL, string $newIndexSQL)
+ public function testRenameColumnWithCustomCompositeIndex(string $indexSQL, string $newIndexSQL): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -969,7 +974,7 @@ public function testRenameColumnWithCustomCompositeIndex(string $indexSQL, strin
$this->assertSame($index['sql'], $newIndexSQL);
}
- public function testChangeColumn()
+ public function testChangeColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -978,6 +983,7 @@ public function testChangeColumn()
$newColumn1 = new Column();
$newColumn1->setName('column1');
$newColumn1->setType('string');
+
$table->changeColumn('column1', $newColumn1);
$this->assertTrue($this->adapter->hasColumn('t', 'column1'));
$newColumn2 = new Column();
@@ -988,7 +994,7 @@ public function testChangeColumn()
$this->assertTrue($this->adapter->hasColumn('t', 'column2'));
}
- public function testChangeColumnDefaultValue()
+ public function testChangeColumnDefaultValue(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'test'])
@@ -1004,7 +1010,7 @@ public function testChangeColumnDefaultValue()
$this->assertEquals("'test1'", $rows[1]['dflt_value']);
}
- public function testChangeColumnWithForeignKey()
+ public function testChangeColumnWithForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -1022,7 +1028,7 @@ public function testChangeColumnWithForeignKey()
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testChangeColumnWithIndex()
+ public function testChangeColumnWithIndex(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -1040,7 +1046,7 @@ public function testChangeColumnWithIndex()
$this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcol'));
}
- public function testChangeColumnWithTrigger()
+ public function testChangeColumnWithTrigger(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -1075,7 +1081,7 @@ public function testChangeColumnWithTrigger()
$this->assertEquals($triggerSQL, $rows[0]['sql']);
}
- public function testChangeColumnDefaultToZero()
+ public function testChangeColumnDefaultToZero(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'integer')
@@ -1089,7 +1095,7 @@ public function testChangeColumnDefaultToZero()
$this->assertEquals('0', $rows[1]['dflt_value']);
}
- public function testChangeColumnDefaultToNull()
+ public function testChangeColumnDefaultToNull(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'test'])
@@ -1103,7 +1109,7 @@ public function testChangeColumnDefaultToNull()
$this->assertNull($rows[1]['dflt_value']);
}
- public function testChangeColumnWithCommasInCommentsOrDefaultValue()
+ public function testChangeColumnWithCommasInCommentsOrDefaultValue(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'one, two or three', 'comment' => 'three, two or one'])
@@ -1119,11 +1125,11 @@ public function testChangeColumnWithCommasInCommentsOrDefaultValue()
}
#[DataProvider('columnCreationArgumentProvider')]
- public function testDropColumn($columnCreationArgs)
+ public function testDropColumn(array $columnCreationArgs): void
{
$table = new Table('t', [], $this->adapter);
$columnName = $columnCreationArgs[0];
- call_user_func_array([$table, 'addColumn'], $columnCreationArgs);
+ $table->addColumn(...$columnCreationArgs);
$table->save();
$this->assertTrue($this->adapter->hasColumn('t', $columnName));
@@ -1132,7 +1138,7 @@ public function testDropColumn($columnCreationArgs)
$this->assertFalse($this->adapter->hasColumn('t', $columnName));
}
- public function testDropColumnWithIndex()
+ public function testDropColumnWithIndex(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -1147,7 +1153,7 @@ public function testDropColumnWithIndex()
$this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol'));
}
- public function testDropColumnWithUniqueIndex()
+ public function testDropColumnWithUniqueIndex(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -1162,7 +1168,7 @@ public function testDropColumnWithUniqueIndex()
$this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol'));
}
- public function testDropColumnWithCompositeIndex()
+ public function testDropColumnWithCompositeIndex(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -1182,7 +1188,7 @@ public function testDropColumnWithCompositeIndex()
* Tests that removing columns does not accidentally drop indices
* on table names that match the column to remove.
*/
- public function testDropColumnWithIndexMatchingTheTableName()
+ public function testDropColumnWithIndexMatchingTheTableName(): void
{
$table = new Table('indexcol', [], $this->adapter);
$table
@@ -1202,7 +1208,7 @@ public function testDropColumnWithIndexMatchingTheTableName()
* Tests that removing columns does not accidentally drop indices
* that contain column names that partially match the column to remove.
*/
- public function testDropColumnWithIndexColumnPartialMatch()
+ public function testDropColumnWithIndexColumnPartialMatch(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -1222,7 +1228,7 @@ public function testDropColumnWithIndexColumnPartialMatch()
/**
* Indices with expressions are not being removed.
*/
- public function testDropColumnWithExpressionIndex()
+ public function testDropColumnWithExpressionIndex(): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -1244,7 +1250,7 @@ public function testDropColumnWithExpressionIndex()
* @param string $newIndexSQL Expected new index creation SQL
*/
#[DataProvider('customIndexSQLDataProvider')]
- public function testDropColumnWithCustomIndex(string $indexSQL, string $newIndexSQL)
+ public function testDropColumnWithCustomIndex(string $indexSQL, string $newIndexSQL): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -1265,7 +1271,7 @@ public function testDropColumnWithCustomIndex(string $indexSQL, string $newIndex
* @param string $newIndexSQL Expected new index creation SQL
*/
#[DataProvider('customCompositeIndexSQLDataProvider')]
- public function testDropColumnWithCustomCompositeIndex(string $indexSQL, string $newIndexSQL)
+ public function testDropColumnWithCustomCompositeIndex(string $indexSQL, string $newIndexSQL): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -1285,7 +1291,7 @@ public function testDropColumnWithCustomCompositeIndex(string $indexSQL, string
$this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol3']));
}
- public static function columnCreationArgumentProvider()
+ public static function columnCreationArgumentProvider(): array
{
return [
[['column1', 'string']],
@@ -1293,7 +1299,7 @@ public static function columnCreationArgumentProvider()
];
}
- public static function columnsProvider()
+ public static function columnsProvider(): array
{
return [
['column1', 'string', []],
@@ -1313,7 +1319,7 @@ public static function columnsProvider()
];
}
- public function testAddIndex()
+ public function testAddIndex(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -1324,7 +1330,7 @@ public function testAddIndex()
$this->assertTrue($table->hasIndex('email'));
}
- public function testDropIndex()
+ public function testDropIndex(): void
{
// single column index
$table = new Table('table1', [], $this->adapter);
@@ -1365,7 +1371,7 @@ public function testDropIndex()
$this->assertFalse($table4->hasIndex(['fname', 'lname']));
}
- public function testDropIndexByName()
+ public function testDropIndexByName(): void
{
// single column index
$table = new Table('table1', [], $this->adapter);
@@ -1387,7 +1393,7 @@ public function testDropIndexByName()
$this->assertFalse($table2->hasIndex(['fname', 'lname']));
}
- public function testAddForeignKey()
+ public function testAddForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -1401,7 +1407,7 @@ public function testAddForeignKey()
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testDropForeignKey()
+ public function testDropForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')
@@ -1431,7 +1437,7 @@ public function testDropForeignKey()
$this->assertTrue($this->adapter->hasTable($table->getName()));
}
- public function testDropForeignKeyWithQuoteVariants()
+ public function testDropForeignKeyWithQuoteVariants(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')
@@ -1465,7 +1471,7 @@ public function testDropForeignKeyWithQuoteVariants()
$this->assertFalse($this->adapter->hasForeignKey('table', ['ref_lots_of_space', 'ref_lots_of_space']));
}
- public function testDropForeignKeyWithMultipleColumns()
+ public function testDropForeignKeyWithMultipleColumns(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable
@@ -1525,11 +1531,8 @@ public static function nonExistentForeignKeyColumnsProvider(): array
];
}
- /**
- * @param array $columns
- */
#[DataProvider('nonExistentForeignKeyColumnsProvider')]
- public function testDropForeignKeyByNonExistentKeyColumns(array $columns)
+ public function testDropForeignKeyByNonExistentKeyColumns(array $columns): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable
@@ -1557,7 +1560,7 @@ public function testDropForeignKeyByNonExistentKeyColumns(array $columns)
$this->adapter->dropForeignKey($table->getName(), $columns);
}
- public function testDropForeignKeyCaseInsensitivity()
+ public function testDropForeignKeyCaseInsensitivity(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->save();
@@ -1572,7 +1575,7 @@ public function testDropForeignKeyCaseInsensitivity()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testDropForeignKeyByName()
+ public function testDropForeignKeyByName(): void
{
$this->expectExceptionMessage('SQLite does not have named foreign keys');
$this->expectException(BadMethodCallException::class);
@@ -1594,7 +1597,7 @@ public function testDropForeignKeyByName()
$this->adapter->dropForeignKey($table->getName(), [], 'my_constraint');
}
- public function testHasDatabase()
+ public function testHasDatabase(): void
{
if ($this->config['database'] === ':memory:') {
$this->markTestSkipped('Skipping hasDatabase() when testing in-memory db.');
@@ -1603,7 +1606,7 @@ public function testHasDatabase()
$this->assertTrue($this->adapter->hasDatabase($this->config['database']));
}
- public function testDropDatabase()
+ public function testDropDatabase(): void
{
$this->assertFalse($this->adapter->hasDatabase('phinx_temp_database'));
$this->adapter->createDatabase('phinx_temp_database');
@@ -1611,7 +1614,7 @@ public function testDropDatabase()
$this->adapter->dropDatabase('phinx_temp_database');
}
- public function testAddColumnWithComment()
+ public function testAddColumnWithComment(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'string', ['comment' => 'Comments from "column1"'])
@@ -1628,7 +1631,7 @@ public function testAddColumnWithComment()
$this->assertMatchesRegularExpression('/\/\* Comments from "column1" \*\//', $sql);
}
- public function testAddColumnTableWithConstraint()
+ public function testAddColumnTableWithConstraint(): void
{
$this->adapter->execute('PRAGMA foreign_keys = ON');
$roles = new Table('constraint_roles', [], $this->adapter);
@@ -1648,13 +1651,13 @@ public function testAddColumnTableWithConstraint()
$updatedRoles
->addColumn('description', 'string', ['default' => 'short desc'])
->update();
- $res = $this->adapter->fetchAll('select * from sqlite_master where type = \'table\'');
+ $res = $this->adapter->fetchAll("select * from sqlite_master where type = 'table'");
$res = $this->adapter->fetchRow('select * from constraint_roles LIMIT 1');
$this->assertArrayHasKey('description', $res, 'Should have new column in output');
$this->assertEquals('short desc', $res['description']);
}
- public function testAddIndexTwoTablesSameIndex()
+ public function testAddIndexTwoTablesSameIndex(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -1675,7 +1678,7 @@ public function testAddIndexTwoTablesSameIndex()
$this->assertTrue($table2->hasIndex('email'));
}
- public function testBulkInsertData()
+ public function testBulkInsertData(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -1698,7 +1701,7 @@ public function testBulkInsertData()
)
->insert(
[
- 'column1' => '\'value4\'',
+ 'column1' => "'value4'",
'column2' => null,
],
)
@@ -1708,14 +1711,14 @@ public function testBulkInsertData()
$this->assertEquals('value1', $rows[0]['column1']);
$this->assertEquals('value2', $rows[1]['column1']);
$this->assertEquals('value3', $rows[2]['column1']);
- $this->assertEquals('\'value4\'', $rows[3]['column1']);
+ $this->assertEquals("'value4'", $rows[3]['column1']);
$this->assertEquals(1, $rows[0]['column2']);
$this->assertEquals(2, $rows[1]['column2']);
$this->assertEquals(3, $rows[2]['column2']);
$this->assertNull($rows[3]['column2']);
}
- public function testBulkInsertLiteral()
+ public function testBulkInsertLiteral(): void
{
$data = [
[
@@ -1741,12 +1744,12 @@ public function testBulkInsertLiteral()
$this->assertEquals('value1', $rows[0]['column1']);
$this->assertEquals('value2', $rows[1]['column1']);
$this->assertEquals('value3', $rows[2]['column1']);
- $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']);
+ $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column2']);
$this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']);
$this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']);
}
- public function testInsertData()
+ public function testInsertData(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -1769,7 +1772,7 @@ public function testInsertData()
)
->insert(
[
- 'column1' => '\'value4\'',
+ 'column1' => "'value4'",
'column2' => null,
],
)
@@ -1780,14 +1783,14 @@ public function testInsertData()
$this->assertEquals('value1', $rows[0]['column1']);
$this->assertEquals('value2', $rows[1]['column1']);
$this->assertEquals('value3', $rows[2]['column1']);
- $this->assertEquals('\'value4\'', $rows[3]['column1']);
+ $this->assertEquals("'value4'", $rows[3]['column1']);
$this->assertEquals(1, $rows[0]['column2']);
$this->assertEquals(2, $rows[1]['column2']);
$this->assertEquals(3, $rows[2]['column2']);
$this->assertNull($rows[3]['column2']);
}
- public function testInsertLiteral()
+ public function testInsertLiteral(): void
{
$data = [
[
@@ -1818,12 +1821,12 @@ public function testInsertLiteral()
$this->assertEquals('test', $rows[0]['column2']);
$this->assertEquals('test', $rows[1]['column2']);
$this->assertEquals('foo', $rows[2]['column2']);
- $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']);
+ $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column3']);
$this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']);
$this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']);
}
- public function testBulkInsertDataEnum()
+ public function testBulkInsertDataEnum(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -1841,7 +1844,7 @@ public function testBulkInsertDataEnum()
$this->assertEquals('c', $rows[0]['column3']);
}
- public function testNullWithoutDefaultValue()
+ public function testNullWithoutDefaultValue(): void
{
// construct table with default/null combinations
$table = new Table('table1', [], $this->adapter);
@@ -1878,7 +1881,7 @@ public function testNullWithoutDefaultValue()
$this->assertEquals('some2', $dd->getDefault());
}
- public function testDumpCreateTable()
+ public function testDumpCreateTable(): void
{
$this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]);
$table = new Table('table1', [], $this->adapter);
@@ -1891,7 +1894,7 @@ public function testDumpCreateTable()
$expectedOutput = <<<'OUTPUT'
CREATE TABLE "table1" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "column1" VARCHAR NOT NULL, "column2" INTEGER, "column3" VARCHAR DEFAULT 'test');
OUTPUT;
- $actualOutput = join("\n", $this->out->messages());
+ $actualOutput = implode("\n", $this->out->messages());
$this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option does not dump create table query to the output');
}
@@ -1900,7 +1903,7 @@ public function testDumpCreateTable()
* Then sets phinx to dry run mode and inserts a record.
* Asserts that phinx outputs the insert statement and doesn't insert a record.
*/
- public function testDumpInsert()
+ public function testDumpInsert(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -1925,9 +1928,9 @@ public function testDumpInsert()
INSERT INTO "table1" ("string_col") VALUES (null);
INSERT INTO "table1" ("int_col") VALUES (23);
OUTPUT;
- $actualOutput = join("\n", $this->out->messages());
+ $actualOutput = implode("\n", $this->out->messages());
$actualOutput = preg_replace("/\r\n|\r/", "\n", $actualOutput); // normalize line endings for Windows
- $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option doesn\'t dump the insert to the output');
+ $this->assertStringContainsString($expectedOutput, $actualOutput, "Passing the --dry-run option doesn't dump the insert to the output");
$countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1');
$this->assertTrue($countQuery->execute());
@@ -1940,7 +1943,7 @@ public function testDumpInsert()
* Then sets phinx to dry run mode and inserts some records.
* Asserts that phinx outputs the insert statement and doesn't insert any record.
*/
- public function testDumpBulkinsert()
+ public function testDumpBulkinsert(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -1962,8 +1965,8 @@ public function testDumpBulkinsert()
$expectedOutput = <<<'OUTPUT'
INSERT INTO "table1" ("string_col", "int_col") VALUES ('test_data1', 23), (null, 42);
OUTPUT;
- $actualOutput = join("\n", $this->out->messages());
- $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option doesn\'t dump the bulkinsert to the output');
+ $actualOutput = implode("\n", $this->out->messages());
+ $this->assertStringContainsString($expectedOutput, $actualOutput, "Passing the --dry-run option doesn't dump the bulkinsert to the output");
$countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1');
$this->assertTrue($countQuery->execute());
@@ -1971,7 +1974,7 @@ public function testDumpBulkinsert()
$this->assertEquals(0, $res[0][0]);
}
- public function testDumpCreateTableAndThenInsert()
+ public function testDumpCreateTableAndThenInsert(): void
{
$this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]);
$table = new Table('table1', ['id' => false, 'primary_key' => ['column1']], $this->adapter);
@@ -1992,7 +1995,7 @@ public function testDumpCreateTableAndThenInsert()
CREATE TABLE "table1" ("column1" VARCHAR NOT NULL, "column2" INTEGER, PRIMARY KEY ("column1"));
INSERT INTO "table1" ("column1", "column2") VALUES ('id1', 1);
OUTPUT;
- $actualOutput = join("\n", $this->out->messages());
+ $actualOutput = implode("\n", $this->out->messages());
$actualOutput = preg_replace("/\r\n|\r/", "\n", $actualOutput); // normalize line endings for Windows
$this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option does not dump create and then insert table queries to the output');
}
@@ -2000,7 +2003,7 @@ public function testDumpCreateTableAndThenInsert()
/**
* Tests interaction with the query builder
*/
- public function testQueryBuilder()
+ public function testQueryBuilder(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -2039,7 +2042,7 @@ public function testQueryBuilder()
$this->assertEquals(1, $stm->rowCount());
}
- public function testQueryWithParams()
+ public function testQueryWithParams(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -2074,7 +2077,7 @@ public function testQueryWithParams()
* Tests adding more than one column to a table
* that already exists due to adapters having different add column instructions
*/
- public function testAlterTableColumnAdd()
+ public function testAlterTableColumnAdd(): void
{
$table = new Table('table1', [], $this->adapter);
$table->create();
@@ -2099,14 +2102,14 @@ public function testAlterTableColumnAdd()
$columnCount = count($columns);
for ($i = 0; $i < $columnCount; $i++) {
- $this->assertSame($expected[$i]['name'], $columns[$i]->getName(), "Wrong name for {$expected[$i]['name']}");
- $this->assertSame($expected[$i]['type'], $columns[$i]->getType(), "Wrong type for {$expected[$i]['name']}");
- $this->assertSame($expected[$i]['default'], $columns[$i]->getDefault() instanceof Literal ? (string)$columns[$i]->getDefault() : $columns[$i]->getDefault(), "Wrong default for {$expected[$i]['name']}");
- $this->assertSame($expected[$i]['null'], $columns[$i]->getNull(), "Wrong null for {$expected[$i]['name']}");
+ $this->assertSame($expected[$i]['name'], $columns[$i]->getName(), 'Wrong name for ' . $expected[$i]['name']);
+ $this->assertSame($expected[$i]['type'], $columns[$i]->getType(), 'Wrong type for ' . $expected[$i]['name']);
+ $this->assertSame($expected[$i]['default'], $columns[$i]->getDefault() instanceof Literal ? (string)$columns[$i]->getDefault() : $columns[$i]->getDefault(), 'Wrong default for ' . $expected[$i]['name']);
+ $this->assertSame($expected[$i]['null'], $columns[$i]->getNull(), 'Wrong null for ' . $expected[$i]['name']);
}
}
- public function testAlterTableWithConstraints()
+ public function testAlterTableWithConstraints(): void
{
$table = new Table('table1', [], $this->adapter);
$table->create();
@@ -2135,10 +2138,10 @@ public function testAlterTableWithConstraints()
$columnCount = count($columns);
for ($i = 0; $i < $columnCount; $i++) {
- $this->assertSame($expected[$i]['name'], $columns[$i]->getName(), "Wrong name for {$expected[$i]['name']}");
- $this->assertSame($expected[$i]['type'], $columns[$i]->getType(), "Wrong type for {$expected[$i]['name']}");
- $this->assertSame($expected[$i]['default'], $columns[$i]->getDefault() instanceof Literal ? (string)$columns[$i]->getDefault() : $columns[$i]->getDefault(), "Wrong default for {$expected[$i]['name']}");
- $this->assertSame($expected[$i]['null'], $columns[$i]->getNull(), "Wrong null for {$expected[$i]['name']}");
+ $this->assertSame($expected[$i]['name'], $columns[$i]->getName(), 'Wrong name for ' . $expected[$i]['name']);
+ $this->assertSame($expected[$i]['type'], $columns[$i]->getType(), 'Wrong type for ' . $expected[$i]['name']);
+ $this->assertSame($expected[$i]['default'], $columns[$i]->getDefault() instanceof Literal ? (string)$columns[$i]->getDefault() : $columns[$i]->getDefault(), 'Wrong default for ' . $expected[$i]['name']);
+ $this->assertSame($expected[$i]['null'], $columns[$i]->getNull(), 'Wrong null for ' . $expected[$i]['name']);
}
}
@@ -2146,7 +2149,7 @@ public function testAlterTableWithConstraints()
* Tests that operations that trigger implicit table drops will not cause
* a foreign key constraint violation error.
*/
- public function testAlterTableDoesNotViolateRestrictedForeignKeyConstraint()
+ public function testAlterTableDoesNotViolateRestrictedForeignKeyConstraint(): void
{
$this->adapter->execute('PRAGMA foreign_keys = ON');
@@ -2213,7 +2216,7 @@ public function testAlterTableDoesNotViolateRestrictedForeignKeyConstraint()
* alteration process (being it implicitly by the process itself or by the user)
* will trigger an error accordingly.
*/
- public function testAlterTableDoesViolateForeignKeyConstraintOnTargetTableChange()
+ public function testAlterTableDoesViolateForeignKeyConstraintOnTargetTableChange(): void
{
$articlesTable = new Table('articles', [], $this->adapter);
$articlesTable
@@ -2249,7 +2252,7 @@ public function testAlterTableDoesViolateForeignKeyConstraintOnTargetTableChange
* alteration process (being it implicitly by the process itself or by the user)
* will trigger an error accordingly.
*/
- public function testAlterTableDoesViolateForeignKeyConstraintOnSourceTableChange()
+ public function testAlterTableDoesViolateForeignKeyConstraintOnSourceTableChange(): void
{
$adapter = $this
->getMockBuilder(SqliteAdapter::class)
@@ -2302,7 +2305,7 @@ public function testAlterTableDoesViolateForeignKeyConstraintOnSourceTableChange
* Tests that the adapter's foreign key validation does not apply when
* the `foreign_keys` pragma is set to `OFF`.
*/
- public function testAlterTableForeignKeyConstraintValidationNotRunningWithDisabledForeignKeys()
+ public function testAlterTableForeignKeyConstraintValidationNotRunningWithDisabledForeignKeys(): void
{
$articlesTable = new Table('articles', [], $this->adapter);
$articlesTable
@@ -2345,7 +2348,7 @@ public function testAlterTableForeignKeyConstraintValidationNotRunningWithDisabl
->update();
}
- public function testLiteralSupport()
+ public function testLiteralSupport(): void
{
$createQuery = <<<'INPUT'
CREATE TABLE `test` (`real_col` DECIMAL)
@@ -2358,23 +2361,23 @@ public function testLiteralSupport()
}
#[DataProvider('provideTableNamesForPresenceCheck')]
- public function testHasTable($createName, $tableName, $exp)
+ public function testHasTable(string $createName, string $tableName, bool $exp): void
{
// Test case for issue #1535
$conn = $this->adapter->getConnection();
- $conn->execute('ATTACH DATABASE \':memory:\' as etc');
+ $conn->execute("ATTACH DATABASE ':memory:' as etc");
$conn->execute('ATTACH DATABASE \':memory:\' as "main.db"');
$conn->execute(sprintf('DROP TABLE IF EXISTS %s', $createName));
$this->assertFalse($this->adapter->hasTable($tableName), sprintf('Adapter claims table %s exists when it does not', $tableName));
$conn->execute(sprintf('CREATE TABLE %s (a text)', $createName));
- if ($exp == true) {
+ if ($exp) {
$this->assertTrue($this->adapter->hasTable($tableName), sprintf('Adapter claims table %s does not exist when it does', $tableName));
} else {
$this->assertFalse($this->adapter->hasTable($tableName), sprintf('Adapter claims table %s exists when it does not', $tableName));
}
}
- public static function provideTableNamesForPresenceCheck()
+ public static function provideTableNamesForPresenceCheck(): array
{
return [
'Ordinary table' => ['t', 't', true],
@@ -2386,8 +2389,8 @@ public static function provideTableNamesForPresenceCheck()
'Wrong schema 1' => ['t', 'etc.t', false],
'Wrong schema 2' => ['t', 'temp.t', false],
'Missing schema' => ['t', 'not_attached.t', false],
- 'Malicious table' => ['"\'"', '\'', true],
- 'Malicious missing table' => ['t', '\'', false],
+ 'Malicious table' => ['"\'"', "'", true],
+ 'Malicious missing table' => ['t', "'", false],
'Table name case 1' => ['t', 'T', true],
'Table name case 2' => ['T', 't', true],
'Schema name case 1' => ['main.t', 'MAIN.t', true],
@@ -2424,10 +2427,10 @@ public function testHasTableAfterExecuteDrop(): void
}
#[DataProvider('provideIndexColumnsToCheck')]
- public function testHasIndex($tableDef, $cols, $exp)
+ public function testHasIndex(string $tableDef, string|array $cols, bool $exp): void
{
$conn = $this->adapter->getConnection();
- if (strpos($tableDef, ';') !== false) {
+ if (str_contains($tableDef, ';')) {
$queries = explode(';', $tableDef);
foreach ($queries as $query) {
$stmt = $conn->execute($query);
@@ -2441,7 +2444,7 @@ public function testHasIndex($tableDef, $cols, $exp)
$this->assertEquals($exp, $this->adapter->hasIndex('t', $cols));
}
- public static function provideIndexColumnsToCheck()
+ public static function provideIndexColumnsToCheck(): array
{
return [
['create table t(a text)', 'a', false],
@@ -2464,10 +2467,10 @@ public static function provideIndexColumnsToCheck()
}
#[DataProvider('provideIndexNamesToCheck')]
- public function testHasIndexByName($tableDef, $index, $exp)
+ public function testHasIndexByName(string $tableDef, string $index, bool $exp): void
{
$conn = $this->adapter->getConnection();
- if (strpos($tableDef, ';') !== false) {
+ if (str_contains($tableDef, ';')) {
$queries = explode(';', $tableDef);
foreach ($queries as $query) {
$stmt = $conn->execute($query);
@@ -2480,7 +2483,7 @@ public function testHasIndexByName($tableDef, $index, $exp)
$this->assertEquals($exp, $this->adapter->hasIndexByName('t', $index));
}
- public static function provideIndexNamesToCheck()
+ public static function provideIndexNamesToCheck(): array
{
return [
['create table t(a text)', 'test', false],
@@ -2496,11 +2499,11 @@ public static function provideIndexNamesToCheck()
}
#[DataProvider('providePrimaryKeysToCheck')]
- public function testHasPrimaryKey($tableDef, $key, $exp)
+ public function testHasPrimaryKey(string $tableDef, string|array $key, bool $exp): void
{
$this->assertFalse($this->adapter->hasTable('t'), 'Dirty test fixture');
$conn = $this->adapter->getConnection();
- if (strpos($tableDef, ';') !== false) {
+ if (str_contains($tableDef, ';')) {
$queries = explode(';', $tableDef);
foreach ($queries as $query) {
$stmt = $conn->execute($query);
@@ -2513,7 +2516,7 @@ public function testHasPrimaryKey($tableDef, $key, $exp)
$this->assertSame($exp, $this->adapter->hasPrimaryKey('t', $key));
}
- public static function providePrimaryKeysToCheck()
+ public static function providePrimaryKeysToCheck(): array
{
return [
['create table t(a integer)', 'a', false],
@@ -2525,7 +2528,7 @@ public static function providePrimaryKeysToCheck()
['create table t("a" integer PRIMARY KEY)', 'a', true],
['create table t([a] integer PRIMARY KEY)', 'a', true],
['create table t(`a` integer PRIMARY KEY)', 'a', true],
- ['create table t(\'a\' integer PRIMARY KEY)', 'a', true],
+ ["create table t('a' integer PRIMARY KEY)", 'a', true],
['create table t(`a.a` integer PRIMARY KEY)', 'a.a', true],
['create table t(a integer primary key)', ['a'], true],
['create table t(a integer primary key)', ['a', 'b'], false],
@@ -2555,7 +2558,7 @@ public static function providePrimaryKeysToCheck()
];
}
- public function testHasNamedPrimaryKey()
+ public function testHasNamedPrimaryKey(): void
{
$this->expectException(InvalidArgumentException::class);
@@ -2563,11 +2566,11 @@ public function testHasNamedPrimaryKey()
}
#[DataProvider('provideForeignKeysToCheck')]
- public function testHasForeignKey($tableDef, $key, $exp)
+ public function testHasForeignKey(string $tableDef, string|array $key, bool $exp): void
{
$conn = $this->adapter->getConnection();
$conn->execute('CREATE TABLE other(a integer, b integer, c integer)');
- if (strpos($tableDef, ';') !== false) {
+ if (str_contains($tableDef, ';')) {
$queries = explode(';', $tableDef);
foreach ($queries as $query) {
$stmt = $conn->execute($query);
@@ -2581,7 +2584,7 @@ public function testHasForeignKey($tableDef, $key, $exp)
$this->assertSame($exp, $this->adapter->hasForeignKey('t', $key));
}
- public static function provideForeignKeysToCheck()
+ public static function provideForeignKeysToCheck(): array
{
return [
['create table t(a integer)', 'a', false],
@@ -2723,13 +2726,13 @@ public static function hasNamedForeignKeyProvider(): array
}
#[DataProvider('provideColumnTypesForValidation')]
- public function testIsValidColumnType($phinxType, $exp)
+ public function testIsValidColumnType(string $phinxType, bool $exp): void
{
$col = (new Column())->setType($phinxType);
$this->assertSame($exp, $this->adapter->isValidColumnType($col));
}
- public static function provideColumnTypesForValidation()
+ public static function provideColumnTypesForValidation(): array
{
return [
[SqliteAdapter::TYPE_BIGINTEGER, true],
@@ -2748,6 +2751,7 @@ public static function provideColumnTypesForValidation()
[SqliteAdapter::TYPE_UUID, true],
[SqliteAdapter::TYPE_TIMESTAMP, true],
[SqliteAdapter::TYPE_CIDR, false],
+ [SqliteAdapter::TYPE_CITEXT, false],
[SqliteAdapter::TYPE_DECIMAL, true],
[SqliteAdapter::TYPE_GEOMETRY, false],
[SqliteAdapter::TYPE_INET, false],
@@ -2761,12 +2765,12 @@ public static function provideColumnTypesForValidation()
}
#[DataProvider('provideDatabaseVersionStrings')]
- public function testDatabaseVersionAtLeast($ver, $exp)
+ public function testDatabaseVersionAtLeast(string $ver, bool $exp): void
{
$this->assertSame($exp, $this->adapter->databaseVersionAtLeast($ver));
}
- public static function provideDatabaseVersionStrings()
+ public static function provideDatabaseVersionStrings(): array
{
return [
['2', true],
@@ -2780,10 +2784,10 @@ public static function provideDatabaseVersionStrings()
}
#[DataProvider('provideColumnNamesToCheck')]
- public function testHasColumn($tableDef, $col, $exp)
+ public function testHasColumn(string $tableDef, string $col, bool $exp): void
{
$conn = $this->adapter->getConnection();
- if (strpos($tableDef, ';') !== false) {
+ if (str_contains($tableDef, ';')) {
$queries = explode(';', $tableDef);
foreach ($queries as $query) {
$stmt = $conn->execute($query);
@@ -2797,7 +2801,7 @@ public function testHasColumn($tableDef, $col, $exp)
$this->assertEquals($exp, $this->adapter->hasColumn('t', $col));
}
- public static function provideColumnNamesToCheck()
+ public static function provideColumnNamesToCheck(): array
{
return [
['create table t(a text)', 'a', true],
@@ -2805,7 +2809,7 @@ public static function provideColumnNamesToCheck()
['create table t(A text)', 'A', true],
['create table t("a" text)', 'a', true],
['create table t([a] text)', 'a', true],
- ['create table t(\'a\' text)', 'a', true],
+ ["create table t('a' text)", 'a', true],
['create table t("A" text)', 'A', true],
['create table t(a text)', 'a', true],
['create table t(b text)', 'a', false],
@@ -2818,10 +2822,11 @@ public static function provideColumnNamesToCheck()
];
}
- public function testGetColumns()
+ public function testGetColumns(): void
{
$conn = $this->adapter->getConnection();
$conn->execute('create table t(a integer, b text, c char(5), d integer(12,6), e integer not null, f integer null)');
+
$exp = [
['name' => 'a', 'type' => 'integer', 'null' => true, 'limit' => null, 'precision' => null, 'scale' => null],
['name' => 'b', 'type' => 'text', 'null' => true, 'limit' => null, 'precision' => null, 'scale' => null],
@@ -2836,16 +2841,17 @@ public function testGetColumns()
$this->assertInstanceOf(Column::class, $act[$index]);
foreach ($data as $key => $value) {
$m = 'get' . ucfirst($key);
- $this->assertEquals($value, $act[$index]->$m(), "Parameter '$key' of column at index $index did not match expectations.");
+ $this->assertEquals($value, $act[$index]->$m(), sprintf("Parameter '%s' of column at index %s did not match expectations.", $key, $index));
}
}
}
#[DataProvider('provideIdentityCandidates')]
- public function testGetColumnsForIdentity($tableDef, $exp)
+ public function testGetColumnsForIdentity(string $tableDef, ?string $exp): void
{
$conn = $this->adapter->getConnection();
$conn->execute($tableDef);
+
$cols = $this->adapter->getColumns('t');
$act = [];
foreach ($cols as $col) {
@@ -2856,7 +2862,7 @@ public function testGetColumnsForIdentity($tableDef, $exp)
$this->assertEquals((array)$exp, $act);
}
- public static function provideIdentityCandidates()
+ public static function provideIdentityCandidates(): array
{
return [
['create table t(a text)', null],
@@ -2871,7 +2877,7 @@ public static function provideIdentityCandidates()
}
#[DataProvider('provideDefaultValues')]
- public function testGetColumnsForDefaults($tableDef, $exp)
+ public function testGetColumnsForDefaults(string $tableDef, string|Literal|int|float|Expression|null $exp): void
{
$conn = $this->adapter->getConnection();
$conn->execute($tableDef);
@@ -2884,7 +2890,7 @@ public function testGetColumnsForDefaults($tableDef, $exp)
}
}
- public static function provideDefaultValues()
+ public static function provideDefaultValues(): array
{
return [
'Implicit null' => ['create table t(a integer)', null],
@@ -2905,12 +2911,12 @@ public static function provideDefaultValues()
'Current timestamp LC' => ['create table t(a datetime default current_timestamp)', 'CURRENT_TIMESTAMP'],
'Current timestamp UC' => ['create table t(a datetime default CURRENT_TIMESTAMP)', 'CURRENT_TIMESTAMP'],
'Current timestamp MC' => ['create table t(a datetime default CURRENT_timestamp)', 'CURRENT_TIMESTAMP'],
- 'String 1' => ['create table t(a text default \'\')', Literal::from('')],
- 'String 2' => ['create table t(a text default \'value!\')', Literal::from('value!')],
- 'String 3' => ['create table t(a text default \'O\'\'Brien\')', Literal::from('O\'Brien')],
- 'String 4' => ['create table t(a text default \'CURRENT_TIMESTAMP\')', Literal::from('CURRENT_TIMESTAMP')],
- 'String 5' => ['create table t(a text default \'current_timestamp\')', Literal::from('current_timestamp')],
- 'String 6' => ['create table t(a text default \'\' /* comment */)', Literal::from('')],
+ 'String 1' => ["create table t(a text default '')", Literal::from('')],
+ 'String 2' => ["create table t(a text default 'value!')", Literal::from('value!')],
+ 'String 3' => ["create table t(a text default 'O''Brien')", Literal::from("O'Brien")],
+ 'String 4' => ["create table t(a text default 'CURRENT_TIMESTAMP')", Literal::from('CURRENT_TIMESTAMP')],
+ 'String 5' => ["create table t(a text default 'current_timestamp')", Literal::from('current_timestamp')],
+ 'String 6' => ["create table t(a text default '' /* comment */)", Literal::from('')],
'Hexadecimal LC' => ['create table t(a integer default 0xff)', 255],
'Hexadecimal UC' => ['create table t(a integer default 0XFF)', 255],
'Hexadecimal MC' => ['create table t(a integer default 0x1F)', 31],
@@ -2934,26 +2940,27 @@ public static function provideDefaultValues()
'Float 9' => ['create table t(a float default 1e+1)', 10.0],
'Float 10' => ['create table t(a float default 1e-1)', 0.1],
'Float 11' => ['create table t(a float default 1E-1)', 0.1],
- 'Blob literal 1' => ['create table t(a float default x\'ff\')', Expression::from('x\'ff\'')],
- 'Blob literal 2' => ['create table t(a float default X\'FF\')', Expression::from('X\'FF\'')],
+ 'Blob literal 1' => ["create table t(a float default x'ff')", Expression::from("x'ff'")],
+ 'Blob literal 2' => ["create table t(a float default X'FF')", Expression::from("X'FF'")],
'Arbitrary expression' => ['create table t(a float default ((2) + (2)))', Expression::from('(2) + (2)')],
- 'Pathological case 1' => ['create table t(a float default (\'/*\' || \'*/\'))', Expression::from('/*\' || \'*/')],
+ 'Pathological case 1' => ["create table t(a float default ('/*' || '*/'))", Expression::from("/*' || '*/")],
];
}
#[DataProvider('provideBooleanDefaultValues')]
- public function testGetColumnsForBooleanDefaults($tableDef, $exp)
+ public function testGetColumnsForBooleanDefaults(string $tableDef, int $exp): void
{
if (!$this->adapter->databaseVersionAtLeast('3.24')) {
$this->markTestSkipped('SQLite 3.24.0 or later is required for this test.');
}
$conn = $this->adapter->getConnection();
$conn->execute($tableDef);
+
$act = $this->adapter->getColumns('t')[0]->getDefault();
$this->assertSame($exp, $act);
}
- public static function provideBooleanDefaultValues()
+ public static function provideBooleanDefaultValues(): array
{
return [
'True LC' => ['create table t(a boolean default true)', 1],
@@ -2966,26 +2973,26 @@ public static function provideBooleanDefaultValues()
}
#[DataProvider('provideTablesForTruncation')]
- public function testTruncateTable($tableDef, $tableName, $tableId)
+ public function testTruncateTable(string $tableDef, string $tableName, string $tableId): void
{
$conn = $this->adapter->getConnection();
$conn->execute($tableDef);
- $conn->execute("INSERT INTO $tableId default values");
- $conn->execute("INSERT INTO $tableId default values");
- $conn->execute("INSERT INTO $tableId default values");
- $this->assertEquals(3, $conn->execute("select count(*) from $tableId")->fetchColumn(0), 'Broken fixture: data were not inserted properly');
- $this->assertEquals(3, $conn->execute("select max(id) from $tableId")->fetchColumn(0), 'Broken fixture: data were not inserted properly');
+ $conn->execute(sprintf('INSERT INTO %s default values', $tableId));
+ $conn->execute(sprintf('INSERT INTO %s default values', $tableId));
+ $conn->execute(sprintf('INSERT INTO %s default values', $tableId));
+ $this->assertEquals(3, $conn->execute('select count(*) from ' . $tableId)->fetchColumn(0), 'Broken fixture: data were not inserted properly');
+ $this->assertEquals(3, $conn->execute('select max(id) from ' . $tableId)->fetchColumn(0), 'Broken fixture: data were not inserted properly');
$this->adapter->truncateTable($tableName);
- $this->assertEquals(0, $conn->execute("select count(*) from $tableId")->fetchColumn(0), 'Table was not truncated');
- $conn->execute("INSERT INTO $tableId default values");
- $this->assertEquals(1, $conn->execute("select max(id) from $tableId")->fetchColumn(0), 'Autoincrement was not reset');
- $conn->execute("DROP TABLE $tableId");
+ $this->assertEquals(0, $conn->execute('select count(*) from ' . $tableId)->fetchColumn(0), 'Table was not truncated');
+ $conn->execute(sprintf('INSERT INTO %s default values', $tableId));
+ $this->assertEquals(1, $conn->execute('select max(id) from ' . $tableId)->fetchColumn(0), 'Autoincrement was not reset');
+ $conn->execute('DROP TABLE ' . $tableId);
}
/**
* @return array
*/
- public static function provideTablesForTruncation()
+ public static function provideTablesForTruncation(): array
{
return [
['create table t(id integer primary key)', 't', 't'],
@@ -3003,7 +3010,7 @@ public static function provideTablesForTruncation()
];
}
- public function testForeignKeyReferenceCorrectAfterRenameColumn()
+ public function testForeignKeyReferenceCorrectAfterRenameColumn(): void
{
$refTableColumnId = 'ref_table_id';
$refTableColumnToRename = 'columnToRename';
@@ -3019,7 +3026,7 @@ public function testForeignKeyReferenceCorrectAfterRenameColumn()
$refTable->renameColumn($refTableColumnToRename, $refTableRenamedColumn)->save();
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), [$refTableColumnId]));
- $this->assertFalse($this->adapter->hasTable("tmp_{$refTable->getName()}"));
+ $this->assertFalse($this->adapter->hasTable('tmp_' . $refTable->getName()));
$this->assertTrue($this->adapter->hasColumn($refTable->getName(), $refTableRenamedColumn));
$rows = $this->adapter->fetchAll('select * from sqlite_master where "type" = \'table\'');
@@ -3028,10 +3035,10 @@ public function testForeignKeyReferenceCorrectAfterRenameColumn()
$sql = $row['sql'];
}
}
- $this->assertStringContainsString("REFERENCES \"{$refTable->getName()}\" (\"id\")", $sql);
+ $this->assertStringContainsString(sprintf('REFERENCES "%s" ("id")', $refTable->getName()), $sql);
}
- public function testForeignKeyReferenceCorrectAfterChangeColumn()
+ public function testForeignKeyReferenceCorrectAfterChangeColumn(): void
{
$refTableColumnId = 'ref_table_id';
$refTableColumnToChange = 'columnToChange';
@@ -3046,7 +3053,7 @@ public function testForeignKeyReferenceCorrectAfterChangeColumn()
$refTable->changeColumn($refTableColumnToChange, 'text')->save();
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), [$refTableColumnId]));
- $this->assertFalse($this->adapter->hasTable("tmp_{$refTable->getName()}"));
+ $this->assertFalse($this->adapter->hasTable('tmp_' . $refTable->getName()));
$this->assertEquals('text', $this->adapter->getColumns($refTable->getName())[1]->getType());
$rows = $this->adapter->fetchAll('select * from sqlite_master where "type" = \'table\'');
@@ -3055,10 +3062,10 @@ public function testForeignKeyReferenceCorrectAfterChangeColumn()
$sql = $row['sql'];
}
}
- $this->assertStringContainsString("REFERENCES \"{$refTable->getName()}\" (\"id\")", $sql);
+ $this->assertStringContainsString(sprintf('REFERENCES "%s" ("id")', $refTable->getName()), $sql);
}
- public function testForeignKeyReferenceCorrectAfterRemoveColumn()
+ public function testForeignKeyReferenceCorrectAfterRemoveColumn(): void
{
$refTableColumnId = 'ref_table_id';
$refTableColumnToRemove = 'columnToRemove';
@@ -3073,7 +3080,7 @@ public function testForeignKeyReferenceCorrectAfterRemoveColumn()
$refTable->removeColumn($refTableColumnToRemove)->save();
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), [$refTableColumnId]));
- $this->assertFalse($this->adapter->hasTable("tmp_{$refTable->getName()}"));
+ $this->assertFalse($this->adapter->hasTable('tmp_' . $refTable->getName()));
$this->assertFalse($this->adapter->hasColumn($refTable->getName(), $refTableColumnToRemove));
$rows = $this->adapter->fetchAll('select * from sqlite_master where "type" = \'table\'');
@@ -3082,10 +3089,10 @@ public function testForeignKeyReferenceCorrectAfterRemoveColumn()
$sql = $row['sql'];
}
}
- $this->assertStringContainsString("REFERENCES \"{$refTable->getName()}\" (\"id\")", $sql);
+ $this->assertStringContainsString(sprintf('REFERENCES "%s" ("id")', $refTable->getName()), $sql);
}
- public function testForeignKeyReferenceCorrectAfterChangePrimaryKey()
+ public function testForeignKeyReferenceCorrectAfterChangePrimaryKey(): void
{
$refTableColumnAdditionalId = 'additional_id';
$refTableColumnId = 'ref_table_id';
@@ -3103,7 +3110,7 @@ public function testForeignKeyReferenceCorrectAfterChangePrimaryKey()
->save();
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), [$refTableColumnId]));
- $this->assertFalse($this->adapter->hasTable("tmp_{$refTable->getName()}"));
+ $this->assertFalse($this->adapter->hasTable('tmp_' . $refTable->getName()));
$this->assertTrue($this->adapter->getColumns($refTable->getName())[1]->getIdentity());
$rows = $this->adapter->fetchAll('select * from sqlite_master where "type" = \'table\'');
@@ -3112,10 +3119,10 @@ public function testForeignKeyReferenceCorrectAfterChangePrimaryKey()
$sql = $row['sql'];
}
}
- $this->assertStringContainsString("REFERENCES \"{$refTable->getName()}\" (\"id\")", $sql);
+ $this->assertStringContainsString(sprintf('REFERENCES "%s" ("id")', $refTable->getName()), $sql);
}
- public function testForeignKeyReferenceCorrectAfterDropForeignKey()
+ public function testForeignKeyReferenceCorrectAfterDropForeignKey(): void
{
$refTableAdditionalColumnId = 'ref_table_additional_id';
$refTableAdditional = new Table('ref_table_additional', [], $this->adapter);
@@ -3135,7 +3142,7 @@ public function testForeignKeyReferenceCorrectAfterDropForeignKey()
$refTable->dropForeignKey($refTableAdditionalColumnId)->save();
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), [$refTableColumnId]));
- $this->assertFalse($this->adapter->hasTable("tmp_{$refTable->getName()}"));
+ $this->assertFalse($this->adapter->hasTable('tmp_' . $refTable->getName()));
$this->assertFalse($this->adapter->hasForeignKey($refTable->getName(), [$refTableAdditionalColumnId]));
$rows = $this->adapter->fetchAll('select * from sqlite_master where "type" = \'table\'');
@@ -3144,17 +3151,17 @@ public function testForeignKeyReferenceCorrectAfterDropForeignKey()
$sql = $row['sql'];
}
}
- $this->assertStringContainsString("REFERENCES \"{$refTable->getName()}\" (\"id\")", $sql);
+ $this->assertStringContainsString(sprintf('REFERENCES "%s" ("id")', $refTable->getName()), $sql);
}
- public function testPdoExceptionUpdateNonExistingTable()
+ public function testPdoExceptionUpdateNonExistingTable(): void
{
$this->expectException(PDOException::class);
$table = new Table('non_existing_table', [], $this->adapter);
$table->addColumn('column', 'string')->update();
}
- public function testAddCheckConstraint()
+ public function testAddCheckConstraint(): void
{
$table = new Table('check_table', [], $this->adapter);
$table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2])
@@ -3166,7 +3173,22 @@ public function testAddCheckConstraint()
$this->assertTrue($this->adapter->hasCheckConstraint('check_table', 'price_positive'));
}
- public function testHasCheckConstraint()
+ public function testAddCheckConstraintAutoGeneratesName(): void
+ {
+ $table = new Table('check_table_auto', [], $this->adapter);
+ $table->addColumn('quantity', 'integer')
+ ->create();
+
+ $expression = 'quantity >= 0';
+ // An empty name must trigger auto-generation, not produce an unnamed constraint.
+ $checkConstraint = new CheckConstraint('', $expression);
+ $this->adapter->addCheckConstraint($table->getTable(), $checkConstraint);
+
+ $expectedName = 'chk_' . substr(md5($expression), 0, 8);
+ $this->assertTrue($this->adapter->hasCheckConstraint('check_table_auto', $expectedName));
+ }
+
+ public function testHasCheckConstraint(): void
{
$table = new Table('check_table3', [], $this->adapter);
$table->addColumn('quantity', 'integer')
@@ -3180,7 +3202,7 @@ public function testHasCheckConstraint()
$this->assertTrue($this->adapter->hasCheckConstraint('check_table3', 'quantity_positive'));
}
- public function testDropCheckConstraint()
+ public function testDropCheckConstraint(): void
{
$table = new Table('check_table4', [], $this->adapter);
$table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2])
@@ -3194,7 +3216,7 @@ public function testDropCheckConstraint()
$this->assertFalse($this->adapter->hasCheckConstraint('check_table4', 'price_check'));
}
- public function testCheckConstraintWithComplexExpression()
+ public function testCheckConstraintWithComplexExpression(): void
{
$table = new Table('check_table5', [], $this->adapter);
$table->addColumn('email', 'string', ['limit' => 255])
@@ -3211,10 +3233,10 @@ public function testCheckConstraintWithComplexExpression()
// Verify the constraint is actually enforced
$quotedTableName = $this->adapter->getConnection()->getDriver()->quoteIdentifier('check_table5');
$this->expectException(PDOException::class);
- $this->adapter->execute("INSERT INTO {$quotedTableName} (email, status) VALUES ('test@example.com', 'invalid')");
+ $this->adapter->execute(sprintf("INSERT INTO %s (email, status) VALUES ('test@example.com', 'invalid')", $quotedTableName));
}
- public function testInsertOrSkipWithDuplicates()
+ public function testInsertOrSkipWithDuplicates(): void
{
$table = new Table('users', [], $this->adapter);
$table->addColumn('email', 'string', ['limit' => 255])
@@ -3237,7 +3259,7 @@ public function testInsertOrSkipWithDuplicates()
$this->assertEquals('John', $rows[0]['name']);
}
- public function testInsertModeResetsAfterInsertOrSkip()
+ public function testInsertModeResetsAfterInsertOrSkip(): void
{
$table = new Table('users', [], $this->adapter);
$table->addColumn('email', 'string', ['limit' => 255])
@@ -3257,7 +3279,7 @@ public function testInsertModeResetsAfterInsertOrSkip()
])->save();
}
- public function testBulkinsertOrSkipWithDuplicates()
+ public function testBulkinsertOrSkipWithDuplicates(): void
{
$table = new Table('products', [], $this->adapter);
$table->addColumn('sku', 'string', ['limit' => 50])
@@ -3284,7 +3306,7 @@ public function testBulkinsertOrSkipWithDuplicates()
$this->assertEquals('30.00', $rows[2]['price']);
}
- public function testInsertOrSkipWithoutDuplicates()
+ public function testInsertOrSkipWithoutDuplicates(): void
{
$table = new Table('categories', [], $this->adapter);
$table->addColumn('name', 'string')
@@ -3300,7 +3322,7 @@ public function testInsertOrSkipWithoutDuplicates()
$this->assertCount(2, $rows);
}
- public function testInsertOrUpdateWithDuplicates()
+ public function testInsertOrUpdateWithDuplicates(): void
{
$table = new Table('currencies', [], $this->adapter);
$table->addColumn('code', 'string', ['limit' => 3])
@@ -3333,7 +3355,7 @@ public function testInsertOrUpdateWithDuplicates()
$this->assertEquals('1.0500', $rows[2]['rate']); // USD updated
}
- public function testInsertOrUpdateWithMultipleUpdateColumns()
+ public function testInsertOrUpdateWithMultipleUpdateColumns(): void
{
$table = new Table('products', [], $this->adapter);
$table->addColumn('sku', 'string', ['limit' => 50])
@@ -3358,7 +3380,7 @@ public function testInsertOrUpdateWithMultipleUpdateColumns()
$this->assertEquals(50, $rows[0]['stock']);
}
- public function testInsertOrUpdateModeResetsAfterSave()
+ public function testInsertOrUpdateModeResetsAfterSave(): void
{
$table = new Table('items', [], $this->adapter);
$table->addColumn('code', 'string', ['limit' => 10])
@@ -3378,7 +3400,7 @@ public function testInsertOrUpdateModeResetsAfterSave()
])->save();
}
- public function testInsertOrUpdateRequiresConflictColumns()
+ public function testInsertOrUpdateRequiresConflictColumns(): void
{
$table = new Table('currencies', [], $this->adapter);
$table->addColumn('code', 'string', ['limit' => 3])
diff --git a/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php b/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php
index f494d45d2..91dc7d0ae 100644
--- a/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php
+++ b/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php
@@ -22,13 +22,12 @@
class SqlserverAdapterTest extends TestCase
{
- /**
- * @var \Migrations\Db\Adapter\SqlServerAdapter
- */
- private $adapter;
+ private SqlserverAdapter $adapter;
private array $config;
+
private StubConsoleOutput $out;
+
private ConsoleIo $io;
protected function setUp(): void
@@ -73,12 +72,12 @@ protected function getConsoleIo(): ConsoleIo
return $this->io;
}
- public function testConnection()
+ public function testConnection(): void
{
$this->assertInstanceOf(Connection::class, $this->adapter->getConnection());
}
- public function testCreatingTheSchemaTableOnConnect()
+ public function testCreatingTheSchemaTableOnConnect(): void
{
$this->adapter->connect();
$this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName()));
@@ -89,25 +88,25 @@ public function testCreatingTheSchemaTableOnConnect()
$this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName()));
}
- public function testSchemaTableIsCreatedWithPrimaryKey()
+ public function testSchemaTableIsCreatedWithPrimaryKey(): void
{
$this->adapter->connect();
new Table($this->adapter->getSchemaTableName(), [], $this->adapter);
$this->assertTrue($this->adapter->hasIndex($this->adapter->getSchemaTableName(), ['version']));
}
- public function testQuoteTableName()
+ public function testQuoteTableName(): void
{
$this->assertEquals('[dbo].[test_table]', $this->adapter->quoteTableName('test_table'));
$this->assertEquals('[schema].[table]', $this->adapter->quoteTableName('schema.table'));
}
- public function testQuoteColumnName()
+ public function testQuoteColumnName(): void
{
$this->assertEquals('[test_column]', $this->adapter->quoteColumnName('test_column'));
}
- public function testCreateTable()
+ public function testCreateTable(): void
{
$table = new Table('ntable', [], $this->adapter);
$table->addColumn('realname', 'string')
@@ -120,7 +119,7 @@ public function testCreateTable()
$this->assertFalse($this->adapter->hasColumn('ntable', 'address'));
}
- public function testCreateTableWithSchema()
+ public function testCreateTableWithSchema(): void
{
$this->adapter->createSchema('nschema');
@@ -138,7 +137,7 @@ public function testCreateTableWithSchema()
$this->adapter->dropSchema('nschema');
}
- public function testCreateTableCustomIdColumn()
+ public function testCreateTableCustomIdColumn(): void
{
$table = new Table('ntable', ['id' => 'custom_id'], $this->adapter);
$table->addColumn('realname', 'string')
@@ -151,7 +150,7 @@ public function testCreateTableCustomIdColumn()
$this->assertFalse($this->adapter->hasColumn('ntable', 'address'));
}
- public function testCreateTableIdentityColumn()
+ public function testCreateTableIdentityColumn(): void
{
$table = new Table('ntable', ['id' => false, 'primary_key' => 'id'], $this->adapter);
$table->addColumn('id', 'integer', ['identity' => true])
@@ -160,7 +159,7 @@ public function testCreateTableIdentityColumn()
$this->assertTrue($this->adapter->hasColumn('ntable', 'id'));
}
- public function testCreateTableWithNoPrimaryKey()
+ public function testCreateTableWithNoPrimaryKey(): void
{
$options = [
'id' => false,
@@ -171,7 +170,7 @@ public function testCreateTableWithNoPrimaryKey()
$this->assertFalse($this->adapter->hasColumn('atable', 'id'));
}
- public function testCreateTableWithConflictingPrimaryKeys()
+ public function testCreateTableWithConflictingPrimaryKeys(): void
{
$options = [
'primary_key' => 'user_id',
@@ -183,7 +182,7 @@ public function testCreateTableWithConflictingPrimaryKeys()
$table->addColumn('user_id', 'integer')->save();
}
- public function testCreateTableWithPrimaryKeySetToImplicitId()
+ public function testCreateTableWithPrimaryKeySetToImplicitId(): void
{
$options = [
'primary_key' => 'id',
@@ -195,7 +194,7 @@ public function testCreateTableWithPrimaryKeySetToImplicitId()
$this->assertTrue($this->adapter->hasColumn('ztable', 'user_id'));
}
- public function testCreateTableWithPrimaryKeyArraySetToImplicitId()
+ public function testCreateTableWithPrimaryKeyArraySetToImplicitId(): void
{
$options = [
'primary_key' => ['id'],
@@ -207,7 +206,7 @@ public function testCreateTableWithPrimaryKeyArraySetToImplicitId()
$this->assertTrue($this->adapter->hasColumn('ztable', 'user_id'));
}
- public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId()
+ public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId(): void
{
$options = [
'primary_key' => ['id', 'user_id'],
@@ -218,7 +217,7 @@ public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId()
$table->addColumn('user_id', 'integer')->save();
}
- public function testCreateTableWithMultiplePrimaryKeys()
+ public function testCreateTableWithMultiplePrimaryKeys(): void
{
$options = [
'id' => false,
@@ -233,7 +232,7 @@ public function testCreateTableWithMultiplePrimaryKeys()
$this->assertFalse($this->adapter->hasIndex('table1', ['tag_id', 'user_email']));
}
- public function testCreateTableWithPrimaryKeyAsUuid()
+ public function testCreateTableWithPrimaryKeyAsUuid(): void
{
$options = [
'id' => false,
@@ -247,7 +246,7 @@ public function testCreateTableWithPrimaryKeyAsUuid()
$this->assertTrue($this->adapter->hasColumn('ztable', 'user_id'));
}
- public function testCreateTableWithPrimaryKeyAsBinaryUuid()
+ public function testCreateTableWithPrimaryKeyAsBinaryUuid(): void
{
$options = [
'id' => false,
@@ -261,7 +260,7 @@ public function testCreateTableWithPrimaryKeyAsBinaryUuid()
$this->assertTrue($this->adapter->hasColumn('ztable', 'user_id'));
}
- public function testCreateTableWithMultipleIndexes()
+ public function testCreateTableWithMultipleIndexes(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -275,7 +274,7 @@ public function testCreateTableWithMultipleIndexes()
$this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_name']));
}
- public function testCreateTableWithUniqueIndexes()
+ public function testCreateTableWithUniqueIndexes(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -285,7 +284,7 @@ public function testCreateTableWithUniqueIndexes()
$this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_email']));
}
- public function testCreateTableWithNamedIndexes()
+ public function testCreateTableWithNamedIndexes(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -319,7 +318,7 @@ public function testCreateTableIndexWithWhere(): void
$this->assertStringContainsString('([email]) WHERE is_verified = true', $indexQuery);
}
- public function testAddPrimaryKey()
+ public function testAddPrimaryKey(): void
{
$table = new Table('table1', ['id' => false], $this->adapter);
$table
@@ -333,7 +332,7 @@ public function testAddPrimaryKey()
$this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column1']));
}
- public function testChangePrimaryKey()
+ public function testChangePrimaryKey(): void
{
$table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter);
$table
@@ -350,7 +349,7 @@ public function testChangePrimaryKey()
$this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column2', 'column3']));
}
- public function testDropPrimaryKey()
+ public function testDropPrimaryKey(): void
{
$table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter);
$table
@@ -364,7 +363,7 @@ public function testDropPrimaryKey()
$this->assertFalse($this->adapter->hasPrimaryKey('table1', ['column1']));
}
- public function testHasPrimaryKeyMultipleColumns()
+ public function testHasPrimaryKeyMultipleColumns(): void
{
$table = new Table('table1', ['id' => false, 'primary_key' => ['column1', 'column2', 'column3']], $this->adapter);
$table
@@ -378,7 +377,7 @@ public function testHasPrimaryKeyMultipleColumns()
$this->assertFalse($table->hasPrimaryKey(['column1', 'column2', 'column3', 'column4']));
}
- public function testHasPrimaryKeyCaseSensitivity()
+ public function testHasPrimaryKeyCaseSensitivity(): void
{
$table = new Table('table', ['id' => false, 'primary_key' => ['column1']], $this->adapter);
$table
@@ -389,7 +388,7 @@ public function testHasPrimaryKeyCaseSensitivity()
$this->assertFalse($table->hasPrimaryKey('cOlUmN1'));
}
- public function testChangeCommentFails()
+ public function testChangeCommentFails(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -401,7 +400,7 @@ public function testChangeCommentFails()
->save();
}
- public function testRenameTable()
+ public function testRenameTable(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -412,7 +411,7 @@ public function testRenameTable()
$this->assertTrue($this->adapter->hasTable('table2'));
}
- public function testAddColumn()
+ public function testAddColumn(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -422,7 +421,7 @@ public function testAddColumn()
$this->assertTrue($table->hasColumn('email'));
}
- public function testAddColumnWithDefaultValue()
+ public function testAddColumnWithDefaultValue(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -436,7 +435,7 @@ public function testAddColumnWithDefaultValue()
}
}
- public function testAddColumnWithDefaultZero()
+ public function testAddColumnWithDefaultZero(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -451,7 +450,7 @@ public function testAddColumnWithDefaultZero()
}
}
- public function testAddColumnWithDefaultNull()
+ public function testAddColumnWithDefaultNull(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -465,7 +464,7 @@ public function testAddColumnWithDefaultNull()
}
}
- public function testAddColumnWithNotNullableNoDefault()
+ public function testAddColumnWithNotNullableNoDefault(): void
{
$table = new Table('table1', [], $this->adapter);
$table
@@ -480,7 +479,7 @@ public function testAddColumnWithNotNullableNoDefault()
$this->assertNull($columns[1]->getDefault());
}
- public function testAddColumnWithDefaultBool()
+ public function testAddColumnWithDefaultBool(): void
{
$table = new Table('table1', [], $this->adapter);
$table->save();
@@ -499,7 +498,7 @@ public function testAddColumnWithDefaultBool()
}
}
- public function testRenameColumn()
+ public function testRenameColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -511,7 +510,7 @@ public function testRenameColumn()
$this->assertTrue($this->adapter->hasColumn('t', 'column2'));
}
- public function testRenamingANonExistentColumn()
+ public function testRenamingANonExistentColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -524,13 +523,13 @@ public function testRenamingANonExistentColumn()
$this->assertInstanceOf(
'InvalidArgumentException',
$e,
- 'Expected exception of type InvalidArgumentException, got ' . get_class($e),
+ 'Expected exception of type InvalidArgumentException, got ' . $e::class,
);
$this->assertEquals('The specified column does not exist: column2', $e->getMessage());
}
}
- public function testChangeColumnType()
+ public function testChangeColumnType(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -549,7 +548,7 @@ public function testChangeColumnType()
}
}
- public function testChangeColumnNameAndNull()
+ public function testChangeColumnNameAndNull(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['null' => false])
@@ -569,7 +568,7 @@ public function testChangeColumnNameAndNull()
}
}
- public function testChangeColumnDefaults()
+ public function testChangeColumnDefaults(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['default' => 'test'])
@@ -591,7 +590,7 @@ public function testChangeColumnDefaults()
$this->assertSame('another test', $columns[1]->getDefault());
}
- public function testChangeColumnDefaultToNull()
+ public function testChangeColumnDefaultToNull(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string', ['null' => true, 'default' => 'test'])
@@ -606,7 +605,7 @@ public function testChangeColumnDefaultToNull()
$this->assertNull($columns[1]->getDefault());
}
- public function testChangeColumnDefaultToZero()
+ public function testChangeColumnDefaultToZero(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'integer')
@@ -621,7 +620,7 @@ public function testChangeColumnDefaultToZero()
$this->assertSame(0, $columns[1]->getDefault());
}
- public function testDropColumn()
+ public function testDropColumn(): void
{
$table = new Table('t', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -631,7 +630,7 @@ public function testDropColumn()
$this->assertFalse($this->adapter->hasColumn('t', 'column1'));
}
- public static function columnsProvider()
+ public static function columnsProvider(): array
{
return [
['column1', 'string', ['null' => true, 'default' => null]],
@@ -655,7 +654,7 @@ public static function columnsProvider()
}
#[DataProvider('columnsProvider')]
- public function testGetColumns($colName, $type, $options, $actualType = null)
+ public function testGetColumns(string $colName, string $type, array $options, ?string $actualType = null): void
{
$table = new Table('t', [], $this->adapter);
$table
@@ -671,7 +670,7 @@ public function testGetColumns($colName, $type, $options, $actualType = null)
$this->assertEquals($actualType ?? $type, $columns[1]->getType());
}
- public function testAddIndex()
+ public function testAddIndex(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -682,7 +681,7 @@ public function testAddIndex()
$this->assertTrue($table->hasIndex('email'));
}
- public function testAddIndexWithSort()
+ public function testAddIndexWithSort(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -710,7 +709,7 @@ public function testAddIndexWithSort()
$this->assertEquals($emailOrder['sort_order'], 'ASC');
}
- public function testAddIndexWithIncludeColumns()
+ public function testAddIndexWithIncludeColumns(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('email', 'string')
@@ -747,7 +746,7 @@ public function testAddIndexWithIncludeColumns()
$this->assertEquals($emailOrder['included'], 1);
}
- public function testGetIndexes()
+ public function testGetIndexes(): void
{
// single column index
$table = new Table('table1', [], $this->adapter);
@@ -771,7 +770,7 @@ public function testGetIndexes()
$this->assertEquals(['email', 'username'], $indexes[2]['columns']);
}
- public function testDropIndex()
+ public function testDropIndex(): void
{
// single column index
$table = new Table('table1', [], $this->adapter);
@@ -812,7 +811,7 @@ public function testDropIndex()
$this->assertFalse($table4->hasIndex(['fname', 'lname']));
}
- public function testDropIndexByName()
+ public function testDropIndexByName(): void
{
// single column index
$table = new Table('table1', [], $this->adapter);
@@ -838,7 +837,7 @@ public function testDropIndexByName()
$this->assertFalse($table2->hasIndex(['fname', 'lname']));
}
- public function testAddForeignKey()
+ public function testAddForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -856,7 +855,7 @@ public function testAddForeignKey()
$this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'], 'fk1'));
}
- public function testDropForeignKey()
+ public function testDropForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->addColumn('field1', 'string')->save();
@@ -872,7 +871,7 @@ public function testDropForeignKey()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id']));
}
- public function testDropForeignKeyWithMultipleColumns()
+ public function testDropForeignKeyWithMultipleColumns(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable
@@ -922,7 +921,7 @@ public function testDropForeignKeyWithMultipleColumns()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_field1', 'ref_table_id']));
}
- public function testDropForeignKeyWithIdenticalMultipleColumns()
+ public function testDropForeignKeyWithIdenticalMultipleColumns(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable
@@ -969,11 +968,8 @@ public static function nonExistentForeignKeyColumnsProvider(): array
];
}
- /**
- * @param array $columns
- */
#[DataProvider('nonExistentForeignKeyColumnsProvider')]
- public function testDropForeignKeyByNonExistentKeyColumns(array $columns)
+ public function testDropForeignKeyByNonExistentKeyColumns(array $columns): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable
@@ -1001,7 +997,7 @@ public function testDropForeignKeyByNonExistentKeyColumns(array $columns)
$this->adapter->dropForeignKey($table->getName(), $columns);
}
- public function testDropForeignKeyCaseSensitivity()
+ public function testDropForeignKeyCaseSensitivity(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->save();
@@ -1023,7 +1019,7 @@ public function testDropForeignKeyCaseSensitivity()
$this->adapter->dropForeignKey($table->getName(), ['ref_table_id']);
}
- public function testDropForeignKeyByName()
+ public function testDropForeignKeyByName(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->save();
@@ -1044,7 +1040,7 @@ public function testDropForeignKeyByName()
}
#[DataProvider('provideForeignKeysToCheck')]
- public function testHasForeignKey($tableDef, $key, $exp)
+ public function testHasForeignKey(string $tableDef, string|array $key, bool $exp): void
{
$conn = $this->adapter->getConnection();
$conn->execute('CREATE TABLE other(a int, b int, c int, unique(a), unique(b), unique(a,b), unique(a,b,c));');
@@ -1052,7 +1048,7 @@ public function testHasForeignKey($tableDef, $key, $exp)
$this->assertSame($exp, $this->adapter->hasForeignKey('t', $key));
}
- public static function provideForeignKeysToCheck()
+ public static function provideForeignKeysToCheck(): array
{
return [
['create table t(a int)', 'a', false],
@@ -1079,7 +1075,7 @@ public static function provideForeignKeysToCheck()
];
}
- public function testHasNamedForeignKey()
+ public function testHasNamedForeignKey(): void
{
$refTable = new Table('ref_table', [], $this->adapter);
$refTable->save();
@@ -1102,13 +1098,13 @@ public function testHasNamedForeignKey()
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), [], 'my_constraint2'));
}
- public function testHasDatabase()
+ public function testHasDatabase(): void
{
$this->assertFalse($this->adapter->hasDatabase('fake_database_name'));
$this->assertTrue($this->adapter->hasDatabase($this->config['database']));
}
- public function testDropDatabase()
+ public function testDropDatabase(): void
{
$this->assertFalse($this->adapter->hasDatabase('phinx_temp_database'));
$this->adapter->createDatabase('phinx_temp_database');
@@ -1116,13 +1112,13 @@ public function testDropDatabase()
$this->adapter->dropDatabase('phinx_temp_database');
}
- public function testCreateSchema()
+ public function testCreateSchema(): void
{
$this->adapter->createSchema('foo');
$this->assertTrue($this->adapter->hasSchema('foo'));
}
- public function testDropSchema()
+ public function testDropSchema(): void
{
$this->adapter->createSchema('foo');
$this->assertTrue($this->adapter->hasSchema('foo'));
@@ -1130,7 +1126,7 @@ public function testDropSchema()
$this->assertFalse($this->adapter->hasSchema('foo'));
}
- public function testDropAllSchemas()
+ public function testDropAllSchemas(): void
{
$this->adapter->createSchema('foo');
$this->adapter->createSchema('bar');
@@ -1142,14 +1138,14 @@ public function testDropAllSchemas()
$this->assertFalse($this->adapter->hasSchema('bar'));
}
- public function testQuoteSchemaName()
+ public function testQuoteSchemaName(): void
{
$this->assertEquals('[schema]', $this->adapter->quoteSchemaName('schema'));
// Dotted schema names are not supported
$this->assertEquals('[schema].[schema]', $this->adapter->quoteSchemaName('schema.schema'));
}
- public function testAddColumnComment()
+ public function testAddColumnComment(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('field1', 'string', ['comment' => $comment = 'Comments from column "field1"'])
@@ -1161,7 +1157,7 @@ public function testAddColumnComment()
}
#[Depends('testAddColumnComment')]
- public function testChangeColumnComment()
+ public function testChangeColumnComment(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('field1', 'string', ['comment' => 'Comments from column "field1"'])
@@ -1176,7 +1172,7 @@ public function testChangeColumnComment()
}
#[Depends('testAddColumnComment')]
- public function testRemoveColumnComment()
+ public function testRemoveColumnComment(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('field1', 'string', ['comment' => 'Comments from column "field1"'])
@@ -1193,7 +1189,7 @@ public function testRemoveColumnComment()
/**
* Test that column names are properly escaped when creating Foreign Keys
*/
- public function testForignKeysArePropertlyEscaped()
+ public function testForignKeysArePropertlyEscaped(): void
{
$userId = 'user';
$sessionId = 'session';
@@ -1209,7 +1205,7 @@ public function testForignKeysArePropertlyEscaped()
$this->assertTrue($foreign->hasForeignKey('user'));
}
- public function testBulkInsertData()
+ public function testBulkInsertData(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -1245,7 +1241,7 @@ public function testBulkInsertData()
$this->assertEquals(3, $rows[2]['column2']);
}
- public function testBulkInsertLiteral()
+ public function testBulkInsertLiteral(): void
{
$data = [
[
@@ -1271,12 +1267,12 @@ public function testBulkInsertLiteral()
$this->assertEquals('value1', $rows[0]['column1']);
$this->assertEquals('value2', $rows[1]['column1']);
$this->assertEquals('value3', $rows[2]['column1']);
- $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']);
+ $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column2']);
$this->assertEquals('2024-01-01 00:00:00.0000000', $rows[1]['column2']);
$this->assertEquals('2025-01-01 00:00:00.0000000', $rows[2]['column2']);
}
- public function testInsertData()
+ public function testInsertData(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -1309,7 +1305,7 @@ public function testInsertData()
$this->assertEquals(3, $rows[2]['column2']);
}
- public function testInsertLiteral()
+ public function testInsertLiteral(): void
{
$data = [
[
@@ -1340,12 +1336,12 @@ public function testInsertLiteral()
$this->assertEquals('test', $rows[0]['column2']);
$this->assertEquals('test', $rows[1]['column2']);
$this->assertEquals('foo', $rows[2]['column2']);
- $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']);
+ $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column3']);
$this->assertEquals('2024-01-01 00:00:00.0000000', $rows[1]['column3']);
$this->assertEquals('2025-01-01 00:00:00.0000000', $rows[2]['column3']);
}
- public function testTruncateTable()
+ public function testTruncateTable(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('column1', 'string')
@@ -1369,7 +1365,7 @@ public function testTruncateTable()
$this->assertCount(0, $rows);
}
- public function testDumpCreateTableAndThenInsert()
+ public function testDumpCreateTableAndThenInsert(): void
{
$options = $this->adapter->getOptions();
$options['dryrun'] = true;
@@ -1393,7 +1389,7 @@ public function testDumpCreateTableAndThenInsert()
CREATE TABLE [dbo].[table1] ([column1] NVARCHAR(255) NOT NULL, [column2] INTEGER DEFAULT NULL, CONSTRAINT PK_table1 PRIMARY KEY ([column1]));
INSERT INTO [dbo].[table1] ([column1], [column2]) VALUES ('id1', 1);
OUTPUT;
- $output = join("\n", $this->out->messages());
+ $output = implode("\n", $this->out->messages());
$actualOutput = str_replace("\r\n", "\n", $output);
$this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option does not dump create and then insert table queries to the output');
}
@@ -1401,7 +1397,7 @@ public function testDumpCreateTableAndThenInsert()
/**
* Tests interaction with the query builder
*/
- public function testQueryBuilder()
+ public function testQueryBuilder(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -1442,7 +1438,7 @@ public function testQueryBuilder()
$stm->closeCursor();
}
- public function testQueryWithParams()
+ public function testQueryWithParams(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('string_col', 'string')
@@ -1473,7 +1469,7 @@ public function testQueryWithParams()
$this->assertEquals(3, $res[0]['c']);
}
- public function testIdentityInsert()
+ public function testIdentityInsert(): void
{
$table = new Table('table1', [], $this->adapter);
$table->addColumn('name', 'string')
@@ -1500,7 +1496,7 @@ public function testIdentityInsert()
$this->assertEquals(50, $res[1]['id']);
}
- public function testInsertOrSkipThrowsException()
+ public function testInsertOrSkipThrowsException(): void
{
$table = new Table('users', [], $this->adapter);
$table->addColumn('email', 'string', ['limit' => 255])
diff --git a/tests/TestCase/Db/Adapter/UnifiedMigrationsTableStorageTest.php b/tests/TestCase/Db/Adapter/UnifiedMigrationsTableStorageTest.php
index d7b30fc83..223609ad5 100644
--- a/tests/TestCase/Db/Adapter/UnifiedMigrationsTableStorageTest.php
+++ b/tests/TestCase/Db/Adapter/UnifiedMigrationsTableStorageTest.php
@@ -21,12 +21,7 @@
*/
class UnifiedMigrationsTableStorageTest extends TestCase
{
- /**
- * @var bool
- */
- private bool $tableCreated = false;
-
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -37,7 +32,7 @@ public function setUp(): void
$this->cleanupTable();
}
- public function tearDown(): void
+ protected function tearDown(): void
{
// Always clean up the table
$this->cleanupTable();
@@ -74,7 +69,7 @@ private function cleanupTable(): void
// Also drop any phinxlog tables that might exist
$connection->execute('DROP TABLE IF EXISTS phinxlog');
$connection->execute('DROP TABLE IF EXISTS migrator_phinxlog');
- } catch (Exception $e) {
+ } catch (Exception) {
// Ignore cleanup errors
}
}
@@ -96,7 +91,6 @@ public function testMigrateCreatesUnifiedTable(): void
$dialect = $connection->getDriver()->schemaDialect();
$this->assertTrue($dialect->hasTable(UnifiedMigrationsTableStorage::TABLE_NAME));
- $this->tableCreated = true;
// Verify records were inserted with null plugin (app migrations)
$result = $connection->selectQuery()
@@ -120,7 +114,6 @@ public function testMigratePluginUsesUnifiedTable(): void
// Run app migrations first to create the table
$this->exec('migrations migrate -c test --source Migrations --no-lock');
$this->assertExitSuccess();
- $this->tableCreated = true;
// Clear the migration records for app (but keep the table)
$this->clearMigrationRecords('test');
@@ -148,7 +141,6 @@ public function testRollbackWithUnifiedTable(): void
// Run migrations
$this->exec('migrations migrate -c test --source Migrations --no-lock');
$this->assertExitSuccess();
- $this->tableCreated = true;
// Verify we have records
$initialCount = $this->getMigrationRecordCount('test');
@@ -168,7 +160,6 @@ public function testStatusWithUnifiedTable(): void
// Run migrations
$this->exec('migrations migrate -c test --source Migrations --no-lock');
$this->assertExitSuccess();
- $this->tableCreated = true;
// Check status
$this->exec('migrations status -c test --source Migrations');
@@ -183,7 +174,6 @@ public function testAppAndPluginMigrationsAreSeparated(): void
// Run app migrations
$this->exec('migrations migrate -c test --source Migrations --no-lock');
$this->assertExitSuccess();
- $this->tableCreated = true;
// Run plugin migrations
$this->exec('migrations migrate -c test --plugin Migrator --no-lock');
diff --git a/tests/TestCase/Db/LiteralTest.php b/tests/TestCase/Db/LiteralTest.php
index e8c55fdb5..1ff0c617a 100644
--- a/tests/TestCase/Db/LiteralTest.php
+++ b/tests/TestCase/Db/LiteralTest.php
@@ -8,18 +8,18 @@
class LiteralTest extends TestCase
{
- public function testToString()
+ public function testToString(): void
{
$str = 'test1';
$instance = new Literal($str);
$this->assertEquals($str, (string)$instance);
}
- public function testFrom()
+ public function testFrom(): void
{
$str = 'test1';
$instance = Literal::from($str);
- $this->assertInstanceOf('\Migrations\Db\Literal', $instance);
+ $this->assertInstanceOf(Literal::class, $instance);
$this->assertEquals($str, (string)$instance);
}
}
diff --git a/tests/TestCase/Db/Table/ColumnTest.php b/tests/TestCase/Db/Table/ColumnTest.php
index 0652149f3..a75c8a4e2 100644
--- a/tests/TestCase/Db/Table/ColumnTest.php
+++ b/tests/TestCase/Db/Table/ColumnTest.php
@@ -22,7 +22,7 @@ protected function tearDown(): void
Configure::delete('Migrations.unsigned_ints');
}
- public function testNullConstructorParameter()
+ public function testNullConstructorParameter(): void
{
$column = new Column(name: 'title');
$this->assertTrue($column->isNull());
@@ -38,7 +38,7 @@ public function testNullConstructorParameter()
$this->assertTrue($column->isNull());
}
- public function testSetOptionThrowsExceptionIfOptionIsNotString()
+ public function testSetOptionThrowsExceptionIfOptionIsNotString(): void
{
$column = new Column();
@@ -48,7 +48,7 @@ public function testSetOptionThrowsExceptionIfOptionIsNotString()
$column->setOptions(['identity']);
}
- public function testSetOptionsIdentity()
+ public function testSetOptionsIdentity(): void
{
$column = new Column();
$this->assertTrue($column->isNull());
@@ -59,7 +59,7 @@ public function testSetOptionsIdentity()
$this->assertTrue($column->isIdentity());
}
- public function testColumnNullFeatureFlag()
+ public function testColumnNullFeatureFlag(): void
{
$column = new Column();
$this->assertTrue($column->isNull());
@@ -275,4 +275,67 @@ public function testUnsignedConfigurationDoesNotAffectNonIntegerTypes(): void
$decimalColumn->setName('price')->setType('decimal');
$this->assertFalse($decimalColumn->isUnsigned());
}
+
+ public function testFixedOptionDefaultsToNull(): void
+ {
+ $column = new Column();
+ $column->setName('data')->setType('binary');
+
+ $this->assertNull($column->getFixed());
+ }
+
+ public function testSetFixedTrue(): void
+ {
+ $column = new Column();
+ $column->setName('hash')->setType('binary')->setFixed(true);
+
+ $this->assertTrue($column->getFixed());
+ }
+
+ public function testSetFixedFalse(): void
+ {
+ $column = new Column();
+ $column->setName('data')->setType('binary')->setFixed(false);
+
+ $this->assertFalse($column->getFixed());
+ }
+
+ public function testSetOptionsWithFixed(): void
+ {
+ $column = new Column();
+ $column->setName('hash')->setType('binary');
+ $column->setOptions(['fixed' => true, 'limit' => 20]);
+
+ $this->assertTrue($column->getFixed());
+ $this->assertSame(20, $column->getLimit());
+
+ // Null via setter should be accepted and preserved
+ $column2 = new Column();
+ $column2->setName('data')->setType('binary')->setFixed(null);
+ $this->assertNull($column2->getFixed());
+
+ // Null via setOptions (as generated by migration snapshots)
+ $column3 = new Column();
+ $column3->setName('data')->setType('binary');
+ $column3->setOptions(['fixed' => null, 'limit' => null, 'null' => false]);
+ $this->assertNull($column3->getFixed());
+ }
+
+ public function testToArrayIncludesFixed(): void
+ {
+ $column = new Column();
+ $column->setName('hash')->setType('binary')->setFixed(true)->setLimit(20);
+
+ $result = $column->toArray();
+ $this->assertTrue($result['fixed']);
+ }
+
+ public function testToArrayFixedNullByDefault(): void
+ {
+ $column = new Column();
+ $column->setName('data')->setType('binary')->setLimit(20);
+
+ $result = $column->toArray();
+ $this->assertNull($result['fixed']);
+ }
}
diff --git a/tests/TestCase/Db/Table/ForeignKeyTest.php b/tests/TestCase/Db/Table/ForeignKeyTest.php
index c2fce98ab..4c9fc179e 100644
--- a/tests/TestCase/Db/Table/ForeignKeyTest.php
+++ b/tests/TestCase/Db/Table/ForeignKeyTest.php
@@ -11,10 +11,7 @@
class ForeignKeyTest extends TestCase
{
- /**
- * @var ForeignKey
- */
- private $fk;
+ private ForeignKey $fk;
protected function setUp(): void
{
@@ -38,7 +35,7 @@ public function testReferencedColumns(): void
$this->assertEquals(['user_id', 'tenant_id'], $this->fk->getReferencedColumns());
}
- public function testOnDeleteSetNullCanBeSetThroughOptions()
+ public function testOnDeleteSetNullCanBeSetThroughOptions(): void
{
$this->assertEquals(
ForeignKey::SET_NULL,
@@ -46,30 +43,22 @@ public function testOnDeleteSetNullCanBeSetThroughOptions()
);
}
- public function testInitiallyActionsEmpty()
+ public function testInitiallyActionsEmpty(): void
{
$this->assertSame(ForeignKey::NO_ACTION, $this->fk->getOnDelete());
$this->assertSame(ForeignKey::NO_ACTION, $this->fk->getOnUpdate());
}
- /**
- * @param string $dirtyValue
- * @param string $valueOfConstant
- */
#[DataProvider('actionsProvider')]
- public function testBothActionsCanBeSetThroughSetters($dirtyValue, $valueOfConstant)
+ public function testBothActionsCanBeSetThroughSetters(string $dirtyValue, string $valueOfConstant): void
{
$this->fk->setOnDelete($dirtyValue)->setOnUpdate($dirtyValue);
$this->assertEquals($valueOfConstant, $this->fk->getOnDelete());
$this->assertEquals($valueOfConstant, $this->fk->getOnUpdate());
}
- /**
- * @param string $dirtyValue
- * @param string $valueOfConstant
- */
#[DataProvider('actionsProvider')]
- public function testBothActionsCanBeSetThroughOptions($dirtyValue, $valueOfConstant)
+ public function testBothActionsCanBeSetThroughOptions(string $dirtyValue, string $valueOfConstant): void
{
$this->fk->setOptions([
'delete' => $dirtyValue,
@@ -79,21 +68,21 @@ public function testBothActionsCanBeSetThroughOptions($dirtyValue, $valueOfConst
$this->assertEquals($valueOfConstant, $this->fk->getOnUpdate());
}
- public function testUnknownActionsNotAllowedThroughSetter()
+ public function testUnknownActionsNotAllowedThroughSetter(): void
{
$this->expectException(InvalidArgumentException::class);
$this->fk->setOnDelete('i m dump');
}
- public function testUnknownActionsNotAllowedThroughOptions()
+ public function testUnknownActionsNotAllowedThroughOptions(): void
{
$this->expectException(InvalidArgumentException::class);
$this->fk->setOptions(['update' => 'no yu a dumb']);
}
- public static function actionsProvider()
+ public static function actionsProvider(): array
{
return [
[ForeignKey::CASCADE, ForeignKey::CASCADE],
@@ -107,7 +96,7 @@ public static function actionsProvider()
];
}
- public function testSetOptionThrowsExceptionIfOptionIsNotString()
+ public function testSetOptionThrowsExceptionIfOptionIsNotString(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('"0" is not a valid foreign key option');
diff --git a/tests/TestCase/Db/Table/IndexTest.php b/tests/TestCase/Db/Table/IndexTest.php
index 6c9011c20..b42c3e136 100644
--- a/tests/TestCase/Db/Table/IndexTest.php
+++ b/tests/TestCase/Db/Table/IndexTest.php
@@ -9,7 +9,7 @@
class IndexTest extends TestCase
{
- public function testSetOptionThrowsExceptionIfOptionIsNotString()
+ public function testSetOptionThrowsExceptionIfOptionIsNotString(): void
{
$column = new Index();
diff --git a/tests/TestCase/Db/Table/TableTest.php b/tests/TestCase/Db/Table/TableTest.php
index 7e400bea1..204cc9d68 100644
--- a/tests/TestCase/Db/Table/TableTest.php
+++ b/tests/TestCase/Db/Table/TableTest.php
@@ -4,9 +4,11 @@
namespace Migrations\Test\TestCase\Db\Table;
use InvalidArgumentException;
+use Migrations\Db\Action\AddCheckConstraint;
use Migrations\Db\Action\AddColumn;
use Migrations\Db\Action\AddForeignKey;
use Migrations\Db\Action\AddIndex;
+use Migrations\Db\Action\DropCheckConstraint;
use Migrations\Db\Action\DropIndex;
use Migrations\Db\Adapter\AdapterInterface;
use Migrations\Db\Adapter\MysqlAdapter;
@@ -14,6 +16,7 @@
use Migrations\Db\Adapter\SqliteAdapter;
use Migrations\Db\Adapter\SqlserverAdapter;
use Migrations\Db\Table;
+use Migrations\Db\Table\CheckConstraint;
use Migrations\Db\Table\Column;
use Migrations\Db\Table\ForeignKey;
use Migrations\Db\Table\Index;
@@ -49,7 +52,7 @@ public static function provideTimestampColumnNames(): array
return $result;
}
- public function testAddColumnWithAnInvalidColumnType()
+ public function testAddColumnWithAnInvalidColumnType(): void
{
try {
$adapter = new MysqlAdapter([]);
@@ -61,13 +64,13 @@ public function testAddColumnWithAnInvalidColumnType()
$this->assertInstanceOf(
'InvalidArgumentException',
$e,
- 'Expected exception of type InvalidArgumentException, got ' . get_class($e),
+ 'Expected exception of type InvalidArgumentException, got ' . $e::class,
);
$this->assertStringStartsWith('An invalid column type ', $e->getMessage());
}
}
- public function testAddColumnWithColumnObject()
+ public function testAddColumnWithColumnObject(): void
{
$adapter = new MysqlAdapter([]);
$column = new Column();
@@ -75,12 +78,22 @@ public function testAddColumnWithColumnObject()
->setType('integer');
$table = new Table('ntable', [], $adapter);
$table->addColumn($column);
+
$actions = $this->getPendingActions($table);
$this->assertInstanceOf(AddColumn::class, $actions[0]);
$this->assertSame($column, $actions[0]->getColumn());
}
- public function testAddColumnWithNoAdapterSpecified()
+ public function testAddColumnWithNullTypeThrows(): void
+ {
+ $adapter = new MysqlAdapter([]);
+ $table = new Table('ntable', [], $adapter);
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Column type must not be null when column name is a string.');
+ $table->addColumn('email');
+ }
+
+ public function testAddColumnWithNoAdapterSpecified(): void
{
try {
$table = new Table('ntable');
@@ -90,12 +103,12 @@ public function testAddColumnWithNoAdapterSpecified()
$this->assertInstanceOf(
'RuntimeException',
$e,
- 'Expected exception of type RuntimeException, got ' . get_class($e),
+ 'Expected exception of type RuntimeException, got ' . $e::class,
);
}
}
- public function testAddComment()
+ public function testAddComment(): void
{
$adapter = new MysqlAdapter([]);
$table = new Table('ntable', ['comment' => 'test comment'], $adapter);
@@ -103,7 +116,7 @@ public function testAddComment()
$this->assertEquals('test comment', $options['comment']);
}
- public function testAddIndexWithIndexObject()
+ public function testAddIndexWithIndexObject(): void
{
$adapter = new MysqlAdapter([]);
$index = new Index();
@@ -111,6 +124,7 @@ public function testAddIndexWithIndexObject()
->setColumns(['email']);
$table = new Table('ntable', [], $adapter);
$table->addIndex($index);
+
$actions = $this->getPendingActions($table);
$this->assertInstanceOf(AddIndex::class, $actions[0]);
$this->assertSame($index, $actions[0]->getIndex());
@@ -144,8 +158,7 @@ public function testAddForeignKeyWithObject(): void
$key->setColumns('user_id')
->setReferencedTable('users')
->setReferencedColumns(['id'])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setOptions(['delete' => 'CASCADE', 'update' => 'CASCADE'])
->setName('fk_user_id'),
);
@@ -159,7 +172,6 @@ public function testAddForeignKeyWithObject(): void
}
/**
- * @param AdapterInterface $adapter
* @param string|null $createdAtColumnName
* @param string|null $updatedAtColumnName
* @param string $expectedCreatedAtColumnName
@@ -169,14 +181,15 @@ public function testAddForeignKeyWithObject(): void
#[DataProvider('provideTimestampColumnNames')]
public function testAddTimestamps(
AdapterInterface $adapter,
- $createdAtColumnName,
- $updatedAtColumnName,
+ string|bool|null $createdAtColumnName,
+ string|bool|null $updatedAtColumnName,
$expectedCreatedAtColumnName,
$expectedUpdatedAtColumnName,
$withTimezone,
): void {
$table = new Table('ntable', [], $adapter);
$table->addTimestamps($createdAtColumnName, $updatedAtColumnName, $withTimezone);
+
$actions = $this->getPendingActions($table);
$columns = [];
@@ -199,14 +212,12 @@ public function testAddTimestamps(
$this->assertEquals('CURRENT_TIMESTAMP', $columns[1]->getDefault());
}
- /**
- * @param AdapterInterface $adapter
- */
#[DataProvider('provideAdapters')]
- public function testAddTimestampsNoUpdated(AdapterInterface $adapter)
+ public function testAddTimestampsNoUpdated(AdapterInterface $adapter): void
{
$table = new Table('ntable', [], $adapter);
$table->addTimestamps(null, false);
+
$actions = $this->getPendingActions($table);
$columns = [];
@@ -224,14 +235,12 @@ public function testAddTimestampsNoUpdated(AdapterInterface $adapter)
$this->assertSame('', $columns[0]->getUpdate());
}
- /**
- * @param AdapterInterface $adapter
- */
#[DataProvider('provideAdapters')]
- public function testAddTimestampsNoCreated(AdapterInterface $adapter)
+ public function testAddTimestampsNoCreated(AdapterInterface $adapter): void
{
$table = new Table('ntable', [], $adapter);
$table->addTimestamps(false, null);
+
$actions = $this->getPendingActions($table);
$columns = [];
@@ -250,11 +259,8 @@ public function testAddTimestampsNoCreated(AdapterInterface $adapter)
$this->assertSame('CURRENT_TIMESTAMP', $columns[0]->getDefault());
}
- /**
- * @param AdapterInterface $adapter
- */
#[DataProvider('provideAdapters')]
- public function testAddTimestampsThrowsOnBothFalse(AdapterInterface $adapter)
+ public function testAddTimestampsThrowsOnBothFalse(AdapterInterface $adapter): void
{
$table = new Table('ntable', [], $adapter);
$this->expectException(RuntimeException::class);
@@ -263,7 +269,6 @@ public function testAddTimestampsThrowsOnBothFalse(AdapterInterface $adapter)
}
/**
- * @param AdapterInterface $adapter
* @param string|null $createdAtColumnName
* @param string|null $updatedAtColumnName
* @param string $expectedCreatedAtColumnName
@@ -273,14 +278,15 @@ public function testAddTimestampsThrowsOnBothFalse(AdapterInterface $adapter)
#[DataProvider('provideTimestampColumnNames')]
public function testAddTimestampsWithTimezone(
AdapterInterface $adapter,
- $createdAtColumnName,
- $updatedAtColumnName,
+ string|bool|null $createdAtColumnName,
+ string|bool|null $updatedAtColumnName,
$expectedCreatedAtColumnName,
$expectedUpdatedAtColumnName,
$withTimezone,
): void {
$table = new Table('ntable', [], $adapter);
$table->addTimestampsWithTimezone($createdAtColumnName, $updatedAtColumnName);
+
$actions = $this->getPendingActions($table);
$columns = [];
@@ -303,7 +309,7 @@ public function testAddTimestampsWithTimezone(
$this->assertEquals('CURRENT_TIMESTAMP', $columns[1]->getDefault());
}
- public function testInsert()
+ public function testInsert(): void
{
$adapterStub = $this->getMockBuilder(MysqlAdapter::class)
->setConstructorArgs([[]])
@@ -320,7 +326,7 @@ public function testInsert()
$this->assertEquals($expectedData, $table->getData());
}
- public function testInsertMultipleRowsWithoutZeroKey()
+ public function testInsertMultipleRowsWithoutZeroKey(): void
{
$adapterStub = $this->getMockBuilder(MysqlAdapter::class)
->setConstructorArgs([[]])
@@ -341,7 +347,7 @@ public function testInsertMultipleRowsWithoutZeroKey()
$this->assertEquals($expectedData, $table->getData());
}
- public function testInsertSaveEmptyData()
+ public function testInsertSaveEmptyData(): void
{
$adapterStub = $this->getMockBuilder(MysqlAdapter::class)
->setConstructorArgs([[]])
@@ -353,7 +359,7 @@ public function testInsertSaveEmptyData()
$table->insert([])->save();
}
- public function testInsertSaveData()
+ public function testInsertSaveData(): void
{
$adapterStub = $this->getMockBuilder(MysqlAdapter::class)
->setConstructorArgs([[]])
@@ -386,7 +392,7 @@ public function testInsertSaveData()
->save();
}
- public function testSaveAfterSaveData()
+ public function testSaveAfterSaveData(): void
{
$adapterStub = $this->getMockBuilder(MysqlAdapter::class)
->setConstructorArgs([[]])
@@ -419,31 +425,29 @@ public function testSaveAfterSaveData()
->save();
}
- public function testResetAfterAddingData()
+ public function testResetAfterAddingData(): void
{
$adapterStub = $this->getMockBuilder(MysqlAdapter::class)
->setConstructorArgs([[]])
->getMock();
$table = new Table('ntable', [], $adapterStub);
$columns = ['column1'];
- $data = [['value1']];
- $table->insert($columns, $data)->save();
+ $table->insert($columns)->save();
$this->assertEquals([], $table->getData());
}
- public function testPendingAfterAddingData()
+ public function testPendingAfterAddingData(): void
{
$adapterStub = $this->getMockBuilder(MysqlAdapter::class)
->setConstructorArgs([[]])
->getMock();
$table = new Table('ntable', [], $adapterStub);
$columns = ['column1'];
- $data = [['value1']];
- $table->insert($columns, $data);
+ $table->insert($columns);
$this->assertTrue($table->hasPendingActions());
}
- public function testPendingAfterAddingColumn()
+ public function testPendingAfterAddingColumn(): void
{
$adapterStub = $this->getMockBuilder(MysqlAdapter::class)
->setConstructorArgs([[]])
@@ -456,7 +460,7 @@ public function testPendingAfterAddingColumn()
$this->assertTrue($table->hasPendingActions());
}
- public function testGetColumn()
+ public function testGetColumn(): void
{
$adapterStub = $this->getMockBuilder(MysqlAdapter::class)
->setConstructorArgs([[]])
@@ -478,23 +482,22 @@ public function testGetColumn()
/**
* @param string $indexIdentifier
- * @param Index $index
*/
#[DataProvider('removeIndexDataprovider')]
- public function testRemoveIndex($indexIdentifier, Index $index)
+ public function testRemoveIndex(string|array $indexIdentifier, Index $index): void
{
$adapter = new MysqlAdapter([]);
$table = new Table('table', [], $adapter);
$table->removeIndex($indexIdentifier);
- $indexes = array_map(function (DropIndex $action) {
+ $indexes = array_map(function (DropIndex $action): Index {
return $action->getIndex();
}, $this->getPendingActions($table));
$this->assertEquals([$index], $indexes);
}
- public static function removeIndexDataprovider()
+ public static function removeIndexDataprovider(): array
{
return [
[
@@ -512,10 +515,47 @@ public static function removeIndexDataprovider()
];
}
+ public function testAddCheckConstraintWithExpression(): void
+ {
+ $adapter = new MysqlAdapter([]);
+ $table = new Table('ntable', [], $adapter);
+ $table->addCheckConstraint('age >= 18', ['name' => 'age_check']);
+
+ $actions = $this->getPendingActions($table);
+ $this->assertInstanceOf(AddCheckConstraint::class, $actions[0]);
+ $constraint = $actions[0]->getCheckConstraint();
+ $this->assertSame('age_check', $constraint->getName());
+ $this->assertSame('age >= 18', $constraint->getExpression());
+ }
+
+ public function testAddCheckConstraintWithObject(): void
+ {
+ $adapter = new MysqlAdapter([]);
+ $table = new Table('ntable', [], $adapter);
+ $constraint = new CheckConstraint('price_positive', 'price > 0');
+ $table->addCheckConstraint($constraint);
+
+ $actions = $this->getPendingActions($table);
+ $this->assertInstanceOf(AddCheckConstraint::class, $actions[0]);
+ $this->assertSame($constraint, $actions[0]->getCheckConstraint());
+ $this->assertSame('price_positive', $actions[0]->getCheckConstraint()->getName());
+ $this->assertSame('price > 0', $actions[0]->getCheckConstraint()->getExpression());
+ }
+
+ public function testDropCheckConstraint(): void
+ {
+ $adapter = new MysqlAdapter([]);
+ $table = new Table('ntable', [], $adapter);
+ $table->dropCheckConstraint('age_check');
+
+ $actions = $this->getPendingActions($table);
+ $this->assertInstanceOf(DropCheckConstraint::class, $actions[0]);
+ $this->assertSame('age_check', $actions[0]->getConstraintName());
+ }
+
protected function getPendingActions($table)
{
- $prop = new ReflectionProperty(get_class($table), 'actions');
- $prop->setAccessible(true);
+ $prop = new ReflectionProperty($table::class, 'actions');
return $prop->getValue($table)->getActions();
}
diff --git a/tests/TestCase/Middleware/PendingMigrationsMiddlewareTest.php b/tests/TestCase/Middleware/PendingMigrationsMiddlewareTest.php
index 9db05c0cc..e13e6f485 100644
--- a/tests/TestCase/Middleware/PendingMigrationsMiddlewareTest.php
+++ b/tests/TestCase/Middleware/PendingMigrationsMiddlewareTest.php
@@ -37,7 +37,7 @@ class PendingMigrationsMiddlewareTest extends TestCase
*
* @return void
*/
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
$connection = ConnectionManager::get('test');
@@ -51,7 +51,7 @@ public function setUp(): void
);
}
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
ConnectionManager::drop('custom');
@@ -66,7 +66,7 @@ public function testAppMigrationsFail(): void
$middleware = new PendingMigrationsMiddleware();
$request = new ServerRequest();
- $handler = new TestRequestHandler(function ($req) {
+ $handler = new TestRequestHandler(function ($req): Response {
return new Response();
});
@@ -95,11 +95,12 @@ public function testAppMigrationsSuccess(): void
],
];
$config = new Config($config);
+
$manager = new Manager($config, $this->io);
$manager->migrate(null, true);
$request = new ServerRequest();
- $handler = new TestRequestHandler(function ($req) {
+ $handler = new TestRequestHandler(function ($req): Response {
return new Response();
});
$result = $middleware->process($request, $handler);
@@ -118,7 +119,7 @@ public function testAppAndPluginsMigrationsFail(): void
]);
$request = new ServerRequest();
- $handler = new TestRequestHandler(function ($req) {
+ $handler = new TestRequestHandler(function ($req): Response {
return new Response();
});
@@ -151,11 +152,12 @@ public function testAppAndPluginsMigrationsSuccess(): void
],
];
$config = new Config($config);
+
$manager = new Manager($config, $this->io);
$manager->migrate(null, true);
$request = new ServerRequest();
- $handler = new TestRequestHandler(function ($req) {
+ $handler = new TestRequestHandler(function ($req): Response {
return new Response();
});
diff --git a/tests/TestCase/Migration/EnvironmentTest.php b/tests/TestCase/Migration/EnvironmentTest.php
index 84d060582..6f2dce7d9 100644
--- a/tests/TestCase/Migration/EnvironmentTest.php
+++ b/tests/TestCase/Migration/EnvironmentTest.php
@@ -9,8 +9,10 @@
use Migrations\BaseSeed;
use Migrations\Db\Adapter\AbstractAdapter;
use Migrations\Db\Adapter\AdapterWrapper;
+use Migrations\DirectionalMigrationInterface;
use Migrations\Migration\Environment;
use Migrations\MigrationInterface;
+use Migrations\ReversibleMigrationInterface;
use PHPUnit\Framework\TestCase;
use RuntimeException;
@@ -26,26 +28,26 @@ protected function setUp(): void
$this->environment = new Environment('test', []);
}
- public function testConstructorWorksAsExpected()
+ public function testConstructorWorksAsExpected(): void
{
$env = new Environment('testenv', ['foo' => 'bar']);
$this->assertEquals('testenv', $env->getName());
$this->assertArrayHasKey('foo', $env->getOptions());
}
- public function testSettingTheName()
+ public function testSettingTheName(): void
{
$this->environment->setName('prod123');
$this->assertEquals('prod123', $this->environment->getName());
}
- public function testSettingOptions()
+ public function testSettingOptions(): void
{
$this->environment->setOptions(['foo' => 'bar']);
$this->assertArrayHasKey('foo', $this->environment->getOptions());
}
- public function testInvalidAdapter()
+ public function testInvalidAdapter(): void
{
$this->environment->setOptions(['adapter' => 'fakeadapter']);
@@ -55,14 +57,14 @@ public function testInvalidAdapter()
$this->environment->getAdapter();
}
- public function testNoAdapter()
+ public function testNoAdapter(): void
{
$this->expectException(RuntimeException::class);
$this->environment->getAdapter();
}
- public function testGetAdapterWithBadConnectionName()
+ public function testGetAdapterWithBadConnectionName(): void
{
$this->environment->setOptions(['connection' => 'lolnope']);
@@ -72,7 +74,7 @@ public function testGetAdapterWithBadConnectionName()
$this->environment->getAdapter();
}
- public function testGetAdapter()
+ public function testGetAdapter(): void
{
/** @var array $config */
$config = ConnectionManager::getConfig('test');
@@ -86,7 +88,7 @@ public function testGetAdapter()
$this->assertInstanceOf(AdapterWrapper::class, $adapter);
}
- public function testSchemaName()
+ public function testSchemaName(): void
{
$this->assertEquals('phinxlog', $this->environment->getSchemaTableName());
@@ -94,7 +96,7 @@ public function testSchemaName()
$this->assertEquals('changelog', $this->environment->getSchemaTableName());
}
- public function testCurrentVersion()
+ public function testCurrentVersion(): void
{
$stub = $this->getMockBuilder(AbstractAdapter::class)
->setConstructorArgs([[]])
@@ -108,7 +110,7 @@ public function testCurrentVersion()
$this->assertEquals(20110301080000, $this->environment->getCurrentVersion());
}
- public function testExecutingAMigrationUp()
+ public function testExecutingAMigrationUp(): void
{
// stub adapter
$adapterStub = $this->getMockBuilder(AbstractAdapter::class)
@@ -123,6 +125,7 @@ public function testExecutingAMigrationUp()
// up
$upMigration = new class (20110301080000) extends BaseMigration {
public bool $executed = false;
+
public function up(): void
{
$this->executed = true;
@@ -133,7 +136,7 @@ public function up(): void
$this->assertTrue($upMigration->executed);
}
- public function testExecutingAMigrationDown()
+ public function testExecutingAMigrationDown(): void
{
// stub adapter
$adapterStub = $this->getMockBuilder(AbstractAdapter::class)
@@ -148,6 +151,7 @@ public function testExecutingAMigrationDown()
// down
$downMigration = new class (20110301080000) extends BaseMigration {
public bool $executed = false;
+
public function down(): void
{
$this->executed = true;
@@ -158,7 +162,7 @@ public function down(): void
$this->assertTrue($downMigration->executed);
}
- public function testExecutingAMigrationWithTransactions()
+ public function testExecutingAMigrationWithTransactions(): void
{
// stub adapter
$adapterStub = $this->getMockBuilder(AbstractAdapter::class)
@@ -179,6 +183,7 @@ public function testExecutingAMigrationWithTransactions()
// migrate
$migration = new class (20110301080000) extends BaseMigration {
public bool $executed = false;
+
public function up(): void
{
$this->executed = true;
@@ -189,7 +194,7 @@ public function up(): void
$this->assertTrue($migration->executed);
}
- public function testExecutingAMigrationWithUseTransactions()
+ public function testExecutingAMigrationWithUseTransactions(): void
{
// stub adapter
$adapterStub = $this->getMockBuilder(AbstractAdapter::class)
@@ -201,8 +206,7 @@ public function testExecutingAMigrationWithUseTransactions()
$adapterStub->expects($this->never())
->method('commitTransaction');
- $adapterStub->expects($this->atLeastOnce())
- ->method('hasTransactions')
+ $adapterStub->method('hasTransactions')
->willReturn(true);
$this->environment->setAdapter($adapterStub);
@@ -226,7 +230,7 @@ public function up(): void
$this->assertTrue($migration->executed);
}
- public function testExecutingAChangeMigrationUp()
+ public function testExecutingAChangeMigrationUp(): void
{
// stub adapter
$adapterStub = $this->getMockBuilder(AbstractAdapter::class)
@@ -241,6 +245,7 @@ public function testExecutingAChangeMigrationUp()
// migration
$migration = new class (20130301080000) extends BaseMigration {
public bool $executed = false;
+
public function change(): void
{
$this->executed = true;
@@ -251,7 +256,7 @@ public function change(): void
$this->assertTrue($migration->executed);
}
- public function testExecutingAChangeMigrationDown()
+ public function testExecutingAChangeMigrationDown(): void
{
// stub adapter
$adapterStub = $this->getMockBuilder(AbstractAdapter::class)
@@ -266,6 +271,55 @@ public function testExecutingAChangeMigrationDown()
// migration
$migration = new class (20130301080000) extends BaseMigration {
public bool $executed = false;
+
+ public function change(): void
+ {
+ $this->executed = true;
+ }
+ };
+
+ $this->environment->executeMigration($migration, MigrationInterface::DOWN);
+ $this->assertTrue($migration->executed);
+ }
+
+ public function testExecutingAReversibleInterfaceMigrationUp(): void
+ {
+ $adapterStub = $this->getMockBuilder(AbstractAdapter::class)
+ ->setConstructorArgs([[]])
+ ->getMock();
+ $adapterStub->expects($this->once())
+ ->method('migrated')
+ ->willReturn($adapterStub);
+
+ $this->environment->setAdapter($adapterStub);
+
+ $migration = new class (20260513120000) extends BaseMigration implements ReversibleMigrationInterface {
+ public bool $executed = false;
+
+ public function change(): void
+ {
+ $this->executed = true;
+ }
+ };
+
+ $this->environment->executeMigration($migration, MigrationInterface::UP);
+ $this->assertTrue($migration->executed);
+ }
+
+ public function testExecutingAReversibleInterfaceMigrationDown(): void
+ {
+ $adapterStub = $this->getMockBuilder(AbstractAdapter::class)
+ ->setConstructorArgs([[]])
+ ->getMock();
+ $adapterStub->expects($this->once())
+ ->method('migrated')
+ ->willReturn($adapterStub);
+
+ $this->environment->setAdapter($adapterStub);
+
+ $migration = new class (20260513120000) extends BaseMigration implements ReversibleMigrationInterface {
+ public bool $executed = false;
+
public function change(): void
{
$this->executed = true;
@@ -276,7 +330,112 @@ public function change(): void
$this->assertTrue($migration->executed);
}
- public function testExecutingAFakeMigration()
+ public function testExecutingADirectionalInterfaceMigrationUp(): void
+ {
+ $adapterStub = $this->getMockBuilder(AbstractAdapter::class)
+ ->setConstructorArgs([[]])
+ ->getMock();
+ $adapterStub->expects($this->once())
+ ->method('migrated')
+ ->willReturn($adapterStub);
+
+ $this->environment->setAdapter($adapterStub);
+
+ $migration = new class (20260513120000) extends BaseMigration implements DirectionalMigrationInterface {
+ public bool $upExecuted = false;
+
+ public bool $downExecuted = false;
+
+ public function up(): void
+ {
+ $this->upExecuted = true;
+ }
+
+ public function down(): void
+ {
+ $this->downExecuted = true;
+ }
+ };
+
+ $this->environment->executeMigration($migration, MigrationInterface::UP);
+ $this->assertTrue($migration->upExecuted);
+ $this->assertFalse($migration->downExecuted);
+ }
+
+ public function testExecutingADirectionalInterfaceMigrationDown(): void
+ {
+ $adapterStub = $this->getMockBuilder(AbstractAdapter::class)
+ ->setConstructorArgs([[]])
+ ->getMock();
+ $adapterStub->expects($this->once())
+ ->method('migrated')
+ ->willReturn($adapterStub);
+
+ $this->environment->setAdapter($adapterStub);
+
+ $migration = new class (20260513120000) extends BaseMigration implements DirectionalMigrationInterface {
+ public bool $upExecuted = false;
+
+ public bool $downExecuted = false;
+
+ public function up(): void
+ {
+ $this->upExecuted = true;
+ }
+
+ public function down(): void
+ {
+ $this->downExecuted = true;
+ }
+ };
+
+ $this->environment->executeMigration($migration, MigrationInterface::DOWN);
+ $this->assertTrue($migration->downExecuted);
+ $this->assertFalse($migration->upExecuted);
+ }
+
+ /**
+ * If a class declares DirectionalMigrationInterface, dispatch must use
+ * up()/down() even if the class happens to define a change() method —
+ * the interface declaration wins over the legacy method_exists check.
+ */
+ public function testDirectionalInterfaceWinsOverChangeMethod(): void
+ {
+ $adapterStub = $this->getMockBuilder(AbstractAdapter::class)
+ ->setConstructorArgs([[]])
+ ->getMock();
+ $adapterStub->expects($this->once())
+ ->method('migrated')
+ ->willReturn($adapterStub);
+
+ $this->environment->setAdapter($adapterStub);
+
+ $migration = new class (20260513120000) extends BaseMigration implements DirectionalMigrationInterface {
+ public bool $upExecuted = false;
+
+ public bool $changeExecuted = false;
+
+ public function up(): void
+ {
+ $this->upExecuted = true;
+ }
+
+ public function down(): void
+ {
+ }
+
+ public function change(): void
+ {
+ $this->changeExecuted = true;
+ }
+ };
+
+ $this->environment->executeMigration($migration, MigrationInterface::UP);
+ $this->assertTrue($migration->upExecuted);
+ $this->assertFalse($migration->changeExecuted);
+ }
+
+ public function testExecutingAFakeMigration(): void
{
// stub adapter
$adapterStub = $this->getMockBuilder(AbstractAdapter::class)
@@ -291,6 +450,7 @@ public function testExecutingAFakeMigration()
// migration
$migration = new class (20130301080000) extends BaseMigration {
public bool $executed = false;
+
public function change(): void
{
$this->executed = true;
@@ -301,7 +461,7 @@ public function change(): void
$this->assertFalse($migration->executed);
}
- public function testGettingInputObject()
+ public function testGettingInputObject(): void
{
$mock = $this->getMockBuilder(ConsoleIo::class)->getMock();
$this->environment->setIo($mock);
@@ -309,7 +469,7 @@ public function testGettingInputObject()
$this->assertInstanceOf(ConsoleIo::class, $inputObject);
}
- public function testExecuteMigrationCallsInit()
+ public function testExecuteMigrationCallsInit(): void
{
// stub adapter
$adapterStub = $this->getMockBuilder(AbstractAdapter::class)
@@ -324,6 +484,7 @@ public function testExecuteMigrationCallsInit()
// up
$upMigration = new class (20110301080000) extends BaseMigration {
public bool $initExecuted = false;
+
public bool $upExecuted = false;
public function init(): void
@@ -341,7 +502,7 @@ public function up(): void
$this->assertTrue($upMigration->upExecuted);
}
- public function testExecuteSeedInit()
+ public function testExecuteSeedInit(): void
{
// stub adapter
$adapterStub = $this->getMockBuilder(AbstractAdapter::class)
@@ -352,6 +513,7 @@ public function testExecuteSeedInit()
$seed = new class (20110301080000) extends BaseSeed {
public bool $initExecuted = false;
+
public bool $runExecuted = false;
public function init(): void
diff --git a/tests/TestCase/Migration/ManagerFactoryTest.php b/tests/TestCase/Migration/ManagerFactoryTest.php
index 0352e887f..12bdd2dbf 100644
--- a/tests/TestCase/Migration/ManagerFactoryTest.php
+++ b/tests/TestCase/Migration/ManagerFactoryTest.php
@@ -6,6 +6,8 @@
use Cake\Console\ConsoleIo;
use Cake\Console\TestSuite\StubConsoleInput;
use Cake\Console\TestSuite\StubConsoleOutput;
+use Cake\Database\Connection;
+use Cake\Database\Driver\Mysql;
use Cake\Datasource\ConnectionManager;
use Migrations\Migration\ManagerFactory;
use PHPUnit\Framework\TestCase;
@@ -16,6 +18,7 @@ public function testConnection(): void
{
$out = new StubConsoleOutput();
$out->setOutputAs(StubConsoleOutput::PLAIN);
+
$in = new StubConsoleInput([]);
$io = new ConsoleIo($out, $out, $in);
@@ -41,6 +44,7 @@ public function testDsnConnection(): void
{
$out = new StubConsoleOutput();
$out->setOutputAs(StubConsoleOutput::PLAIN);
+
$in = new StubConsoleInput([]);
$io = new ConsoleIo($out, $out, $in);
@@ -53,9 +57,9 @@ public function testDsnConnection(): void
'scheme' => 'mysql',
'username' => 'root',
'host' => '127.0.0.1',
- 'className' => 'Cake\Database\Connection',
+ 'className' => Connection::class,
'database' => 'db_tmp',
- 'driver' => 'Cake\Database\Driver\Mysql',
+ 'driver' => Mysql::class,
'name' => 'tmp',
];
$this->assertEquals($expected, ConnectionManager::getConfig('tmp'));
diff --git a/tests/TestCase/Migration/ManagerTest.php b/tests/TestCase/Migration/ManagerTest.php
index ba7c324dd..8d88b5755 100644
--- a/tests/TestCase/Migration/ManagerTest.php
+++ b/tests/TestCase/Migration/ManagerTest.php
@@ -13,6 +13,7 @@
use Migrations\Db\Adapter\AdapterInterface;
use Migrations\Migration\Environment;
use Migrations\Migration\Manager;
+use Migrations\MigrationInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
@@ -40,10 +41,7 @@ class ManagerTest extends TestCase
*/
protected $in;
- /**
- * @var Manager
- */
- private $manager;
+ private ?Manager $manager = null;
protected function setUp(): void
{
@@ -51,6 +49,7 @@ protected function setUp(): void
$this->out = new StubConsoleOutput();
$this->out->setOutputAs(StubConsoleOutput::PLAIN);
+
$this->in = new StubConsoleInput([]);
$this->io = new ConsoleIo($this->out, $this->out, $this->in);
@@ -70,9 +69,9 @@ protected static function getDriverType(): string
protected function getOutput(): string
{
$lines = $this->out->messages();
- $lines = array_map(fn($line) => strip_tags($line), $lines);
+ $lines = array_map(strip_tags(...), $lines);
- return join("\n", $lines);
+ return implode("\n", $lines);
}
protected function tearDown(): void
@@ -80,11 +79,6 @@ protected function tearDown(): void
$this->manager = null;
}
- private static function getCorrectedPath(string $path): string
- {
- return str_replace('/', DIRECTORY_SEPARATOR, $path);
- }
-
/**
* Returns a sample configuration array for use with the unit tests.
*
@@ -306,7 +300,7 @@ public function testPrintStatusMethodWithBreakpointSet(): void
$this->assertEquals($expected, $return);
}
- public function testPrintStatusMethodWithNoMigrations()
+ public function testPrintStatusMethodWithNoMigrations(): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -325,7 +319,7 @@ public function testPrintStatusMethodWithNoMigrations()
$this->assertEquals([], $return);
}
- public function testPrintStatusMethodWithMissingMigrations()
+ public function testPrintStatusMethodWithMissingMigrations(): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -384,7 +378,7 @@ public function testPrintStatusMethodWithMissingMigrations()
$this->assertEquals($expected, $return);
}
- public function testPrintStatusMethodWithMissingLastMigration()
+ public function testPrintStatusMethodWithMissingLastMigration(): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -445,7 +439,7 @@ public function testPrintStatusMethodWithMissingLastMigration()
$this->assertEquals($expected, $return);
}
- public function testPrintStatusMethodWithMissingMigrationsAndBreakpointSet()
+ public function testPrintStatusMethodWithMissingMigrationsAndBreakpointSet(): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -504,7 +498,7 @@ public function testPrintStatusMethodWithMissingMigrationsAndBreakpointSet()
$this->assertEquals($expected, $return);
}
- public function testPrintStatusMethodWithDownMigrations()
+ public function testPrintStatusMethodWithDownMigrations(): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -540,7 +534,7 @@ public function testPrintStatusMethodWithDownMigrations()
$this->assertEquals($expected, $return);
}
- public function testPrintStatusMethodWithMissingAndDownMigrations()
+ public function testPrintStatusMethodWithMissingAndDownMigrations(): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -605,7 +599,7 @@ public function testPrintStatusMethodWithMissingAndDownMigrations()
$this->assertEquals($expected, $return);
}
- public function testGetMigrationsWithDuplicateMigrationVersions()
+ public function testGetMigrationsWithDuplicateMigrationVersions(): void
{
$config = new Config(['paths' => ['migrations' => ROOT . '/config/Duplicateversions']]);
$manager = new Manager($config, $this->io);
@@ -616,7 +610,7 @@ public function testGetMigrationsWithDuplicateMigrationVersions()
$manager->getMigrations();
}
- public function testGetMigrationsWithDuplicateMigrationNames()
+ public function testGetMigrationsWithDuplicateMigrationNames(): void
{
$config = new Config(['paths' => ['migrations' => ROOT . '/config/Duplicatenames']]);
$manager = new Manager($config, $this->io);
@@ -627,7 +621,7 @@ public function testGetMigrationsWithDuplicateMigrationNames()
$manager->getMigrations();
}
- public function testGetMigrationsWithInvalidMigrationClassName()
+ public function testGetMigrationsWithInvalidMigrationClassName(): void
{
$config = new Config(['paths' => ['migrations' => ROOT . '/config/Invalidclassname']]);
$manager = new Manager($config, $this->io);
@@ -639,7 +633,19 @@ public function testGetMigrationsWithInvalidMigrationClassName()
$manager->getMigrations();
}
- public function testGetMigrationsWithAnonymousClass()
+ public function testGetMigrationsWithLegacyAbstractMigrationClass(): void
+ {
+ $config = new Config(['paths' => ['migrations' => ROOT . '/config/LegacyAbstractMigration']]);
+ $manager = new Manager($config, $this->io);
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessageMatches('/uses the legacy `Migrations\\\\AbstractMigration` class/');
+ $this->expectExceptionMessageMatches('/20260327000000_LegacyAbstractMigration\\.php/');
+
+ $manager->getMigrations();
+ }
+
+ public function testGetMigrationsWithAnonymousClass(): void
{
$config = new Config(['paths' => ['migrations' => ROOT . '/config/AnonymousMigrations']]);
$manager = new Manager($config, $this->io);
@@ -653,13 +659,13 @@ public function testGetMigrationsWithAnonymousClass()
$migration = reset($migrations);
// Check that it's a valid migration object
- $this->assertInstanceOf('\Migrations\MigrationInterface', $migration);
+ $this->assertInstanceOf(MigrationInterface::class, $migration);
// Check the version was set correctly (2024_12_08_150000 => 20241208150000)
$this->assertEquals(20241208150000, $migration->getVersion());
}
- public function testGettingAValidEnvironment()
+ public function testGettingAValidEnvironment(): void
{
$this->assertInstanceOf(
Environment::class,
@@ -672,12 +678,9 @@ public function testGettingAValidEnvironment()
* migration to point to.
*
* @param string[] $availableMigrations
- * @param string $dateString
- * @param string $expectedMigration
- * @param string $message
*/
#[DataProvider('migrateDateDataProvider')]
- public function testMigrationsByDate(array $availableMigrations, $dateString, $expectedMigration, $message)
+ public function testMigrationsByDate(array $availableMigrations, string $dateString, string $expectedMigration, string $message): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -707,12 +710,10 @@ public function testMigrationsByDate(array $availableMigrations, $dateString, $e
* migration to point to.
*
* @param string[] $availableMigrations
- * @param int $count
* @param string $expectedMigration
- * @param string $message
*/
#[DataProvider('migrateByCountDataProvider')]
- public function testMigrationsByCount(array $availableMigrations, $count, $expectedMigration, $message)
+ public function testMigrationsByCount(array $availableMigrations, int $count, ?string $expectedMigration, string $message): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -732,7 +733,7 @@ public function testMigrationsByCount(array $availableMigrations, $count, $expec
// Mock getMigrations to return the available migrations
$migrations = [];
foreach ($availableMigrations as $version) {
- $migration = $this->getMockBuilder('\Migrations\MigrationInterface')
+ $migration = $this->getMockBuilder(MigrationInterface::class)
->getMock();
$migration->expects($this->any())
->method('getVersion')
@@ -746,11 +747,11 @@ public function testMigrationsByCount(array $availableMigrations, $count, $expec
// Use reflection to set the migrations property
$reflection = new ReflectionClass($this->manager);
$migrationsProperty = $reflection->getProperty('migrations');
- $migrationsProperty->setAccessible(true);
$migrationsProperty->setValue($this->manager, $migrations);
$this->manager->setEnvironment($envStub);
$this->manager->migrate(null, false, $count);
+
$output = $this->getOutput();
if (is_null($expectedMigration)) {
$this->assertEmpty($output, $message);
@@ -764,7 +765,7 @@ public function testMigrationsByCount(array $availableMigrations, $count, $expec
* migration to point to.
*/
#[DataProvider('rollbackToVersionDataProvider')]
- public function testRollbackToVersion($availableRollbacks, $version, $expectedOutput)
+ public function testRollbackToVersion(array $availableRollbacks, ?string $version, string|array|null $expectedOutput): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -776,6 +777,7 @@ public function testRollbackToVersion($availableRollbacks, $version, $expectedOu
$this->manager->setEnvironment($envStub);
$this->manager->rollback($version);
+
$output = $this->getOutput();
if (is_null($expectedOutput)) {
$output = explode("\n", $output);
@@ -796,7 +798,7 @@ public function testRollbackToVersion($availableRollbacks, $version, $expectedOu
* migration to point to.
*/
#[DataProvider('rollbackToDateDataProvider')]
- public function testRollbackToDate($availableRollbacks, $version, $expectedOutput)
+ public function testRollbackToDate(array $availableRollbacks, string $version, string|array|null $expectedOutput): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -808,6 +810,7 @@ public function testRollbackToDate($availableRollbacks, $version, $expectedOutpu
$this->manager->setEnvironment($envStub);
$this->manager->rollback($version, false, false);
+
$output = $this->getOutput();
if (is_null($expectedOutput)) {
$output = explode("\n", $output);
@@ -827,7 +830,7 @@ public function testRollbackToDate($availableRollbacks, $version, $expectedOutpu
* Test that rollbacking with count stops at the right migration.
*/
#[DataProvider('rollbackByCountDataProvider')]
- public function testRollbackByCount($availableRollbacks, $count, $expectedOutput)
+ public function testRollbackByCount(array $availableRollbacks, int $count, string|array|null $expectedOutput): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -840,7 +843,7 @@ public function testRollbackByCount($availableRollbacks, $count, $expectedOutput
// Mock getMigrations to return migrations matching the version log
$migrations = [];
foreach ($availableRollbacks as $version => $details) {
- $migration = $this->getMockBuilder('\Migrations\MigrationInterface')
+ $migration = $this->getMockBuilder(MigrationInterface::class)
->getMock();
$migration->expects($this->any())
->method('getVersion')
@@ -857,11 +860,11 @@ public function testRollbackByCount($availableRollbacks, $count, $expectedOutput
// Use reflection to set the migrations property
$reflection = new ReflectionClass($this->manager);
$migrationsProperty = $reflection->getProperty('migrations');
- $migrationsProperty->setAccessible(true);
$migrationsProperty->setValue($this->manager, $migrations);
$this->manager->setEnvironment($envStub);
$this->manager->rollbackByCount($count);
+
$output = $this->getOutput();
if (is_null($expectedOutput)) {
$output = explode("\n", $output);
@@ -882,7 +885,7 @@ public function testRollbackByCount($availableRollbacks, $count, $expectedOutput
* migration to point to.
*/
#[DataProvider('rollbackToVersionByExecutionTimeDataProvider')]
- public function testRollbackToVersionByExecutionTime($availableRollbacks, $version, $expectedOutput)
+ public function testRollbackToVersionByExecutionTime(array $availableRollbacks, ?string $version, string|array $expectedOutput): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -900,6 +903,7 @@ public function testRollbackToVersionByExecutionTime($availableRollbacks, $versi
$this->manager = new Manager($config, $this->io);
$this->manager->setEnvironment($envStub);
$this->manager->rollback($version);
+
$output = $this->getOutput();
if (is_null($expectedOutput)) {
@@ -920,7 +924,7 @@ public function testRollbackToVersionByExecutionTime($availableRollbacks, $versi
* migration to point to.
*/
#[DataProvider('rollbackToVersionByExecutionTimeDataProvider')]
- public function testRollbackToVersionByName($availableRollbacks, $version, $expectedOutput)
+ public function testRollbackToVersionByName(array $availableRollbacks, ?string $version, string|array $expectedOutput): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -938,6 +942,7 @@ public function testRollbackToVersionByName($availableRollbacks, $version, $expe
$this->manager = new Manager($config, $this->io);
$this->manager->setEnvironment($envStub);
$this->manager->rollback($availableRollbacks[$version]['migration_name'] ?? $version);
+
$output = $this->getOutput();
if (is_null($expectedOutput)) {
@@ -958,7 +963,7 @@ public function testRollbackToVersionByName($availableRollbacks, $version, $expe
* migration to point to.
*/
#[DataProvider('rollbackToDateByExecutionTimeDataProvider')]
- public function testRollbackToDateByExecutionTime($availableRollbacks, $date, $expectedOutput)
+ public function testRollbackToDateByExecutionTime(array $availableRollbacks, ?string $date, string|array|null $expectedOutput): void
{
// stub environment
$envStub = $this->getMockBuilder(Environment::class)
@@ -976,6 +981,7 @@ public function testRollbackToDateByExecutionTime($availableRollbacks, $date, $e
$this->manager = new Manager($config, $this->io);
$this->manager->setEnvironment($envStub);
$this->manager->rollback($date, false, false);
+
$output = $this->getOutput();
if (is_null($expectedOutput)) {
@@ -1009,6 +1015,7 @@ public function testRollbackToVersionWithSingleMigrationDoesNotFail(): void
$this->manager->setEnvironment($envStub);
$this->manager->rollback();
+
$output = $this->getOutput();
$this->assertStringContainsString('== 20120111235330 TestMigration: reverting', $output);
@@ -1042,6 +1049,7 @@ public function testRollbackToVersionWithTwoMigrations(): void
$this->manager->setEnvironment($envStub);
$this->manager->rollback();
+
$output = $this->getOutput();
$this->assertStringNotContainsString('== 20120111235330 TestMigration: reverting', $output);
@@ -1068,7 +1076,8 @@ public function testRollbackLast(array $availableRolbacks, string $versionOrder,
$this->manager = new Manager($config, $this->io);
$this->manager->setEnvironment($envStub);
- $this->manager->rollback(null);
+ $this->manager->rollback();
+
$output = $this->getOutput();
if (is_null($expectedOutput)) {
$this->assertEquals('No migrations to rollback' . PHP_EOL, $output);
@@ -2413,7 +2422,7 @@ public function testExecuteSeedWorksAsExpected(): void
$this->manager->setEnvironment($envStub);
$this->manager->seed();
- $output = join("\n", $this->out->messages());
+ $output = implode("\n", $this->out->messages());
$this->assertStringContainsString('GSeeder', $output);
$this->assertStringContainsString('PostSeeder', $output);
$this->assertStringContainsString('UserSeeder', $output);
@@ -2427,7 +2436,8 @@ public function testExecuteASingleSeedWorksAsExpected(): void
->getMock();
$this->manager->setEnvironment($envStub);
$this->manager->seed('UserSeeder');
- $output = join("\n", $this->out->messages());
+
+ $output = implode("\n", $this->out->messages());
$this->assertStringContainsString('UserSeeder', $output);
}
@@ -2462,7 +2472,7 @@ public function testSeedWillNotBeExecuted(): void
$this->manager->setEnvironment($envStub);
$this->manager->seed('UserSeederNotExecuted');
- $output = join("\n", $this->out->messages());
+ $output = implode("\n", $this->out->messages());
$this->assertStringContainsString('skipped', $output);
}
@@ -2950,7 +2960,7 @@ public function testInvalidVersionBreakpoint(): void
$this->manager->setEnvironment($envStub);
$this->manager->setBreakpoint(20120133235330);
- $output = join("\n", $this->out->messages());
+ $output = implode("\n", $this->out->messages());
$this->assertStringContainsString('warning 20120133235330 is not a valid version', $output);
}
@@ -2977,4 +2987,20 @@ public function testMigrationWillNotBeExecuted(): void
$this->assertTrue($adapter->hasTable('info'));
}
+
+ public function testMigrationShouldExecuteCanQueryDatabase(): void
+ {
+ $configArray = $this->getConfigArray();
+ $adapter = $this->manager->getEnvironment()->getAdapter();
+
+ // override the migrations directory to use a shouldExecute() that queries the database
+ $configArray['paths']['migrations'] = ROOT . '/config/ShouldExecuteWithDb/';
+ $config = new Config($configArray);
+
+ // shouldExecute() calls getAdapter()->hasTable(); before the fix this threw 'Adapter not set.'
+ $this->manager->setConfig($config);
+ $this->manager->migrate(20201207205100);
+
+ $this->assertTrue($adapter->hasTable('info'));
+ }
}
diff --git a/tests/TestCase/MigrationsTest.php b/tests/TestCase/MigrationsTest.php
index 2830b88c2..44bc6c4af 100644
--- a/tests/TestCase/MigrationsTest.php
+++ b/tests/TestCase/MigrationsTest.php
@@ -55,7 +55,7 @@ class MigrationsTest extends TestCase
*
* @return void
*/
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -108,7 +108,7 @@ public function setUp(): void
*
* @return void
*/
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
unset($this->Connection, $this->migrations);
@@ -125,7 +125,7 @@ public function tearDown(): void
*
* @return void
*/
- public function testStatus()
+ public function testStatus(): void
{
$result = $this->migrations->status();
$expected = [
@@ -158,7 +158,7 @@ public function testStatus()
*
* @return void
*/
- public function testMigrateAndRollback()
+ public function testMigrateAndRollback(): void
{
if ($this->Connection->getDriver() instanceof Sqlserver) {
// TODO This test currently fails in CI because numbers table
@@ -252,7 +252,7 @@ public function testMigrateAndRollback()
*
* @return void
*/
- public function testCreateWithEncoding()
+ public function testCreateWithEncoding(): void
{
$this->skipIf(env('DB') !== 'mysql', 'Requires MySQL');
@@ -279,7 +279,7 @@ public function testCreateWithEncoding()
*
* @return void
*/
- public function testMarkMigratedAll()
+ public function testMarkMigratedAll(): void
{
$markMigrated = $this->migrations->markMigrated();
$this->assertTrue($markMigrated);
@@ -316,7 +316,7 @@ public function testMarkMigratedAll()
*
* @return void
*/
- public function testMarkMigratedAllAsVersion()
+ public function testMarkMigratedAllAsVersion(): void
{
$markMigrated = $this->migrations->markMigrated('all');
$this->assertTrue($markMigrated);
@@ -352,7 +352,7 @@ public function testMarkMigratedAllAsVersion()
*
* @return void
*/
- public function testMarkMigratedTarget()
+ public function testMarkMigratedTarget(): void
{
$markMigrated = $this->migrations->markMigrated(null, ['target' => '20150704160200']);
$this->assertTrue($markMigrated);
@@ -394,7 +394,7 @@ public function testMarkMigratedTarget()
*
* @return void
*/
- public function testMarkMigratedTargetError()
+ public function testMarkMigratedTargetError(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Migration `20150704160610` was not found !');
@@ -407,7 +407,7 @@ public function testMarkMigratedTargetError()
*
* @return void
*/
- public function testMarkMigratedTargetExclude()
+ public function testMarkMigratedTargetExclude(): void
{
$markMigrated = $this->migrations->markMigrated(null, ['target' => '20150704160200', 'exclude' => true]);
$this->assertTrue($markMigrated);
@@ -449,7 +449,7 @@ public function testMarkMigratedTargetExclude()
*
* @return void
*/
- public function testMarkMigratedTargetOnly()
+ public function testMarkMigratedTargetOnly(): void
{
$markMigrated = $this->migrations->markMigrated(null, ['target' => '20150724233100', 'only' => true]);
$this->assertTrue($markMigrated);
@@ -491,7 +491,7 @@ public function testMarkMigratedTargetOnly()
*
* @return void
*/
- public function testMarkMigratedTargetExcludeOnly()
+ public function testMarkMigratedTargetExcludeOnly(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('You should use `exclude` OR `only` (not both) along with a `target` argument');
@@ -504,7 +504,7 @@ public function testMarkMigratedTargetExcludeOnly()
*
* @return void
*/
- public function testMarkMigratedVersion()
+ public function testMarkMigratedVersion(): void
{
$markMigrated = $this->migrations->markMigrated(20150704160200);
$this->assertTrue($markMigrated);
@@ -546,7 +546,7 @@ public function testMarkMigratedVersion()
*
* @return void
*/
- public function testOverrideOptions()
+ public function testOverrideOptions(): void
{
$result = $this->migrations->status();
$expectedStatus = [
@@ -614,7 +614,7 @@ public function testOverrideOptions()
*
* @return void
*/
- public function testMigrateDateOption()
+ public function testMigrateDateOption(): void
{
// If we want to migrate to a date before the first first migration date,
// we should not migrate anything
@@ -674,6 +674,7 @@ public function testMigrateDateOption()
// If we want to migrate to a date after the last migration date,
// we should migrate everything
$this->migrations->migrate(['date' => '20150730']);
+
$expectedStatus = [
[
'status' => 'up',
@@ -790,7 +791,7 @@ public function testMigrateDateOption()
*
* @return void
*/
- public function testSeed()
+ public function testSeed(): void
{
$this->migrations->migrate();
$seed = $this->migrations->seed(['source' => 'Seeds']);
@@ -866,7 +867,7 @@ public function testSeed()
*
* @return void
*/
- public function testSeedOneSeeder()
+ public function testSeedOneSeeder(): void
{
$this->migrations->migrate();
@@ -915,7 +916,7 @@ public function testSeedOneSeeder()
*
* @return void
*/
- public function testSeedOneSeederShortName()
+ public function testSeedOneSeederShortName(): void
{
$this->migrations->migrate();
@@ -964,7 +965,7 @@ public function testSeedOneSeederShortName()
*
* @return void
*/
- public function testSeedCallSeeder()
+ public function testSeedCallSeeder(): void
{
$this->migrations->migrate();
@@ -1025,7 +1026,7 @@ public function testSeedCallSeeder()
*
* @return void
*/
- public function testSeedWrongSeed()
+ public function testSeedWrongSeed(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The seed `DerpSeed` does not exist');
@@ -1086,7 +1087,7 @@ public function testMigrateSnapshots(string $basePath, string $filename, array $
'parts', 'special_pks', 'special_tags', 'texts', 'users',
];
foreach ($cleanup as $item) {
- $snapshotConn->execute("DROP TABLE IF EXISTS {$item}");
+ $snapshotConn->execute('DROP TABLE IF EXISTS ' . $item);
}
$migrations = new Migrations([
@@ -1103,7 +1104,7 @@ public function testMigrateSnapshots(string $basePath, string $filename, array $
/**
* Tests that migrating in case of error throws an exception
*/
- public function testMigrateErrors()
+ public function testMigrateErrors(): void
{
$this->expectException(Exception::class);
$this->migrations->markMigrated(20150704160200);
@@ -1113,7 +1114,7 @@ public function testMigrateErrors()
/**
* Tests that rolling back in case of error throws an exception
*/
- public function testRollbackErrors()
+ public function testRollbackErrors(): void
{
$this->expectException(Exception::class);
$this->migrations->markMigrated('all');
@@ -1124,7 +1125,7 @@ public function testRollbackErrors()
* Tests that marking migrated a non-existant migrations returns an error
* and can return a error message
*/
- public function testMarkMigratedErrors()
+ public function testMarkMigratedErrors(): void
{
$this->expectException(Exception::class);
$this->migrations->markMigrated(20150704000000);
diff --git a/tests/TestCase/TestCase.php b/tests/TestCase/TestCase.php
index 06b0f68ec..e9734f79c 100644
--- a/tests/TestCase/TestCase.php
+++ b/tests/TestCase/TestCase.php
@@ -39,7 +39,7 @@ abstract class TestCase extends BaseTestCase
*/
protected $generatedFiles = [];
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -47,7 +47,7 @@ public function setUp(): void
$this->loadPlugins(['Cake/TwigView']);
}
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
@@ -72,9 +72,9 @@ public function tearDown(): void
* @param string $name plugin name to load
* @return void
*/
- protected function _loadTestPlugin($name)
+ protected function _loadTestPlugin(string $name)
{
- $root = dirname(dirname(__FILE__)) . DS;
+ $root = dirname(__FILE__, 2) . DS;
$path = $root . 'test_app' . DS . 'Plugin' . DS . $name . DS;
$this->loadPlugins([
@@ -91,7 +91,7 @@ protected function _loadTestPlugin($name)
* @param string $message The message to use if a check fails.
* @return void
*/
- protected function assertFilesExist(array $files, $message = '')
+ protected function assertFilesExist(array $files, string $message = '')
{
foreach ($files as $file) {
$this->assertFileExists($file, $message);
@@ -106,7 +106,7 @@ protected function assertFilesExist(array $files, $message = '')
* @param string $message The error message.
* @return void
*/
- protected function assertFileContains($expected, $path, $message = '')
+ protected function assertFileContains(string $expected, string $path, string $message = '')
{
$this->assertFileExists($path, 'Cannot test contents, file does not exist.');
@@ -122,7 +122,7 @@ protected function assertFileContains($expected, $path, $message = '')
* @param string $message The error message.
* @return void
*/
- protected function assertFileNotContains($expected, $path, $message = '')
+ protected function assertFileNotContains(string $expected, string $path, string $message = '')
{
$this->assertFileExists($path, 'Cannot test contents, file does not exist.');
diff --git a/tests/TestCase/TestSuite/MigratorTest.php b/tests/TestCase/TestSuite/MigratorTest.php
index de3baa975..3a9804400 100644
--- a/tests/TestCase/TestSuite/MigratorTest.php
+++ b/tests/TestCase/TestSuite/MigratorTest.php
@@ -16,6 +16,7 @@
use Cake\Chronos\ChronosDate;
use Cake\Core\Configure;
use Cake\Database\Driver\Postgres;
+use Cake\Database\Schema\TableSchema;
use Cake\Datasource\ConnectionManager;
use Cake\TestSuite\ConnectionHelper;
use Cake\TestSuite\TestCase;
@@ -32,30 +33,77 @@ class MigratorTest extends TestCase
protected $restore;
/**
- * Get the migration table name for the Migrator plugin.
+ * Get the migration table name for a plugin, or for the application when null.
*
+ * @param string|null $plugin The plugin name.
* @return string
*/
- protected function getMigratorTableName(): string
+ protected function getMigratorTableName(?string $plugin = 'Migrator'): string
{
- return Configure::read('Migrations.legacyTables') === false
- ? UnifiedMigrationsTableStorage::TABLE_NAME
- : 'migrator_phinxlog';
+ if (Configure::read('Migrations.legacyTables') === false) {
+ return UnifiedMigrationsTableStorage::TABLE_NAME;
+ }
+
+ return $plugin === null ? 'phinxlog' : 'migrator_phinxlog';
}
/**
* Build a WHERE clause for filtering by plugin in unified mode.
*
+ * @param string|null $plugin The plugin name.
* @return array
*/
- protected function getMigratorWhereClause(): array
+ protected function getMigratorWhereClause(?string $plugin = 'Migrator'): array
+ {
+ if (Configure::read('Migrations.legacyTables') !== false) {
+ return [];
+ }
+
+ return $plugin === null ? ['plugin IS' => null] : ['plugin' => $plugin];
+ }
+
+ protected function makeInspectableMigrator(): Migrator
{
- return Configure::read('Migrations.legacyTables') === false
- ? ['plugin' => 'Migrator']
- : [];
+ return new class () extends Migrator {
+ /**
+ * @return array
+ */
+ public function exposedGetMigrationTables(string $connection): array
+ {
+ return array_values($this->getMigrationTables($connection));
+ }
+
+ /**
+ * @return array
+ */
+ public function exposedGetNonPhinxTables(string $connection, array $skip = []): array
+ {
+ return array_values($this->getNonPhinxTables($connection, $skip));
+ }
+ };
}
- public function setUp(): void
+ protected function createPortableTable(string $name, bool $withPlugin = false): void
+ {
+ $connection = ConnectionManager::get('test');
+ $schema = new TableSchema($name);
+ $schema
+ ->addColumn('version', ['type' => 'biginteger', 'null' => false])
+ ->addColumn('migration_name', ['type' => 'string', 'limit' => 100, 'null' => false])
+ ->addColumn('start_time', ['type' => 'timestamp', 'default' => null, 'null' => true])
+ ->addColumn('end_time', ['type' => 'timestamp', 'default' => null, 'null' => true])
+ ->addColumn('breakpoint', ['type' => 'boolean', 'default' => false, 'null' => false]);
+
+ if ($withPlugin) {
+ $schema->addColumn('plugin', ['type' => 'string', 'limit' => 100, 'default' => null, 'null' => true]);
+ }
+
+ foreach ($schema->createSql($connection) as $statement) {
+ $connection->execute($statement);
+ }
+ }
+
+ protected function setUp(): void
{
parent::setUp();
@@ -67,7 +115,7 @@ public function setUp(): void
(new ConnectionHelper())->dropTables('test');
}
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
if ($this->restore) {
@@ -95,6 +143,32 @@ public function testMigrateDropTruncate(): void
$this->assertCount(0, $connection->selectQuery()->select(['*'])->from('migrator')->execute()->fetchAll());
}
+ public function testGetMigrationTablesIncludesUnifiedLedger(): void
+ {
+ $this->skipIf(Configure::read('Migrations.legacyTables') !== false);
+
+ $connection = ConnectionManager::get('test');
+ $connection->execute('CREATE TABLE sample_table (id INT)');
+ $this->createPortableTable('cake_migrations', true);
+
+ $migrator = $this->makeInspectableMigrator();
+
+ $this->assertSame(['cake_migrations'], $migrator->exposedGetMigrationTables('test'));
+ $this->assertSame(['sample_table'], $migrator->exposedGetNonPhinxTables('test'));
+ }
+
+ public function testGetMigrationTablesIncludesLegacyLedger(): void
+ {
+ $connection = ConnectionManager::get('test');
+ $connection->execute('CREATE TABLE sample_table (id INT)');
+ $this->createPortableTable('app_phinxlog');
+
+ $migrator = $this->makeInspectableMigrator();
+
+ $this->assertSame(['app_phinxlog'], $migrator->exposedGetMigrationTables('test'));
+ $this->assertSame(['sample_table'], $migrator->exposedGetNonPhinxTables('test'));
+ }
+
public function testMigrateDropNoTruncate(): void
{
$migrator = new Migrator();
@@ -150,8 +224,6 @@ public function testRunManyMultipleSkip(): void
{
$connection = ConnectionManager::get('test');
$this->skipIf($connection->getDriver() instanceof Postgres);
- // Skip for unified mode - migration history detection works differently
- $this->skipIf(Configure::read('Migrations.legacyTables') === false);
$migrator = new Migrator();
// Run migrations for the first time.
@@ -160,6 +232,9 @@ public function testRunManyMultipleSkip(): void
['plugin' => 'Migrator', 'source' => 'Migrations2'],
]);
+ // Forget one applied migration so that the next run has a reason to drop.
+ $this->forgetMigration('20211001000000');
+
// Run migrations the second time. Skip clauses will cause problems.
try {
$migrator->runMany([
@@ -185,24 +260,35 @@ public function testTruncateAfterMigrations(): void
$this->assertCount(0, $connection->selectQuery()->select(['*'])->from('migrator')->execute()->fetchAll());
}
- private function setMigrationEndDateToYesterday()
+ private function setMigrationEndDateToYesterday(?string $plugin = 'Migrator'): void
{
$query = ConnectionManager::get('test')->updateQuery()
- ->update($this->getMigratorTableName())
+ ->update($this->getMigratorTableName($plugin))
->set('end_time', ChronosDate::yesterday(), 'timestamp');
- $where = $this->getMigratorWhereClause();
+ $where = $this->getMigratorWhereClause($plugin);
if ($where) {
$query->where($where);
}
$query->execute();
}
- private function fetchMigrationEndDate(): ChronosDate
+ private function forgetMigration(string $version): void
+ {
+ $query = ConnectionManager::get('test')->deleteQuery()
+ ->delete($this->getMigratorTableName())
+ ->where(['version' => $version]);
+ foreach ($this->getMigratorWhereClause() as $field => $value) {
+ $query->where([$field => $value]);
+ }
+ $query->execute();
+ }
+
+ private function fetchMigrationEndDate(?string $plugin = 'Migrator'): ChronosDate
{
$query = ConnectionManager::get('test')->selectQuery()
->select('end_time')
- ->from($this->getMigratorTableName());
- $where = $this->getMigratorWhereClause();
+ ->from($this->getMigratorTableName($plugin));
+ $where = $this->getMigratorWhereClause($plugin);
if ($where) {
$query->where($where);
}
@@ -255,6 +341,49 @@ public function testSkipMigrationDroppingIfOnlyUpMigrationsWithTwoSetsOfMigratio
$this->assertTrue($this->fetchMigrationEndDate()->isYesterday());
}
+ public function testSkipMigrationDroppingWithTwoSourcesSharingHistory(): void
+ {
+ // Both sets resolve to the same migration history: same connection and
+ // same plugin. Each source only sees its own files on disk, so the
+ // other source's applied migrations must not be treated as missing.
+ $sets = [
+ ['plugin' => 'Migrator'],
+ ['plugin' => 'Migrator', 'source' => 'Migrations2'],
+ ];
+
+ $migrator = new Migrator();
+ $migrator->runMany($sets, false);
+
+ $this->setMigrationEndDateToYesterday();
+
+ $migrator->runMany($sets, false);
+
+ // Ensure that the end time is unchanged, meaning that the tables were not
+ // dropped and the migrations were not re-run.
+ $this->assertTrue($this->fetchMigrationEndDate()->isYesterday());
+ }
+
+ public function testSkipMigrationDroppingWithTwoAppSourcesSharingHistory(): void
+ {
+ // Two application sources on the same connection: no plugin, so both
+ // record into the same migration history.
+ $sets = [
+ ['source' => '../Plugin/Migrator/config/Migrations'],
+ ['source' => '../Plugin/Migrator/config/Migrations2'],
+ ];
+
+ $migrator = new Migrator();
+ $migrator->runMany($sets, false);
+
+ $this->setMigrationEndDateToYesterday(null);
+
+ $migrator->runMany($sets, false);
+
+ // Ensure that the end time is unchanged, meaning that the tables were not
+ // dropped and the migrations were not re-run.
+ $this->assertTrue($this->fetchMigrationEndDate(null)->isYesterday());
+ }
+
public function testDropMigrationsIfDownMigrations(): void
{
// Skip for unified mode - migration history detection works differently
diff --git a/tests/TestCase/Util/ColumnParserTest.php b/tests/TestCase/Util/ColumnParserTest.php
index d56d0616a..5e332e32e 100644
--- a/tests/TestCase/Util/ColumnParserTest.php
+++ b/tests/TestCase/Util/ColumnParserTest.php
@@ -31,13 +31,13 @@ class ColumnParserTest extends TestCase
*
* @return void
*/
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
$this->columnParser = new ColumnParser();
}
- public function testParseFields()
+ public function testParseFields(): void
{
$this->assertEquals([
'id' => [
@@ -227,7 +227,41 @@ public function testParseFields()
$this->assertEquals($expected, $actual);
}
- public function testParseIndexes()
+ /**
+ * A composite primary key (e.g. a join table) must not auto-increment its
+ * integer columns, otherwise MySQL rejects the table with
+ * "there can be only one auto column".
+ *
+ * @return void
+ */
+ public function testParseFieldsCompositePrimaryKey(): void
+ {
+ $expected = [
+ 'article_id' => [
+ 'columnType' => 'integer',
+ 'options' => [
+ 'null' => false,
+ 'default' => null,
+ 'limit' => 11,
+ ],
+ ],
+ 'tag_id' => [
+ 'columnType' => 'integer',
+ 'options' => [
+ 'null' => false,
+ 'default' => null,
+ 'limit' => 11,
+ ],
+ ],
+ ];
+ $actual = $this->columnParser->parseFields([
+ 'article_id:integer:primary',
+ 'tag_id:integer:primary',
+ ]);
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testParseIndexes(): void
{
$this->assertEquals(['UNIQUE_ID' => [
'columns' => ['id'],
@@ -247,7 +281,7 @@ public function testParseIndexes()
]));
}
- public function testParsePrimaryKey()
+ public function testParsePrimaryKey(): void
{
$this->assertEquals(['id'], $this->columnParser->parsePrimaryKey(['id:primary']));
$this->assertEquals(['id'], $this->columnParser->parsePrimaryKey(['id:integer:primary']));
@@ -258,7 +292,7 @@ public function testParsePrimaryKey()
);
}
- public function testValidArguments()
+ public function testValidArguments(): void
{
$this->assertEquals(
['id'],
@@ -310,7 +344,7 @@ public function testValidArguments()
);
}
- public function testGetType()
+ public function testGetType(): void
{
$this->assertSame('integer', $this->columnParser->getType('id', null));
$this->assertSame('integer', $this->columnParser->getType('id', 'primary_key'));
@@ -332,7 +366,7 @@ public function testGetType()
$this->assertSame('decimal', $this->columnParser->getType('longitude', null));
}
- public function testGetTypeAndLength()
+ public function testGetTypeAndLength(): void
{
$this->assertEquals(['string', 255], $this->columnParser->getTypeAndLength('name', 'string'));
$this->assertEquals(['integer', 11], $this->columnParser->getTypeAndLength('counter', 'integer'));
@@ -346,7 +380,7 @@ public function testGetTypeAndLength()
$this->assertEquals(['decimal', [10, 6]], $this->columnParser->getTypeAndLength('latitude', 'decimal[10,6]'));
}
- public function testGetTypeAndLengthReturnsIntegerTypes()
+ public function testGetTypeAndLengthReturnsIntegerTypes(): void
{
// Test that lengths are returned as integers, not strings
[, $length] = $this->columnParser->getTypeAndLength('name', 'string[128]');
@@ -376,7 +410,7 @@ public function testGetTypeAndLengthReturnsIntegerTypes()
$this->assertSame(11, $length);
}
- public function testGetLength()
+ public function testGetLength(): void
{
$this->assertSame(255, $this->columnParser->getLength('string'));
$this->assertSame(11, $this->columnParser->getLength('integer'));
@@ -385,7 +419,7 @@ public function testGetLength()
$this->assertNull($this->columnParser->getLength('text'));
}
- public function testGetIndexName()
+ public function testGetIndexName(): void
{
$this->assertSame('SOME_INDEX', $this->columnParser->getIndexName('id', null, 'SOME_INDEX', true));
$this->assertSame('SOME_INDEX', $this->columnParser->getIndexName('id', null, 'SOME_INDEX', false));
@@ -398,7 +432,7 @@ public function testGetIndexName()
$this->assertSame('PRIMARY', $this->columnParser->getIndexName('id', 'primary', null, true));
}
- public function testParseFieldsWithReferences()
+ public function testParseFieldsWithReferences(): void
{
// Test basic references - should convert to integer
$expected = [
@@ -443,7 +477,7 @@ public function testParseFieldsWithReferences()
$this->assertEquals($expected, $actual);
}
- public function testParseFieldsWithDefaultValues()
+ public function testParseFieldsWithDefaultValues(): void
{
// Test boolean default true
$expected = [
@@ -557,7 +591,7 @@ public function testParseFieldsWithDefaultValues()
$this->assertEquals($expected, $actual);
}
- public function testParseDefaultValue()
+ public function testParseDefaultValue(): void
{
// Test null and empty values
$this->assertNull($this->columnParser->parseDefaultValue(null, 'string'));
@@ -588,7 +622,7 @@ public function testParseDefaultValue()
$this->assertSame('CURRENT_TIMESTAMP', $this->columnParser->parseDefaultValue('CURRENT_TIMESTAMP', 'datetime'));
}
- public function testParseIndexesWithDefaultValues()
+ public function testParseIndexesWithDefaultValues(): void
{
// Ensure indexes still work with default values in the definition
$expected = [
@@ -611,7 +645,7 @@ public function testParseIndexesWithDefaultValues()
$this->assertEquals($expected, $actual);
}
- public function testValidArgumentsWithDefaultValues()
+ public function testValidArgumentsWithDefaultValues(): void
{
$this->assertEquals(
['active:boolean:default[true]'],
@@ -627,7 +661,7 @@ public function testValidArgumentsWithDefaultValues()
);
}
- public function testParseForeignKeys()
+ public function testParseForeignKeys(): void
{
// Test basic reference - infer table name from field
$expected = [
diff --git a/tests/TestCase/Util/UtilTest.php b/tests/TestCase/Util/UtilTest.php
index c017c7641..6c9aed720 100644
--- a/tests/TestCase/Util/UtilTest.php
+++ b/tests/TestCase/Util/UtilTest.php
@@ -12,12 +12,12 @@
class UtilTest extends TestCase
{
- private function getCorrectedPath($path)
+ private function getCorrectedPath(string $path): string
{
return str_replace('/', DIRECTORY_SEPARATOR, $path);
}
- public function testGetExistingMigrationClassNames()
+ public function testGetExistingMigrationClassNames(): void
{
$expectedResults = [
'TestMigration',
@@ -31,14 +31,14 @@ public function testGetExistingMigrationClassNames()
}
}
- public function testGetExistingMigrationClassNamesWithFile()
+ public function testGetExistingMigrationClassNamesWithFile(): void
{
$file = $this->getCorrectedPath(__DIR__ . '/_files/migrations/20120111235330_test_migration.php');
$existingClassNames = Util::getExistingMigrationClassNames($file);
$this->assertCount(0, $existingClassNames);
}
- public function testGetCurrentTimestamp()
+ public function testGetCurrentTimestamp(): void
{
$dt = new DateTime('now', new DateTimeZone('UTC'));
$expected = $dt->format(Util::DATE_FORMAT);
@@ -104,7 +104,7 @@ public static function providerMapFileName(): array
}
#[DataProvider('providerMapFileName')]
- public function testMapFileNameToClassName(string $fileName, string $className)
+ public function testMapFileNameToClassName(string $fileName, string $className): void
{
$this->assertEquals($className, Util::mapFileNameToClassName($fileName));
}
@@ -117,7 +117,7 @@ public function testMapReadableFileNameToClassName(): void
$this->assertEquals('DropOrdersTable', Util::mapFileNameToClassName('2024_01_01_000000_DropOrdersTable.php'));
}
- public function testGlobPath()
+ public function testGlobPath(): void
{
$files = Util::glob(__DIR__ . '/_files/migrations/empty.txt');
$this->assertCount(1, $files);
@@ -130,7 +130,7 @@ public function testGlobPath()
$this->assertEquals('not_a_migration.php', basename($files[2]));
}
- public function testGlobAll()
+ public function testGlobAll(): void
{
$files = Util::globAll([
__DIR__ . '/_files/migrations/*.php',
@@ -144,7 +144,7 @@ public function testGlobAll()
$this->assertEquals('empty.txt', basename($files[3]));
}
- public function testGetFiles()
+ public function testGetFiles(): void
{
$files = Util::getFiles([
__DIR__ . '/_files/migrations',
diff --git a/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php b/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php
index fd8abe276..69edb0937 100644
--- a/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php
+++ b/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php
@@ -8,7 +8,7 @@ class TestMigration extends BaseMigration
/**
* Migrate Up.
*/
- public function up()
+ public function up(): void
{
// do nothing
}
@@ -16,7 +16,7 @@ public function up()
/**
* Migrate Down.
*/
- public function down()
+ public function down(): void
{
// do nothing
}
diff --git a/tests/TestCase/View/Helper/MigrationHelperTest.php b/tests/TestCase/View/Helper/MigrationHelperTest.php
index c78009360..76b1974c5 100644
--- a/tests/TestCase/View/Helper/MigrationHelperTest.php
+++ b/tests/TestCase/View/Helper/MigrationHelperTest.php
@@ -16,9 +16,11 @@
use Cake\Database\Driver\Mysql;
use Cake\Database\Driver\Sqlserver;
use Cake\Database\Schema\Collection;
+use Cake\Database\Schema\TableSchema;
use Cake\Datasource\ConnectionManager;
use Cake\TestSuite\TestCase;
use Cake\View\View;
+use Migrations\Db\Adapter\MysqlAdapter;
use Migrations\View\Helper\MigrationHelper;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
@@ -74,7 +76,7 @@ class MigrationHelperTest extends TestCase
*
* @return void
*/
- public function setUp(): void
+ protected function setUp(): void
{
parent::setUp();
@@ -139,34 +141,34 @@ public function setUp(): void
*
* @return void
*/
- public function tearDown(): void
+ protected function tearDown(): void
{
parent::tearDown();
unset($this->helper, $this->view, $this->collection, $this->connection);
}
- public function testTableMethod()
+ public function testTableMethod(): void
{
$this->assertSame('drop', $this->helper->tableMethod('drop_table'));
$this->assertSame('create', $this->helper->tableMethod('create_table'));
$this->assertSame('update', $this->helper->tableMethod('other_method'));
}
- public function testIndexMethod()
+ public function testIndexMethod(): void
{
$this->assertSame('removeIndex', $this->helper->indexMethod('drop_field'));
$this->assertSame('addIndex', $this->helper->indexMethod('add_field'));
$this->assertSame('addIndex', $this->helper->indexMethod('alter_field'));
}
- public function testColumnMethod()
+ public function testColumnMethod(): void
{
$this->assertSame('removeColumn', $this->helper->columnMethod('drop_field'));
$this->assertSame('addColumn', $this->helper->columnMethod('add_field'));
$this->assertSame('changeColumn', $this->helper->columnMethod('alter_field'));
}
- public function testColumns()
+ public function testColumns(): void
{
$extra = [];
if ($this->connection->getDriver() instanceof Sqlserver) {
@@ -216,7 +218,7 @@ public function testColumns()
], $this->helper->columns('users'));
}
- public function testColumn()
+ public function testColumn(): void
{
$tableSchema = $this->collection->describe('users');
@@ -287,7 +289,7 @@ public function testColumn()
], $this->helper->column($tableSchema, 'updated'));
}
- public function testValue()
+ public function testValue(): void
{
$this->assertSame('null', $this->helper->value(null));
$this->assertSame('null', $this->helper->value('null'));
@@ -307,7 +309,7 @@ public function testValue()
$this->assertSame("'o\\\"ne'", $this->helper->value('o"ne'));
}
- public function testAttributes()
+ public function testAttributes(): void
{
$attributes = [
'null' => false,
@@ -379,7 +381,58 @@ public function testAttributes()
$this->assertEquals($attributes, $result);
}
- public function testStringifyList()
+ /**
+ * Test that attributes() preserves the onUpdate attribute
+ *
+ * `onUpdate` is a first-class column key in CakePHP's TableSchema for datetime
+ * and timestamp types, so attributes() must not filter it out. The schema is
+ * built by hand rather than reflected so this holds for every driver.
+ */
+ public function testAttributesPreservesOnUpdate(): void
+ {
+ $tableSchema = new TableSchema('on_update_columns');
+ $tableSchema->addColumn('modified', [
+ 'type' => 'datetime',
+ 'null' => false,
+ 'onUpdate' => 'CURRENT_TIMESTAMP',
+ ]);
+ $tableSchema->addColumn('plain', [
+ 'type' => 'datetime',
+ 'null' => true,
+ ]);
+
+ $modified = $this->helper->attributes($tableSchema, 'modified');
+ $this->assertArrayHasKey('onUpdate', $modified, 'onUpdate should survive attributes()');
+ $this->assertSame('CURRENT_TIMESTAMP', $modified['onUpdate']);
+
+ $plain = $this->helper->attributes($tableSchema, 'plain');
+ $this->assertArrayNotHasKey('onUpdate', $plain, 'columns without ON UPDATE should not gain the key');
+ }
+
+ /**
+ * Test that a column with an ON UPDATE clause bakes the Phinx `update` option
+ *
+ * Guards the whole snapshot path: attributes() must preserve `onUpdate` and
+ * getColumnOption() must translate it to Phinx's `update` option. Either half
+ * regressing silently drops the clause from generated migrations.
+ */
+ public function testColumnsRendersOnUpdateAsUpdateOption(): void
+ {
+ $tableSchema = new TableSchema('on_update_columns');
+ $tableSchema->addColumn('modified', [
+ 'type' => 'datetime',
+ 'null' => false,
+ 'onUpdate' => 'CURRENT_TIMESTAMP',
+ ]);
+
+ $columns = $this->helper->columns($tableSchema);
+ $options = $this->helper->getColumnOption($columns['modified']['options']);
+
+ $this->assertArrayNotHasKey('onUpdate', $options);
+ $this->assertSame('CURRENT_TIMESTAMP', $options['update']);
+ }
+
+ public function testStringifyList(): void
{
$this->assertSame('', $this->helper->stringifyList([]));
$this->assertSame("
@@ -456,4 +509,117 @@ public function testGetColumnOptionConvertsCollateToCollation(): void
$this->assertArrayHasKey('collation', $result, 'collation should be set from collate value');
$this->assertSame('en_US.UTF-8', $result['collation']);
}
+
+ /**
+ * Test that getColumnOption converts onUpdate to update
+ *
+ * CakePHP reflects `ON UPDATE` clauses as 'onUpdate', but Phinx uses the
+ * 'update' column option, so this must be converted for the clause to
+ * survive a snapshot.
+ */
+ public function testGetColumnOptionConvertsOnUpdateToUpdate(): void
+ {
+ $options = [
+ 'null' => true,
+ 'default' => null,
+ 'onUpdate' => 'CURRENT_TIMESTAMP',
+ ];
+
+ $result = $this->helper->getColumnOption($options);
+
+ $this->assertArrayNotHasKey('onUpdate', $result, 'onUpdate should be converted to update');
+ $this->assertArrayHasKey('update', $result, 'update should be set from onUpdate value');
+ $this->assertSame('CURRENT_TIMESTAMP', $result['update']);
+ }
+
+ /**
+ * Test that getColumnOption removes null onUpdate
+ *
+ * Column::toArray() always emits an 'onUpdate' key, so columns without an
+ * `ON UPDATE` clause carry a null. Column::setUpdate() is not nullable, so
+ * passing it through would be a TypeError.
+ */
+ public function testGetColumnOptionRemovesNullOnUpdate(): void
+ {
+ $options = [
+ 'null' => true,
+ 'default' => null,
+ 'onUpdate' => null,
+ ];
+
+ $result = $this->helper->getColumnOption($options);
+
+ $this->assertArrayNotHasKey('onUpdate', $result, 'onUpdate => null should be removed');
+ $this->assertArrayNotHasKey('update', $result, 'update should not be set when onUpdate is null');
+ }
+
+ /**
+ * Test that getColumnOption includes the fixed option for binary columns
+ */
+ public function testGetColumnOptionIncludesFixed(): void
+ {
+ $options = [
+ 'length' => 20,
+ 'null' => true,
+ 'default' => null,
+ 'fixed' => true,
+ ];
+
+ $result = $this->helper->getColumnOption($options);
+
+ $this->assertArrayHasKey('fixed', $result);
+ $this->assertTrue($result['fixed']);
+ }
+
+ /**
+ * Test that getColumnOption excludes fixed when not set
+ */
+ public function testGetColumnOptionExcludesFixedWhenNotSet(): void
+ {
+ $options = [
+ 'length' => 20,
+ 'null' => true,
+ 'default' => null,
+ ];
+
+ $result = $this->helper->getColumnOption($options);
+
+ $this->assertArrayNotHasKey('fixed', $result);
+ }
+
+ /**
+ * Test that getColumnOption converts CakePHP's LENGTH_LONG to migrations TEXT_LONG
+ *
+ * CakePHP uses LENGTH_LONG = 4294967295 for LONGTEXT, but migrations expects
+ * TEXT_LONG = 2147483647. This ensures generated migrations use the correct value.
+ */
+ public function testGetColumnOptionConvertsLengthLongToTextLong(): void
+ {
+ $options = [
+ 'limit' => TableSchema::LENGTH_LONG, // 4294967295
+ 'null' => true,
+ 'default' => null,
+ ];
+
+ $result = $this->helper->getColumnOption($options);
+
+ $this->assertArrayHasKey('limit', $result);
+ $this->assertSame(MysqlAdapter::TEXT_LONG, $result['limit']); // 2147483647
+ }
+
+ /**
+ * Test that getColumnOption preserves other limit values unchanged
+ */
+ public function testGetColumnOptionPreservesOtherLimits(): void
+ {
+ $options = [
+ 'limit' => 255, // TEXT_TINY / LENGTH_TINY - same value
+ 'null' => true,
+ 'default' => null,
+ ];
+
+ $result = $this->helper->getColumnOption($options);
+
+ $this->assertSame(255, $result['limit']);
+ }
}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index cf4caad6f..f92d9157b 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -24,10 +24,10 @@
use Migrator\MigratorPlugin;
use function Cake\Core\env;
-$findRoot = function ($root) {
+$findRoot = function ($root): string {
do {
$lastRoot = $root;
- $root = dirname($root);
+ $root = dirname((string)$root);
if (is_dir($root . '/vendor/cakephp/cakephp')) {
return $root;
}
@@ -38,7 +38,7 @@
unset($findRoot);
chdir($root);
-require_once 'vendor/autoload.php';
+require_once __DIR__ . '/../vendor/autoload.php';
if (!defined('DS')) {
define('DS', DIRECTORY_SEPARATOR);
diff --git a/tests/comparisons/Create/TestCreateChange.php b/tests/comparisons/Create/TestCreateChange.php
index 071aa889f..b45fa96cb 100644
--- a/tests/comparisons/Create/TestCreateChange.php
+++ b/tests/comparisons/Create/TestCreateChange.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class TestCreateChange extends BaseMigration
+class TestCreateChange extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
* @return void
*/
public function change(): void
diff --git a/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php b/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php
index a6dd8bf6a..f223ba5f9 100644
--- a/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php
+++ b/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TheDiffAddRemoveMysql extends BaseMigration
+class TheDiffAddRemoveMysql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -41,7 +42,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php b/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php
index 28fda29b9..b30bb8ef1 100644
--- a/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php
+++ b/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TheDiffAddRemovePgsql extends BaseMigration
+class TheDiffAddRemovePgsql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -33,7 +34,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php b/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php
index 984829c9c..42a958bb2 100644
--- a/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php
+++ b/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class InitialDecimalChangeMysql extends BaseMigration
+class InitialDecimalChangeMysql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -29,7 +30,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php b/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php
index a32449e17..940f89b62 100644
--- a/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php
+++ b/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TheDiffDecimalChangeMysql extends BaseMigration
+class TheDiffDecimalChangeMysql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -36,7 +37,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock b/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock
index 58fa3362e..9d56436ca 100644
Binary files a/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock and b/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock differ
diff --git a/tests/comparisons/Diff/default/the_diff_default_mysql.php b/tests/comparisons/Diff/default/the_diff_default_mysql.php
index a889d9e7c..02f8bc5c5 100644
--- a/tests/comparisons/Diff/default/the_diff_default_mysql.php
+++ b/tests/comparisons/Diff/default/the_diff_default_mysql.php
@@ -2,21 +2,22 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TheDiffDefaultMysql extends BaseMigration
+class TheDiffDefaultMysql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
public function up(): void
{
$this->table('articles')
- ->dropForeignKey([], 'articles_ibfk_1')
+ ->dropForeignKey([], 'articles_user_id')
->removeIndexByName('UNIQUE_SLUG')
->removeIndexByName('rating_index')
->removeIndexByName('BY_NAME')
@@ -86,7 +87,7 @@ public function up(): void
])
->addIndex(
$this->index('user_id')
- ->setName('categories_ibfk_1')
+ ->setName('categories_user_id')
)
->addIndex(
$this->index('name')
@@ -99,9 +100,9 @@ public function up(): void
$this->foreignKey('user_id')
->setReferencedTable('users')
->setReferencedColumns('id')
- ->setOnDelete('RESTRICT')
- ->setOnUpdate('RESTRICT')
- ->setName('categories_ibfk_1')
+ ->setDelete('RESTRICT')
+ ->setUpdate('RESTRICT')
+ ->setName('categories_user_id')
)
->update();
@@ -139,7 +140,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
@@ -232,9 +233,9 @@ public function down(): void
$this->foreignKey('user_id')
->setReferencedTable('users')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
- ->setName('articles_ibfk_1')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
+ ->setName('articles_user_id')
)
->update();
diff --git a/tests/comparisons/Diff/default/the_diff_default_pgsql.php b/tests/comparisons/Diff/default/the_diff_default_pgsql.php
index 54b61d85f..a604f9477 100644
--- a/tests/comparisons/Diff/default/the_diff_default_pgsql.php
+++ b/tests/comparisons/Diff/default/the_diff_default_pgsql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TheDiffDefaultPgsql extends BaseMigration
+class TheDiffDefaultPgsql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -64,8 +65,8 @@ public function up(): void
$this->foreignKey('user_id')
->setReferencedTable('users')
->setReferencedColumns('id')
- ->setOnDelete('RESTRICT')
- ->setOnUpdate('RESTRICT')
+ ->setDelete('RESTRICT')
+ ->setUpdate('RESTRICT')
)
->update();
@@ -114,8 +115,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
)
->update();
@@ -126,7 +127,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
@@ -207,8 +208,8 @@ public function down(): void
$this->foreignKey('user_id')
->setReferencedTable('users')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
)
->update();
diff --git a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php
index 93edeb6d8..5a65ce86d 100644
--- a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php
+++ b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TheDiffSimpleMysql extends BaseMigration
+class TheDiffSimpleMysql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -70,9 +71,9 @@ public function up(): void
$this->foreignKey('user_id')
->setReferencedTable('users')
->setReferencedColumns('id')
- ->setOnDelete('RESTRICT')
- ->setOnUpdate('RESTRICT')
- ->setName('articles_ibfk_1')
+ ->setDelete('RESTRICT')
+ ->setUpdate('RESTRICT')
+ ->setName('articles_user_id')
)
->update();
}
@@ -81,7 +82,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php b/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php
index 948bcc954..4c3559117 100644
--- a/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php
+++ b/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TheDiffSimplePgsql extends BaseMigration
+class TheDiffSimplePgsql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -62,7 +63,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php b/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php
index 4e5e45848..e1c5641e1 100644
--- a/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php
+++ b/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TheDiffWithAutoIdCompatibleSignedPrimaryKeysMysql extends BaseMigration
+class TheDiffWithAutoIdCompatibleSignedPrimaryKeysMysql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -25,7 +26,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php b/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php
index 1a645545d..18523539e 100644
--- a/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php
+++ b/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TheDiffWithAutoIdIncompatibleSignedPrimaryKeysMysql extends BaseMigration
+class TheDiffWithAutoIdIncompatibleSignedPrimaryKeysMysql extends BaseMigration implements DirectionalMigrationInterface
{
public bool $autoId = false;
@@ -11,7 +12,7 @@ class TheDiffWithAutoIdIncompatibleSignedPrimaryKeysMysql extends BaseMigration
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -35,7 +36,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php b/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php
index 5d06abc3e..a43b4970a 100644
--- a/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php
+++ b/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TheDiffWithAutoIdIncompatibleUnsignedPrimaryKeysMysql extends BaseMigration
+class TheDiffWithAutoIdIncompatibleUnsignedPrimaryKeysMysql extends BaseMigration implements DirectionalMigrationInterface
{
public bool $autoId = false;
@@ -11,7 +12,7 @@ class TheDiffWithAutoIdIncompatibleUnsignedPrimaryKeysMysql extends BaseMigratio
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -35,7 +36,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php b/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php
index eca100cc3..76dce67f9 100644
--- a/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php
+++ b/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotAutoIdDisabledPgsql extends BaseMigration
+class TestSnapshotAutoIdDisabledPgsql extends BaseMigration implements DirectionalMigrationInterface
{
public bool $autoId = false;
@@ -11,7 +12,7 @@ class TestSnapshotAutoIdDisabledPgsql extends BaseMigration
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -370,8 +371,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -387,8 +388,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -398,8 +399,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -409,7 +410,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php b/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php
index e297c18ce..898c63076 100644
--- a/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php
+++ b/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotNotEmptyPgsql extends BaseMigration
+class TestSnapshotNotEmptyPgsql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -310,8 +311,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -327,8 +328,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -338,8 +339,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -349,7 +350,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php b/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php
index 2541ce034..c6a0c149f 100644
--- a/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php
+++ b/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotPluginBlogPgsql extends BaseMigration
+class TestSnapshotPluginBlogPgsql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -310,8 +311,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -327,8 +328,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -338,8 +339,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -349,7 +350,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php b/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php
index 1191bd8c9..7164b99fc 100644
--- a/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php
+++ b/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class TestSnapshotWithChangePgsql extends BaseMigration
+class TestSnapshotWithChangePgsql extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
@@ -310,8 +311,8 @@ public function change(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -327,8 +328,8 @@ public function change(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -338,8 +339,8 @@ public function change(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
diff --git a/tests/comparisons/Migration/sqlite/test_snapshot_auto_id_disabled_sqlite.php b/tests/comparisons/Migration/sqlite/test_snapshot_auto_id_disabled_sqlite.php
index 24da5e35f..65f678ea0 100644
--- a/tests/comparisons/Migration/sqlite/test_snapshot_auto_id_disabled_sqlite.php
+++ b/tests/comparisons/Migration/sqlite/test_snapshot_auto_id_disabled_sqlite.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotAutoIdDisabledSqlite extends BaseMigration
+class TestSnapshotAutoIdDisabledSqlite extends BaseMigration implements DirectionalMigrationInterface
{
public bool $autoId = false;
@@ -11,7 +12,7 @@ class TestSnapshotAutoIdDisabledSqlite extends BaseMigration
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -351,8 +352,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -368,8 +369,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -379,8 +380,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -390,7 +391,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php b/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php
index e292c18e3..b0d7b00e6 100644
--- a/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php
+++ b/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotNotEmptySqlite extends BaseMigration
+class TestSnapshotNotEmptySqlite extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -291,8 +292,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -308,8 +309,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -319,8 +320,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -330,7 +331,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php b/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php
index 78ae984e4..58dee0c5d 100644
--- a/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php
+++ b/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotPluginBlogSqlite extends BaseMigration
+class TestSnapshotPluginBlogSqlite extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -291,8 +292,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -308,8 +309,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -319,8 +320,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -330,7 +331,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php b/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php
index f1fbd6eec..3f424a457 100644
--- a/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php
+++ b/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class TestSnapshotWithChangeSqlite extends BaseMigration
+class TestSnapshotWithChangeSqlite extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
@@ -291,8 +292,8 @@ public function change(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -308,8 +309,8 @@ public function change(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -319,8 +320,8 @@ public function change(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
diff --git a/tests/comparisons/Migration/sqlserver/test_snapshot_auto_id_disabled_sqlserver.php b/tests/comparisons/Migration/sqlserver/test_snapshot_auto_id_disabled_sqlserver.php
index 347b5cdd5..42944cfb2 100644
--- a/tests/comparisons/Migration/sqlserver/test_snapshot_auto_id_disabled_sqlserver.php
+++ b/tests/comparisons/Migration/sqlserver/test_snapshot_auto_id_disabled_sqlserver.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotAutoIdDisabledSqlserver extends BaseMigration
+class TestSnapshotAutoIdDisabledSqlserver extends BaseMigration implements DirectionalMigrationInterface
{
public bool $autoId = false;
@@ -11,7 +12,7 @@ class TestSnapshotAutoIdDisabledSqlserver extends BaseMigration
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -386,8 +387,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -403,8 +404,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -414,8 +415,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -425,7 +426,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php b/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php
index e0aad74e8..ef05845e7 100644
--- a/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php
+++ b/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotNotEmptySqlserver extends BaseMigration
+class TestSnapshotNotEmptySqlserver extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -326,8 +327,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -343,8 +344,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -354,8 +355,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -365,7 +366,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php b/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php
index c8d0e8dbf..defb754ec 100644
--- a/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php
+++ b/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotPluginBlogSqlserver extends BaseMigration
+class TestSnapshotPluginBlogSqlserver extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -326,8 +327,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -343,8 +344,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -354,8 +355,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -365,7 +366,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php b/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php
index 36ef3a9b1..39505971f 100644
--- a/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php
+++ b/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class TestSnapshotWithChangeSqlserver extends BaseMigration
+class TestSnapshotWithChangeSqlserver extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
@@ -326,8 +327,8 @@ public function change(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -343,8 +344,8 @@ public function change(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -354,8 +355,8 @@ public function change(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
diff --git a/tests/comparisons/Migration/testAddFieldWithReference.php b/tests/comparisons/Migration/testAddFieldWithReference.php
index b50ad2f92..0e69918d5 100644
--- a/tests/comparisons/Migration/testAddFieldWithReference.php
+++ b/tests/comparisons/Migration/testAddFieldWithReference.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class AddCategoryIdToProducts extends BaseMigration
+class AddCategoryIdToProducts extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
@@ -25,8 +26,8 @@ public function change(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('fk_category_id')
);
$table->update();
diff --git a/tests/comparisons/Migration/testCreate.php b/tests/comparisons/Migration/testCreate.php
index 39bc88392..5e0e77285 100644
--- a/tests/comparisons/Migration/testCreate.php
+++ b/tests/comparisons/Migration/testCreate.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class CreateUsers extends BaseMigration
+class CreateUsers extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/testCreateDatetime.php b/tests/comparisons/Migration/testCreateDatetime.php
index 0547f6851..ef3889b43 100644
--- a/tests/comparisons/Migration/testCreateDatetime.php
+++ b/tests/comparisons/Migration/testCreateDatetime.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class CreateUsers extends BaseMigration
+class CreateUsers extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/testCreateDropMigration.php b/tests/comparisons/Migration/testCreateDropMigration.php
index a7550b664..b1f1accc5 100644
--- a/tests/comparisons/Migration/testCreateDropMigration.php
+++ b/tests/comparisons/Migration/testCreateDropMigration.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class DropUsers extends BaseMigration
+class DropUsers extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/testCreateFieldLength.php b/tests/comparisons/Migration/testCreateFieldLength.php
index 43220d035..82b31e1f3 100644
--- a/tests/comparisons/Migration/testCreateFieldLength.php
+++ b/tests/comparisons/Migration/testCreateFieldLength.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class CreateUsers extends BaseMigration
+class CreateUsers extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/testCreatePrimaryKey.php b/tests/comparisons/Migration/testCreatePrimaryKey.php
index 44efb2631..30c702684 100644
--- a/tests/comparisons/Migration/testCreatePrimaryKey.php
+++ b/tests/comparisons/Migration/testCreatePrimaryKey.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class CreateUsers extends BaseMigration
+class CreateUsers extends BaseMigration implements ReversibleMigrationInterface
{
public bool $autoId = false;
@@ -11,7 +12,7 @@ class CreateUsers extends BaseMigration
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php b/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php
index de8c21792..5abb519d2 100644
--- a/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php
+++ b/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class CreateUsers extends BaseMigration
+class CreateUsers extends BaseMigration implements ReversibleMigrationInterface
{
public bool $autoId = false;
@@ -11,7 +12,7 @@ class CreateUsers extends BaseMigration
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/testCreateWithReferences.php b/tests/comparisons/Migration/testCreateWithReferences.php
index 11c01ca2b..f8d081dc4 100644
--- a/tests/comparisons/Migration/testCreateWithReferences.php
+++ b/tests/comparisons/Migration/testCreateWithReferences.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class CreatePosts extends BaseMigration
+class CreatePosts extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
@@ -30,8 +31,8 @@ public function change(): void
$this->foreignKey('user_id')
->setReferencedTable('users')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('fk_user_id')
);
$table->create();
diff --git a/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php b/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php
index a4829f3c2..5d48963d7 100644
--- a/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php
+++ b/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class CreateArticles extends BaseMigration
+class CreateArticles extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
@@ -30,8 +31,8 @@ public function change(): void
$this->foreignKey('author_id')
->setReferencedTable('authors')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('fk_author_id')
);
$table->create();
diff --git a/tests/comparisons/Migration/testNoContents.php b/tests/comparisons/Migration/testNoContents.php
index 19c2524b5..85369dd26 100644
--- a/tests/comparisons/Migration/testNoContents.php
+++ b/tests/comparisons/Migration/testNoContents.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class NoContents extends BaseMigration
+class NoContents extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php b/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php
index 9024c29ad..3999820c9 100644
--- a/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php
+++ b/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotAutoIdDisabled extends BaseMigration
+class TestSnapshotAutoIdDisabled extends BaseMigration implements DirectionalMigrationInterface
{
public bool $autoId = false;
@@ -11,7 +12,7 @@ class TestSnapshotAutoIdDisabled extends BaseMigration
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -376,8 +377,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -393,8 +394,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -404,8 +405,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -415,7 +416,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/test_snapshot_not_empty.php b/tests/comparisons/Migration/test_snapshot_not_empty.php
index 1a277d711..e513a2b71 100644
--- a/tests/comparisons/Migration/test_snapshot_not_empty.php
+++ b/tests/comparisons/Migration/test_snapshot_not_empty.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotNotEmpty extends BaseMigration
+class TestSnapshotNotEmpty extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -308,8 +309,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -325,8 +326,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -336,8 +337,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -347,7 +348,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/test_snapshot_plugin_blog.php b/tests/comparisons/Migration/test_snapshot_plugin_blog.php
index 20dd3fba9..bca19c803 100644
--- a/tests/comparisons/Migration/test_snapshot_plugin_blog.php
+++ b/tests/comparisons/Migration/test_snapshot_plugin_blog.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotPluginBlog extends BaseMigration
+class TestSnapshotPluginBlog extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -308,8 +309,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -325,8 +326,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -336,8 +337,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -347,7 +348,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php b/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php
index 51b0002a4..6436c28d4 100644
--- a/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php
+++ b/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotPostgresTimestampTzPgsql extends BaseMigration
+class TestSnapshotPostgresTimestampTzPgsql extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -320,8 +321,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -337,8 +338,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -348,8 +349,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -359,7 +360,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php b/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php
index df2ea8ba0..1e81c8ab5 100644
--- a/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php
+++ b/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotWithAutoIdCompatibleSignedPrimaryKeys extends BaseMigration
+class TestSnapshotWithAutoIdCompatibleSignedPrimaryKeys extends BaseMigration implements DirectionalMigrationInterface
{
public bool $autoId = false;
@@ -11,7 +12,7 @@ class TestSnapshotWithAutoIdCompatibleSignedPrimaryKeys extends BaseMigration
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -375,8 +376,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -392,8 +393,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -403,8 +404,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -414,7 +415,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php b/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php
index 11fadb44f..dfd208926 100644
--- a/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php
+++ b/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotWithAutoIdIncompatibleSignedPrimaryKeys extends BaseMigration
+class TestSnapshotWithAutoIdIncompatibleSignedPrimaryKeys extends BaseMigration implements DirectionalMigrationInterface
{
public bool $autoId = false;
@@ -11,7 +12,7 @@ class TestSnapshotWithAutoIdIncompatibleSignedPrimaryKeys extends BaseMigration
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -375,8 +376,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -392,8 +393,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -403,8 +404,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -414,7 +415,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_unsigned_primary_keys.php b/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_unsigned_primary_keys.php
index 93570e1ec..c9047700d 100644
--- a/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_unsigned_primary_keys.php
+++ b/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_unsigned_primary_keys.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotWithAutoIdIncompatibleUnsignedPrimaryKeys extends BaseMigration
+class TestSnapshotWithAutoIdIncompatibleUnsignedPrimaryKeys extends BaseMigration implements DirectionalMigrationInterface
{
public bool $autoId = false;
@@ -11,7 +12,7 @@ class TestSnapshotWithAutoIdIncompatibleUnsignedPrimaryKeys extends BaseMigratio
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -376,8 +377,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -393,8 +394,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -404,8 +405,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -415,7 +416,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/test_snapshot_with_change.php b/tests/comparisons/Migration/test_snapshot_with_change.php
index ec5f12a64..2a9d8df65 100644
--- a/tests/comparisons/Migration/test_snapshot_with_change.php
+++ b/tests/comparisons/Migration/test_snapshot_with_change.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\ReversibleMigrationInterface;
-class TestSnapshotWithChange extends BaseMigration
+class TestSnapshotWithChange extends BaseMigration implements ReversibleMigrationInterface
{
/**
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*
* @return void
*/
@@ -308,8 +309,8 @@ public function change(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -325,8 +326,8 @@ public function change(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -336,8 +337,8 @@ public function change(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
diff --git a/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php b/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php
index 45cad82a0..c75c5e721 100644
--- a/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php
+++ b/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php
@@ -2,14 +2,15 @@
declare(strict_types=1);
use Migrations\BaseMigration;
+use Migrations\DirectionalMigrationInterface;
-class TestSnapshotWithNonDefaultCollation extends BaseMigration
+class TestSnapshotWithNonDefaultCollation extends BaseMigration implements DirectionalMigrationInterface
{
/**
* Up Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method
*
* @return void
*/
@@ -309,8 +310,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('NO_ACTION')
- ->setOnUpdate('NO_ACTION')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
->setName('articles_category_fk')
)
->update();
@@ -326,8 +327,8 @@ public function up(): void
'category_id',
'id',
])
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('orders_product_fk')
)
->update();
@@ -337,8 +338,8 @@ public function up(): void
$this->foreignKey('category_id')
->setReferencedTable('categories')
->setReferencedColumns('id')
- ->setOnDelete('CASCADE')
- ->setOnUpdate('CASCADE')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
->setName('products_category_fk')
)
->update();
@@ -348,7 +349,7 @@ public function up(): void
* Down Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
*
* @return void
*/
diff --git a/tests/comparisons/Migration/test_snapshot_with_on_update.php b/tests/comparisons/Migration/test_snapshot_with_on_update.php
new file mode 100644
index 000000000..85d284b60
--- /dev/null
+++ b/tests/comparisons/Migration/test_snapshot_with_on_update.php
@@ -0,0 +1,388 @@
+table('articles')
+ ->addColumn('title', 'string', [
+ 'comment' => 'Article title',
+ 'default' => null,
+ 'limit' => 255,
+ 'null' => true,
+ ])
+ ->addColumn('category_id', 'integer', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ 'signed' => false,
+ ])
+ ->addColumn('product_id', 'integer', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ 'signed' => false,
+ ])
+ ->addColumn('note', 'string', [
+ 'default' => '7.4',
+ 'limit' => 255,
+ 'null' => true,
+ ])
+ ->addColumn('counter', 'integer', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ 'signed' => false,
+ ])
+ ->addColumn('active', 'boolean', [
+ 'default' => false,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->addColumn('created', 'timestamp', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->addColumn('modified', 'timestamp', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->addIndex(
+ $this->index('category_id')
+ ->setName('articles_category_fk')
+ )
+ ->addIndex(
+ $this->index('title')
+ ->setName('articles_title_idx')
+ )
+ ->create();
+
+ $this->table('categories')
+ ->addColumn('parent_id', 'integer', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ 'signed' => false,
+ ])
+ ->addColumn('title', 'string', [
+ 'default' => null,
+ 'limit' => 255,
+ 'null' => true,
+ ])
+ ->addColumn('slug', 'string', [
+ 'default' => null,
+ 'limit' => 100,
+ 'null' => true,
+ ])
+ ->addColumn('created', 'timestamp', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->addColumn('modified', 'timestamp', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->addIndex(
+ $this->index('slug')
+ ->setName('categories_slug_unique')
+ ->setType('unique')
+ )
+ ->create();
+
+ $this->table('composite_pks', ['id' => false, 'primary_key' => ['id', 'name']])
+ ->addColumn('id', 'uuid', [
+ 'default' => 'a4950df3-515f-474c-be4c-6a027c1957e7',
+ 'limit' => null,
+ 'null' => false,
+ ])
+ ->addColumn('name', 'string', [
+ 'default' => '',
+ 'limit' => 10,
+ 'null' => false,
+ ])
+ ->create();
+
+ $this->table('events')
+ ->addColumn('title', 'string', [
+ 'default' => null,
+ 'limit' => 255,
+ 'null' => true,
+ ])
+ ->addColumn('description', 'text', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->addColumn('published', 'string', [
+ 'default' => 'N',
+ 'limit' => 1,
+ 'null' => true,
+ ])
+ ->create();
+
+ $this->table('orders')
+ ->addColumn('product_category', 'integer', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => false,
+ 'signed' => false,
+ ])
+ ->addColumn('product_id', 'integer', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => false,
+ 'signed' => false,
+ ])
+ ->addIndex(
+ $this->index([
+ 'product_category',
+ 'product_id',
+ ])
+ ->setName('orders_product_category_idx')
+ )
+ ->create();
+
+ $this->table('parts')
+ ->addColumn('name', 'string', [
+ 'default' => null,
+ 'limit' => 255,
+ 'null' => true,
+ ])
+ ->addColumn('number', 'integer', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ 'signed' => false,
+ ])
+ ->create();
+
+ $this->table('products')
+ ->addColumn('title', 'string', [
+ 'default' => null,
+ 'limit' => 255,
+ 'null' => true,
+ ])
+ ->addColumn('slug', 'string', [
+ 'default' => null,
+ 'limit' => 100,
+ 'null' => true,
+ ])
+ ->addColumn('category_id', 'integer', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ 'signed' => false,
+ ])
+ ->addColumn('created', 'timestamp', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->addColumn('modified', 'timestamp', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->addIndex(
+ $this->index('slug')
+ ->setName('products_slug_unique')
+ ->setType('unique')
+ )
+ ->addIndex(
+ $this->index([
+ 'category_id',
+ 'id',
+ ])
+ ->setName('products_category_unique')
+ ->setType('unique')
+ )
+ ->addIndex(
+ $this->index('title')
+ ->setName('products_title_idx')
+ ->setType('fulltext')
+ )
+ ->create();
+
+ $this->table('special_pks', ['id' => false, 'primary_key' => ['id']])
+ ->addColumn('id', 'uuid', [
+ 'default' => 'a4950df3-515f-474c-be4c-6a027c1957e7',
+ 'limit' => null,
+ 'null' => false,
+ ])
+ ->addColumn('name', 'string', [
+ 'default' => null,
+ 'limit' => 256,
+ 'null' => true,
+ ])
+ ->create();
+
+ $this->table('special_tags')
+ ->addColumn('article_id', 'integer', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => false,
+ 'signed' => false,
+ ])
+ ->addColumn('author_id', 'integer', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ 'signed' => false,
+ ])
+ ->addColumn('tag_id', 'integer', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => false,
+ 'signed' => false,
+ ])
+ ->addColumn('highlighted', 'boolean', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->addColumn('highlighted_time', 'timestamp', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->addIndex(
+ $this->index('article_id')
+ ->setName('special_tags_article_unique')
+ ->setType('unique')
+ )
+ ->create();
+
+ $this->table('texts', ['id' => false])
+ ->addColumn('title', 'string', [
+ 'default' => null,
+ 'limit' => 255,
+ 'null' => true,
+ ])
+ ->addColumn('description', 'text', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->create();
+
+ $this->table('users')
+ ->addColumn('username', 'string', [
+ 'default' => null,
+ 'limit' => 256,
+ 'null' => true,
+ ])
+ ->addColumn('password', 'string', [
+ 'default' => null,
+ 'limit' => 256,
+ 'null' => true,
+ ])
+ ->addColumn('created', 'timestamp', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ ])
+ ->addColumn('updated', 'timestamp', [
+ 'default' => null,
+ 'limit' => null,
+ 'null' => true,
+ 'update' => 'CURRENT_TIMESTAMP',
+ ])
+ ->create();
+
+ $this->table('articles')
+ ->addForeignKey(
+ $this->foreignKey('category_id')
+ ->setReferencedTable('categories')
+ ->setReferencedColumns('id')
+ ->setDelete('NO_ACTION')
+ ->setUpdate('NO_ACTION')
+ ->setName('articles_category_fk')
+ )
+ ->update();
+
+ $this->table('orders')
+ ->addForeignKey(
+ $this->foreignKey([
+ 'product_category',
+ 'product_id',
+ ])
+ ->setReferencedTable('products')
+ ->setReferencedColumns([
+ 'category_id',
+ 'id',
+ ])
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
+ ->setName('orders_product_fk')
+ )
+ ->update();
+
+ $this->table('products')
+ ->addForeignKey(
+ $this->foreignKey('category_id')
+ ->setReferencedTable('categories')
+ ->setReferencedColumns('id')
+ ->setDelete('CASCADE')
+ ->setUpdate('CASCADE')
+ ->setName('products_category_fk')
+ )
+ ->update();
+ }
+
+ /**
+ * Down Method.
+ *
+ * More information on this method is available here:
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method
+ *
+ * @return void
+ */
+ public function down(): void
+ {
+ $this->table('articles')
+ ->dropForeignKey(
+ 'category_id'
+ )->save();
+
+ $this->table('orders')
+ ->dropForeignKey(
+ [
+ 'product_category',
+ 'product_id',
+ ]
+ )->save();
+
+ $this->table('products')
+ ->dropForeignKey(
+ 'category_id'
+ )->save();
+
+ $this->table('articles')->drop()->save();
+ $this->table('categories')->drop()->save();
+ $this->table('composite_pks')->drop()->save();
+ $this->table('events')->drop()->save();
+ $this->table('orders')->drop()->save();
+ $this->table('parts')->drop()->save();
+ $this->table('products')->drop()->save();
+ $this->table('special_pks')->drop()->save();
+ $this->table('special_tags')->drop()->save();
+ $this->table('texts')->drop()->save();
+ $this->table('users')->drop()->save();
+ }
+}
diff --git a/tests/comparisons/Seeds/pgsql/testWithData.php b/tests/comparisons/Seeds/pgsql/testWithData.php
index 272d14ecd..1aa40b98a 100644
--- a/tests/comparisons/Seeds/pgsql/testWithData.php
+++ b/tests/comparisons/Seeds/pgsql/testWithData.php
@@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php b/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php
index 45fda5df6..2b02ec3ba 100644
--- a/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php
+++ b/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php
@@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/comparisons/Seeds/php81/testWithData.php b/tests/comparisons/Seeds/php81/testWithData.php
index 272d14ecd..1aa40b98a 100644
--- a/tests/comparisons/Seeds/php81/testWithData.php
+++ b/tests/comparisons/Seeds/php81/testWithData.php
@@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/comparisons/Seeds/php81/testWithDataAndLimit.php b/tests/comparisons/Seeds/php81/testWithDataAndLimit.php
index 45fda5df6..2b02ec3ba 100644
--- a/tests/comparisons/Seeds/php81/testWithDataAndLimit.php
+++ b/tests/comparisons/Seeds/php81/testWithDataAndLimit.php
@@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/comparisons/Seeds/sqlserver/testWithData.php b/tests/comparisons/Seeds/sqlserver/testWithData.php
index 6cd3d80d6..dc7a7e3c4 100644
--- a/tests/comparisons/Seeds/sqlserver/testWithData.php
+++ b/tests/comparisons/Seeds/sqlserver/testWithData.php
@@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php b/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php
index 3da226273..d9af7a606 100644
--- a/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php
+++ b/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php
@@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/comparisons/Seeds/testBasicBaking.php b/tests/comparisons/Seeds/testBasicBaking.php
index ce5dc9915..2aa8dd0b2 100644
--- a/tests/comparisons/Seeds/testBasicBaking.php
+++ b/tests/comparisons/Seeds/testBasicBaking.php
@@ -14,7 +14,7 @@ class ArticlesSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/comparisons/Seeds/testPrettifyArray.php b/tests/comparisons/Seeds/testPrettifyArray.php
index c5a2978da..e077a82d0 100644
--- a/tests/comparisons/Seeds/testPrettifyArray.php
+++ b/tests/comparisons/Seeds/testPrettifyArray.php
@@ -14,7 +14,7 @@ class TextsSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/comparisons/Seeds/testWithData.php b/tests/comparisons/Seeds/testWithData.php
index 6cd3d80d6..dc7a7e3c4 100644
--- a/tests/comparisons/Seeds/testWithData.php
+++ b/tests/comparisons/Seeds/testWithData.php
@@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/comparisons/Seeds/testWithDataAndFields.php b/tests/comparisons/Seeds/testWithDataAndFields.php
index 02bdace54..7249f5a82 100644
--- a/tests/comparisons/Seeds/testWithDataAndFields.php
+++ b/tests/comparisons/Seeds/testWithDataAndFields.php
@@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/comparisons/Seeds/testWithDataAndLimit.php b/tests/comparisons/Seeds/testWithDataAndLimit.php
index 3da226273..d9af7a606 100644
--- a/tests/comparisons/Seeds/testWithDataAndLimit.php
+++ b/tests/comparisons/Seeds/testWithDataAndLimit.php
@@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php b/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php
index e7bc5ae24..c52597f6c 100644
--- a/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php
+++ b/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php
@@ -23,7 +23,7 @@ public static function defaultName(): string
/**
* @inheritDoc
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
return parent::buildOptionParser($parser)
->addOption('test-target-folder')
diff --git a/tests/test_app/App/Http/TestRequestHandler.php b/tests/test_app/App/Http/TestRequestHandler.php
index 2b376a4f5..dfe37dce3 100644
--- a/tests/test_app/App/Http/TestRequestHandler.php
+++ b/tests/test_app/App/Http/TestRequestHandler.php
@@ -10,11 +10,14 @@
class TestRequestHandler implements RequestHandlerInterface
{
+ /**
+ * @var callable
+ */
public $callable;
public function __construct(?callable $callable = null)
{
- $this->callable = $callable ?: function ($request) {
+ $this->callable = $callable ?: function ($request): Response {
return new Response();
};
}
diff --git a/tests/test_app/Plugin/Migrator/config/Migrations/20211001000000_migrator.php b/tests/test_app/Plugin/Migrator/config/Migrations/20211001000000_migrator.php
index abc0aaa4f..c3051a35c 100644
--- a/tests/test_app/Plugin/Migrator/config/Migrations/20211001000000_migrator.php
+++ b/tests/test_app/Plugin/Migrator/config/Migrations/20211001000000_migrator.php
@@ -4,7 +4,7 @@
class Migrator extends BaseMigration
{
- public function up()
+ public function up(): void
{
$this->table('migrator')->addColumn('test', 'integer')->create();
$this->table('migrator')->insert(['test' => 1])->save();
diff --git a/tests/test_app/Plugin/Migrator/config/Migrations2/20211002000000_migrator2.php b/tests/test_app/Plugin/Migrator/config/Migrations2/20211002000000_migrator2.php
index e06b74823..8cb055972 100644
--- a/tests/test_app/Plugin/Migrator/config/Migrations2/20211002000000_migrator2.php
+++ b/tests/test_app/Plugin/Migrator/config/Migrations2/20211002000000_migrator2.php
@@ -4,7 +4,7 @@
class Migrator2 extends BaseMigration
{
- public function up()
+ public function up(): void
{
$this->table('migrator')->insert(['test' => 2])->save();
}
diff --git a/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginLettersSeed.php b/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginLettersSeed.php
index 952893f7b..5b1e7cedd 100644
--- a/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginLettersSeed.php
+++ b/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginLettersSeed.php
@@ -13,7 +13,7 @@ class PluginLettersSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*/
public function run(): void
{
diff --git a/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginSubLettersSeed.php b/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginSubLettersSeed.php
index 1a3f77c39..9df45af15 100644
--- a/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginSubLettersSeed.php
+++ b/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginSubLettersSeed.php
@@ -13,7 +13,7 @@ class PluginSubLettersSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*/
public function run(): void
{
diff --git a/tests/test_app/config/AltSeeds/AnotherNumbersSeed.php b/tests/test_app/config/AltSeeds/AnotherNumbersSeed.php
index efa478b60..8f8f4b53a 100644
--- a/tests/test_app/config/AltSeeds/AnotherNumbersSeed.php
+++ b/tests/test_app/config/AltSeeds/AnotherNumbersSeed.php
@@ -13,7 +13,7 @@ class AnotherNumbersSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*/
public function run(): void
{
diff --git a/tests/test_app/config/AltSeeds/NumbersAltSeed.php b/tests/test_app/config/AltSeeds/NumbersAltSeed.php
index 1455134eb..3d640f482 100644
--- a/tests/test_app/config/AltSeeds/NumbersAltSeed.php
+++ b/tests/test_app/config/AltSeeds/NumbersAltSeed.php
@@ -13,7 +13,7 @@ class NumbersAltSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*/
public function run(): void
{
diff --git a/tests/test_app/config/AnonymousMigrations/2024_12_08_150000_CreateTestAnonymousTable.php b/tests/test_app/config/AnonymousMigrations/2024_12_08_150000_CreateTestAnonymousTable.php
index 1543dd13e..9f0ca6dac 100644
--- a/tests/test_app/config/AnonymousMigrations/2024_12_08_150000_CreateTestAnonymousTable.php
+++ b/tests/test_app/config/AnonymousMigrations/2024_12_08_150000_CreateTestAnonymousTable.php
@@ -9,7 +9,7 @@
* Change Method.
*
* More information on this method is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method
*/
public function change(): void
{
diff --git a/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php b/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php
index 5558fb593..029f48b9a 100644
--- a/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php
+++ b/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php
@@ -13,7 +13,7 @@ class MigrationSeedNumbers extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*/
public function run(): void
{
diff --git a/tests/test_app/config/CallSeeds/DatabaseSeed.php b/tests/test_app/config/CallSeeds/DatabaseSeed.php
index e8e69f01d..8321e103d 100644
--- a/tests/test_app/config/CallSeeds/DatabaseSeed.php
+++ b/tests/test_app/config/CallSeeds/DatabaseSeed.php
@@ -13,7 +13,7 @@ class DatabaseSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*/
public function run(): void
{
diff --git a/tests/test_app/config/CallSeeds/LettersSeed.php b/tests/test_app/config/CallSeeds/LettersSeed.php
index a2591b6ff..c4a8267c9 100644
--- a/tests/test_app/config/CallSeeds/LettersSeed.php
+++ b/tests/test_app/config/CallSeeds/LettersSeed.php
@@ -13,7 +13,7 @@ class LettersSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*/
public function run(): void
{
diff --git a/tests/test_app/config/CallSeeds/NumbersCallSeed.php b/tests/test_app/config/CallSeeds/NumbersCallSeed.php
index 24f56bde2..3f36c1b5c 100644
--- a/tests/test_app/config/CallSeeds/NumbersCallSeed.php
+++ b/tests/test_app/config/CallSeeds/NumbersCallSeed.php
@@ -13,7 +13,7 @@ class NumbersCallSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*/
public function run(): void
{
diff --git a/tests/test_app/config/CallSeeds/ShortNameCallSeed.php b/tests/test_app/config/CallSeeds/ShortNameCallSeed.php
index 00ff336e3..ad9485c29 100644
--- a/tests/test_app/config/CallSeeds/ShortNameCallSeed.php
+++ b/tests/test_app/config/CallSeeds/ShortNameCallSeed.php
@@ -13,7 +13,7 @@ class ShortNameCallSeed extends BaseSeed
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*/
public function run(): void
{
diff --git a/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php b/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php
index a24c27de9..8f274973f 100644
--- a/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php
+++ b/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php
@@ -4,7 +4,7 @@
class FirstFkIndexMigration extends BaseMigration
{
- public function up()
+ public function up(): void
{
$this->table('table2', [
'id' => false,
@@ -87,7 +87,7 @@ public function up()
->create();
}
- public function down()
+ public function down(): void
{
$this->table('table1', [
'id' => false,
diff --git a/tests/test_app/config/DropColumnFkIndexRegression/20190928205060_second_fk_index_migration.php b/tests/test_app/config/DropColumnFkIndexRegression/20190928205060_second_fk_index_migration.php
index f6c7cb22e..2835b5b9f 100644
--- a/tests/test_app/config/DropColumnFkIndexRegression/20190928205060_second_fk_index_migration.php
+++ b/tests/test_app/config/DropColumnFkIndexRegression/20190928205060_second_fk_index_migration.php
@@ -4,7 +4,7 @@
class SecondFkIndexMigration extends BaseMigration
{
- public function up()
+ public function up(): void
{
$this->table('table1', [
'id' => false,
diff --git a/tests/test_app/config/DropIndexRegression/20121213232502_create_drop_fk_initial_schema.php b/tests/test_app/config/DropIndexRegression/20121213232502_create_drop_fk_initial_schema.php
index 88f18e47d..f173b072a 100644
--- a/tests/test_app/config/DropIndexRegression/20121213232502_create_drop_fk_initial_schema.php
+++ b/tests/test_app/config/DropIndexRegression/20121213232502_create_drop_fk_initial_schema.php
@@ -7,7 +7,7 @@ class CreateDropFkInitialSchema extends BaseMigration
/**
* Change.
*/
- public function change()
+ public function change(): void
{
$this->table('my_table')
->addColumn('name', 'string')
diff --git a/tests/test_app/config/DropIndexRegression/20121223011815_add_regression_drop_fk.php b/tests/test_app/config/DropIndexRegression/20121223011815_add_regression_drop_fk.php
index 4b1a5a7c9..72f8d3827 100644
--- a/tests/test_app/config/DropIndexRegression/20121223011815_add_regression_drop_fk.php
+++ b/tests/test_app/config/DropIndexRegression/20121223011815_add_regression_drop_fk.php
@@ -7,7 +7,7 @@ class AddRegressionDropFk extends BaseMigration
/**
* Change.
*/
- public function change()
+ public function change(): void
{
$table = $this->table('my_table');
$table
diff --git a/tests/test_app/config/DropIndexRegression/20121223011816_change_fk_regression.php b/tests/test_app/config/DropIndexRegression/20121223011816_change_fk_regression.php
index b3ec6f21b..020e82b01 100644
--- a/tests/test_app/config/DropIndexRegression/20121223011816_change_fk_regression.php
+++ b/tests/test_app/config/DropIndexRegression/20121223011816_change_fk_regression.php
@@ -7,7 +7,7 @@ class ChangeFkRegression extends BaseMigration
/**
* Migrate Up.
*/
- public function up()
+ public function up(): void
{
$table = $this->table('my_table');
$table
diff --git a/tests/test_app/config/DropIndexRegression/20121223011817_change_column_regression.php b/tests/test_app/config/DropIndexRegression/20121223011817_change_column_regression.php
index abbd5a8ac..f95ce31e8 100644
--- a/tests/test_app/config/DropIndexRegression/20121223011817_change_column_regression.php
+++ b/tests/test_app/config/DropIndexRegression/20121223011817_change_column_regression.php
@@ -7,7 +7,7 @@ class ChangeColumnRegression extends BaseMigration
/**
* Migrate Up.
*/
- public function up()
+ public function up(): void
{
$table = $this->table('my_table');
$table
diff --git a/tests/test_app/config/DropTableWithFkRegression/20190928205056_first_drop_fk_migration.php b/tests/test_app/config/DropTableWithFkRegression/20190928205056_first_drop_fk_migration.php
index 376669a65..36f4be4a4 100644
--- a/tests/test_app/config/DropTableWithFkRegression/20190928205056_first_drop_fk_migration.php
+++ b/tests/test_app/config/DropTableWithFkRegression/20190928205056_first_drop_fk_migration.php
@@ -4,7 +4,7 @@
class FirstDropFkMigration extends BaseMigration
{
- public function change()
+ public function change(): void
{
$this->table('orders')
->addColumn('order_date', 'timestamp')
diff --git a/tests/test_app/config/DropTableWithFkRegression/20190928205060_second_drop_fk_migration.php b/tests/test_app/config/DropTableWithFkRegression/20190928205060_second_drop_fk_migration.php
index 2fc464337..f1161cb41 100644
--- a/tests/test_app/config/DropTableWithFkRegression/20190928205060_second_drop_fk_migration.php
+++ b/tests/test_app/config/DropTableWithFkRegression/20190928205060_second_drop_fk_migration.php
@@ -4,7 +4,7 @@
class SecondDropFkMigration extends BaseMigration
{
- public function change()
+ public function change(): void
{
$this->table('customers')
->addColumn('name', 'text')
diff --git a/tests/test_app/config/Duplicatenames/20120111235330_duplicate_migration_name.php b/tests/test_app/config/Duplicatenames/20120111235330_duplicate_migration_name.php
index 4a8dddeb4..d133d6c04 100644
--- a/tests/test_app/config/Duplicatenames/20120111235330_duplicate_migration_name.php
+++ b/tests/test_app/config/Duplicatenames/20120111235330_duplicate_migration_name.php
@@ -7,7 +7,7 @@ class DuplicateMigrationName extends BaseMigration
/**
* Migrate Up.
*/
- public function up()
+ public function up(): void
{
// do nothing
}
@@ -15,7 +15,7 @@ public function up()
/**
* Migrate Down.
*/
- public function down()
+ public function down(): void
{
// do nothing
}
diff --git a/tests/test_app/config/Duplicatenames/20120111235331_duplicate_migration_name.php b/tests/test_app/config/Duplicatenames/20120111235331_duplicate_migration_name.php
index 4a8dddeb4..d133d6c04 100644
--- a/tests/test_app/config/Duplicatenames/20120111235331_duplicate_migration_name.php
+++ b/tests/test_app/config/Duplicatenames/20120111235331_duplicate_migration_name.php
@@ -7,7 +7,7 @@ class DuplicateMigrationName extends BaseMigration
/**
* Migrate Up.
*/
- public function up()
+ public function up(): void
{
// do nothing
}
@@ -15,7 +15,7 @@ public function up()
/**
* Migrate Down.
*/
- public function down()
+ public function down(): void
{
// do nothing
}
diff --git a/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration.php b/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration.php
index 159962d0a..7f9a90ba4 100644
--- a/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration.php
+++ b/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration.php
@@ -7,7 +7,7 @@ class DuplicateMigration extends BaseMigration
/**
* Migrate Up.
*/
- public function up()
+ public function up(): void
{
// do nothing
}
@@ -15,7 +15,7 @@ public function up()
/**
* Migrate Down.
*/
- public function down()
+ public function down(): void
{
// do nothing
}
diff --git a/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration_2.php b/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration_2.php
index 4f548649c..7e0bce59d 100644
--- a/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration_2.php
+++ b/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration_2.php
@@ -7,7 +7,7 @@ class DuplicateMigration2 extends BaseMigration
/**
* Migrate Up.
*/
- public function up()
+ public function up(): void
{
// do nothing
}
@@ -15,7 +15,7 @@ public function up()
/**
* Migrate Down.
*/
- public function down()
+ public function down(): void
{
// do nothing
}
diff --git a/tests/test_app/config/Invalidclassname/20120111235330_invalid_class.php b/tests/test_app/config/Invalidclassname/20120111235330_invalid_class.php
index b147e2f92..942b856b6 100644
--- a/tests/test_app/config/Invalidclassname/20120111235330_invalid_class.php
+++ b/tests/test_app/config/Invalidclassname/20120111235330_invalid_class.php
@@ -7,7 +7,7 @@ class DifferentClassName extends BaseMigration
/**
* Migrate Up.
*/
- public function up()
+ public function up(): void
{
// do nothing
}
@@ -15,7 +15,7 @@ public function up()
/**
* Migrate Down.
*/
- public function down()
+ public function down(): void
{
// do nothing
}
diff --git a/tests/test_app/config/LegacyAbstractMigration/20260327000000_LegacyAbstractMigration.php b/tests/test_app/config/LegacyAbstractMigration/20260327000000_LegacyAbstractMigration.php
new file mode 100644
index 000000000..c7e43eb88
--- /dev/null
+++ b/tests/test_app/config/LegacyAbstractMigration/20260327000000_LegacyAbstractMigration.php
@@ -0,0 +1,11 @@
+table('articles')
->addColumn('title', 'string', [
@@ -304,7 +304,7 @@ public function up()
->update();
}
- public function down()
+ public function down(): void
{
$this->table('articles')
->dropForeignKey(
diff --git a/tests/test_app/config/Reversiblemigrations/20121213232502_create_initial_schema.php b/tests/test_app/config/Reversiblemigrations/20121213232502_create_initial_schema.php
index f27456ea0..4f94bfd69 100644
--- a/tests/test_app/config/Reversiblemigrations/20121213232502_create_initial_schema.php
+++ b/tests/test_app/config/Reversiblemigrations/20121213232502_create_initial_schema.php
@@ -8,7 +8,7 @@ class CreateInitialSchema extends BaseMigration
/**
* Change.
*/
- public function change()
+ public function change(): void
{
// users table
$users = $this->table('users');
diff --git a/tests/test_app/config/Reversiblemigrations/20121223011815_update_info_table.php b/tests/test_app/config/Reversiblemigrations/20121223011815_update_info_table.php
index 147fdfcf7..3b2a15e9f 100644
--- a/tests/test_app/config/Reversiblemigrations/20121223011815_update_info_table.php
+++ b/tests/test_app/config/Reversiblemigrations/20121223011815_update_info_table.php
@@ -8,7 +8,7 @@ class UpdateInfoTable extends BaseMigration
/**
* Change.
*/
- public function change()
+ public function change(): void
{
// info table
$info = $this->table('info');
diff --git a/tests/test_app/config/Reversiblemigrations/20121224200649_rename_info_table_to_statuses_table.php b/tests/test_app/config/Reversiblemigrations/20121224200649_rename_info_table_to_statuses_table.php
index 767a0f9c6..5e49f0383 100644
--- a/tests/test_app/config/Reversiblemigrations/20121224200649_rename_info_table_to_statuses_table.php
+++ b/tests/test_app/config/Reversiblemigrations/20121224200649_rename_info_table_to_statuses_table.php
@@ -7,7 +7,7 @@ class RenameInfoTableToStatusesTable extends BaseMigration
/**
* Change.
*/
- public function change()
+ public function change(): void
{
// users table
$table = $this->table('info');
diff --git a/tests/test_app/config/Reversiblemigrations/20121224200739_rename_bio_to_biography.php b/tests/test_app/config/Reversiblemigrations/20121224200739_rename_bio_to_biography.php
index 42423bafc..3e91fa180 100644
--- a/tests/test_app/config/Reversiblemigrations/20121224200739_rename_bio_to_biography.php
+++ b/tests/test_app/config/Reversiblemigrations/20121224200739_rename_bio_to_biography.php
@@ -7,7 +7,7 @@ class RenameBioToBiography extends BaseMigration
/**
* Change.
*/
- public function change()
+ public function change(): void
{
// users table
$table = $this->table('users');
diff --git a/tests/test_app/config/Reversiblemigrations/20121224200852_create_user_logins_table.php b/tests/test_app/config/Reversiblemigrations/20121224200852_create_user_logins_table.php
index 8e7ec6332..24dfdec76 100644
--- a/tests/test_app/config/Reversiblemigrations/20121224200852_create_user_logins_table.php
+++ b/tests/test_app/config/Reversiblemigrations/20121224200852_create_user_logins_table.php
@@ -8,7 +8,7 @@ class CreateUserLoginsTable extends BaseMigration
/**
* Change.
*/
- public function change()
+ public function change(): void
{
// user logins table
$table = $this->table('user_logins');
diff --git a/tests/test_app/config/Reversiblemigrations/20170824134305_direction_aware_reversible_up.php b/tests/test_app/config/Reversiblemigrations/20170824134305_direction_aware_reversible_up.php
index d87da33d2..12b49727e 100644
--- a/tests/test_app/config/Reversiblemigrations/20170824134305_direction_aware_reversible_up.php
+++ b/tests/test_app/config/Reversiblemigrations/20170824134305_direction_aware_reversible_up.php
@@ -5,7 +5,7 @@
class DirectionAwareReversibleUp extends BaseMigration
{
- public function change()
+ public function change(): void
{
$this->table('change_direction_test')
->addColumn('thing', Column::STRING, [
diff --git a/tests/test_app/config/Reversiblemigrations/20170831121929_direction_aware_reversible_down.php b/tests/test_app/config/Reversiblemigrations/20170831121929_direction_aware_reversible_down.php
index 3455b2ded..3229cd498 100644
--- a/tests/test_app/config/Reversiblemigrations/20170831121929_direction_aware_reversible_down.php
+++ b/tests/test_app/config/Reversiblemigrations/20170831121929_direction_aware_reversible_down.php
@@ -6,7 +6,7 @@
class DirectionAwareReversibleDown extends BaseMigration
{
- public function change()
+ public function change(): void
{
$this->table('change_direction_test')
->addColumn('subthing', Column::STRING, [
diff --git a/tests/test_app/config/Reversiblemigrations/20180431121930_tricky_edge_case.php b/tests/test_app/config/Reversiblemigrations/20180431121930_tricky_edge_case.php
index fdd8f32d6..00a80f3ba 100644
--- a/tests/test_app/config/Reversiblemigrations/20180431121930_tricky_edge_case.php
+++ b/tests/test_app/config/Reversiblemigrations/20180431121930_tricky_edge_case.php
@@ -5,7 +5,7 @@
class TrickyEdgeCase extends BaseMigration
{
- public function change()
+ public function change(): void
{
$table = $this->table('user_logins');
$table
diff --git a/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php b/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php
index d118bed6e..6c9f3ce63 100644
--- a/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php
+++ b/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php
@@ -11,7 +11,7 @@ class CreateTestIndexLimitSpecifierTable extends BaseMigration
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html
*
* The following commands can be used in this method and Migrations will
* automatically reverse them when rolling back:
@@ -26,7 +26,7 @@ class CreateTestIndexLimitSpecifierTable extends BaseMigration
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
- public function change()
+ public function change(): void
{
$table = $this->table('test_index_limit_specifier');
$table->addColumn('column1', Column::STRING)
diff --git a/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php b/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php
index 74b4b3238..99f47ef19 100644
--- a/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php
+++ b/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php
@@ -12,7 +12,7 @@ class AddColumnIndexFk extends BaseMigration
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
- * https://book.cakephp.org/migrations/5/en/migrations.html
+ * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html
*
* The following commands can be used in this method and Migrations will
* automatically reverse them when rolling back:
@@ -31,7 +31,7 @@ class AddColumnIndexFk extends BaseMigration
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
- public function change()
+ public function change(): void
{
$table = $this->table('statuses')
->addColumn('user_id', Column::INTEGER, [
diff --git a/tests/test_app/config/Seeds/AnonymousStoreSeed.php b/tests/test_app/config/Seeds/AnonymousStoreSeed.php
index 4812a5b66..293061f09 100644
--- a/tests/test_app/config/Seeds/AnonymousStoreSeed.php
+++ b/tests/test_app/config/Seeds/AnonymousStoreSeed.php
@@ -11,7 +11,7 @@
* Write your database seeder using this method.
*
* More information on writing seeds is available here:
- * https://book.cakephp.org/migrations/5/en/seeding.html
+ * https://book.cakephp.org/migrations/5/guides/seeding.html
*
* @return void
*/
diff --git a/tests/test_app/config/ShouldExecute/20201207205056_should_not_execute_migration.php b/tests/test_app/config/ShouldExecute/20201207205056_should_not_execute_migration.php
index 1e45f604e..d6c4da16e 100644
--- a/tests/test_app/config/ShouldExecute/20201207205056_should_not_execute_migration.php
+++ b/tests/test_app/config/ShouldExecute/20201207205056_should_not_execute_migration.php
@@ -5,7 +5,7 @@
class ShouldNotExecuteMigration extends BaseMigration
{
- public function change()
+ public function change(): void
{
// info table
$this->table('info')->create();
diff --git a/tests/test_app/config/ShouldExecute/20201207205057_should_execute_migration.php b/tests/test_app/config/ShouldExecute/20201207205057_should_execute_migration.php
index 7e34549b3..d35a45edd 100644
--- a/tests/test_app/config/ShouldExecute/20201207205057_should_execute_migration.php
+++ b/tests/test_app/config/ShouldExecute/20201207205057_should_execute_migration.php
@@ -5,7 +5,7 @@
class ShouldExecuteMigration extends BaseMigration
{
- public function change()
+ public function change(): void
{
// info table
$this->table('info')->create();
diff --git a/tests/test_app/config/ShouldExecuteWithDb/20201207205100_should_execute_with_db_migration.php b/tests/test_app/config/ShouldExecuteWithDb/20201207205100_should_execute_with_db_migration.php
new file mode 100644
index 000000000..0f158bda6
--- /dev/null
+++ b/tests/test_app/config/ShouldExecuteWithDb/20201207205100_should_execute_with_db_migration.php
@@ -0,0 +1,20 @@
+table('info')->create();
+ }
+
+ public function shouldExecute(): bool
+ {
+ // Query the database to decide eligibility. Before the fix the adapter
+ // was not set at this point and getAdapter() threw 'Adapter not set.'.
+ return !$this->getAdapter()->hasTable('info');
+ }
+}