Canonical Strapi app for v4→v5 migration integration tests and benchmarks. It ships rich schemas (relations, dynamic zones, i18n, draft/publish, stress cases), seed scripts, validate-migration.ts, and database tooling used by CI and local workflows.
Location: this directory stays at
examples/complex(Yarn workspace namecomplex) for historical reasons. It is test infrastructure, not a casual demo likegetstarted. Test orchestration lives intests/migration/. Future: we may relocate the app undertests/migration/(e.g.tests/migration/fixture/) so ownership and CI path filters are clearer; until then, treat changes here as changes to the migration-test contract.
The project includes 8 content types covering the feature space v4→v5 migrations touch.
basic— no draft/publish, no i18nbasic-dp— draft/publishbasic-dp-i18n— draft/publish + i18nrelation— relations + morphs + components + DZrelation-dp— + draft/publishrelation-dp-i18n— + i18n
Intentionally unrealistic; each targets a specific migration code path.
hc-m2m-source/hc-m2m-target— high-cardinality many-to-many. At--multiplier 100produces ~2K sources × ~2K targets × 10 fanout = 20K+ join rows, crossing the 1000-row chunk boundary incopyRelationTableRows.
- PostgreSQL 16 — via podman/docker container on
${POSTGRES_PORT:-5432} - MySQL 8 — via container on
${MYSQL_PORT:-3306} - MariaDB 11 — via container on
${MARIADB_PORT:-3307} - SQLite — file-based at
../complex-v4/.tmp/data.db(override withSQLITE_DATABASE_FILENAME)
Container runtime is auto-detected in this order: podman compose → podman-compose → docker compose → docker-compose. Override with STRAPI_BENCH_RUNTIME=podman|docker on mixed-install hosts.
This fixture includes tools for testing migrations between Strapi v4 and v5 by creating an isolated v4 project and managing database snapshots. It ships its own docker-compose.dev.yml so database containers are independent of the monorepo root.
-
Create/Update the external v4 project:
yarn setup:v4
This creates a Strapi v4 project outside the monorepo (default: a sibling directory named
complex-v4). You can override the location viaV4_OUTSIDE_DIR. -
Install v4 deps (one-time):
cd <path-printed-by-setup> yarn install
-
Configure the v4 project (only if you need custom DB creds):
cp .env.example .env # Edit .env as needed -
Start the v4 project:
yarn develop:postgres # or :mysql, :mariadb, :sqlite
The same per-command pattern applies to postgres, mysql, mariadb, and sqlite:
yarn db:start:<db> # start the DB container (no-op for sqlite)
yarn db:stop:<db> # stop the DB container (no-op for sqlite)
yarn db:snapshot:<db> <name> # snapshot current DB state
yarn db:restore:<db> <name> # restore DB from a named snapshot
yarn db:wipe:<db> # drop + recreate (clean slate)
yarn db:check:<db> # print table row counts (runs ANALYZE first for fresh stats)Snapshots live in snapshots/ and are gitignored:
- PostgreSQL:
snapshots/postgres-<name>.sql - MySQL:
snapshots/mysql-<name>.sql - MariaDB:
snapshots/mariadb-<name>.sql - SQLite:
snapshots/sqlite-<name>.db(raw file copy; fast)
Same UX as MySQL; Compose maps host MARIADB_PORT → container 3306 (default host port 3307 so it can run beside MySQL on 3306).
yarn db:start:mariadb
yarn db:stop:mariadb
yarn db:snapshot:mariadb <name>
yarn db:restore:mariadb <name>
yarn db:wipe:mariadb
yarn db:check:mariadb-
Setup v4 project (if not already done):
yarn setup:v4
-
Wipe the database (ensures v4 format, no v5 schema):
yarn db:wipe:postgres
-
Start v4 project (in separate terminal, use the path printed by setup):
cd <path-printed-by-setup> yarn develop:postgres
(v4 will automatically start its database if needed)
-
Seed test data in the v4 project:
yarn seed
-
Create snapshot:
cd examples/complex yarn db:snapshot:postgres mybackup -
Stop v4 server (Ctrl+C in v4 terminal)
-
Start v5 server with the same database:
yarn develop:postgres
Migrations will run automatically on startup.
-
Validate migration (no HTTP server needed):
yarn test:migration
This includes a document_id backfill check (internal migration
5.0.0-02-created-document-id): every table with adocumentIdattribute, including uploadfiles, must have noNULLdocument_idafter migrations. The v4 seed always creates media files so thefilestable is populated before the v4→v5 upgrade. -
Test and fix bugs as needed
-
Restore snapshot to reset database:
yarn db:restore:postgres mybackup
-
Repeat from step 7 to test fixes
Note: The database container stays running even after stopping Strapi, so you can inspect the database or run multiple tests without restarting the container. Manual workflows use Compose project name strapi_complex so they do not collide with other containers (automated yarn test:migrations uses strapi_migration_v5 by default).
From the repository root (quick smoke, ~1–2 min on a warm tree):
yarn test:migrations:smokeFull flow after yarn build (or --skip-build when dist is current):
yarn test:migrations --initial legacy --database sqlite --skip-buildYou must pass --initial <4.x semver> (or use --scenario tests/migration/scenarios/v4-to-head.json): v4 baseline npm version. The last step is always workspace (this monorepo). There is no final Strapi version flag.
The orchestrator (tests/migration/scripts/run-migration-scenario.ts) wipes examples/complex/.migration-v5/, scaffolds a disposable v4 app via scripts/setup-v4-project.ts, seeds it, then validates on the same database against workspace Strapi. Postgres / MySQL / MariaDB legs start DB containers via examples/complex/docker-compose.dev.yml; sqlite uses a local file and needs no Docker. Compose project strapi_migration_v5 by default. Instant dry-run: yarn test:migrations:plan --initial legacy.
CI: tests/migration/README.md (migration_v5 job, Node 20, latest v4 from @strapi/strapi@legacy).
Options:
--initial <4.x semver>— required without--scenario--scenario <path>— JSON scenario (default:tests/migration/scenarios/v4-to-head.json)--initial-node/--workspace-node— optional host Node major guard (single process; aliases must match)--database sqlite(default locally) |postgres|mysql|mariadb--multiplier N,--build,--skip-build
Optional env: tests/migration/v5/.env.example. Strapi v4 scaffold targets Node ≤ 20 (CI passes --initial-node 20). Env defaults (DATABASE_CLIENT, MIGRATION_MULTIPLIER / SEED_MULTIPLIER) apply when the matching CLI flag is omitted.
Pass flags after the script name (avoid an extra -- before --database or Yarn may not forward options).
Examples:
yarn test:migrations --initial legacy --database sqlite --skip-buildyarn test:migrations --scenario tests/migration/scenarios/v4-to-head.jsonyarn test:migrations --initial-node 20— fail fast if Node major ≠ 20
Checkpoints: tests/migration/CHECKPOINTS.md.
For reviewing PRs that touch v4→v5 migration code, this project ships a benchmark harness that captures per-migration timings and produces baseline-vs-candidate reports across any combination of databases and multipliers.
# One-time setup
yarn setup:v4
cd ../../complex-v4 && yarn install && cd -
# Seed data (one snapshot per DB × multiplier, kept in snapshots/)
yarn bench:seed --db postgres --multiplier 100
# Capture baseline — on develop (or whatever you're comparing against)
yarn bench:run --db postgres --multiplier 100 --label baseline
# Capture candidate — git checkout or cherry-pick the PR, rebuild, then:
yarn workspace @strapi/database run build
yarn workspace @strapi/core run build
yarn bench:run --db postgres --multiplier 100 --label pr-xxxxx
# Generate matrix comparison report
yarn bench:compare --baseline baseline --candidate pr-xxxxxReports land in results/:
compare-<timestamp>.md— clipboard-ready markdown, also echoed to stdoutcompare-<timestamp>.html— self-contained single-file HTML with inline SVG charts, sortable tables, and light/dark theme support viaprefers-color-scheme
yarn bench:seed --db <db> --multiplier <n>— wipe + boot v4 + seed + snapshot. One-time per (db, multiplier). Runtime scales with multiplier; atm=100expect ~8–10 min per DB depending on hardware.yarn bench:run --db <db> --multiplier <n> --label <label>— restore snapshot + spawn Strapi v5 in migrate-then-exit mode + capture per-migration timings via a Node--requirepreload that wraps the internal migration runner logger (migrating/migratedevents). Emits a result JSON toresults/<db>-<label>-<timestamp>.json. Typically ~15s to several minutes depending on dataset size.yarn bench:compare --baseline <label> --candidate <label>— render a multiplier × database matrix plus per-cell per-migration breakdowns, to both markdown and self-contained HTML. Accepts partial data (missing cells render as—).yarn bench:suite --multiplier <n> [--dbs postgres,mysql,mariadb,sqlite]— chainedbench:runacross DBs for a given multiplier. Runs under whatever Strapi version is currently checked out; label via--label.
- On
develop, seed once per (db, multiplier) you want data for. - Run baselines:
yarn bench:run --db <db> --multiplier <n> --label baseline. - Cherry-pick the PR's commits (or
gh pr checkout), rebuild@strapi/databaseand@strapi/core. - Run candidates with the same
(db, multiplier)combinations,--label pr-xxxxx. - Reset cherry-pick + rebuild.
yarn bench:compare --baseline baseline --candidate pr-xxxxx— paste the markdown into a PR comment; attach the zipped HTML as an upload (GitHub comments don't render.htmldirectly).
Snapshots are reused across bench:run invocations — you only re-seed when the schema itself changes.
STRAPI_BENCH_HOOK_OUTPUT=<path>— enables the timing preload (set automatically bybench.ts, exposed for debugging). The hook self-disables when this isn't set, so the--requirecan safely live in other dev configs.STRAPI_BENCH_HOOK_DEBUG=1— verbose preload output (migration attach/record events to stderr).STRAPI_BENCH_RUNTIME=podman|docker— override the auto-detected container runtime.SEED_CONCURRENCY=<n>— how many entity-creation tasks run in parallel duringbench:seed/seed. Default5, which stays under Strapi v4's default knex pool of{min: 2, max: 10}. Tune up only if you've also raised the pool max.
The easiest way to start Strapi with a specific database:
yarn develop:postgres # PostgreSQL container + Strapi dev server
yarn develop:mysql # MySQL container + Strapi dev server
yarn develop:mariadb # MariaDB container + Strapi dev server
yarn develop:sqlite # SQLite file (no container) + Strapi dev serverThese commands:
- ✅ Automatically start the database container if it's not already running (no-op for sqlite)
- ✅ Configure Strapi to use the specified database (no manual config needed)
- ✅ Start the Strapi development server
- ✅ Keep the database container running when you press Ctrl+C (only Strapi stops)
Note: Default ports:
- PostgreSQL:
5432(override withPOSTGRES_PORT) - MySQL:
3306(override withMYSQL_PORT) - MariaDB: host port
3307by default (override withMARIADB_PORT; maps to container3306)
Set the override env var if you have a local DB already bound to the default port:
POSTGRES_PORT=5433 yarn develop:postgresyarn develop— Start development server (defaults to PostgreSQL; requires a running DB)yarn build— Build for productionyarn start— Start production serveryarn strapi— Run Strapi CLI commands
Use the v5 seeder in this project to generate a large dataset for homepage perf testing:
yarn seed:v5You can scale the volume with a multiplier:
yarn seed:v5 -- --multiplier 20Or:
SEED_MULTIPLIER=20 yarn seed:v5