examples/complex/README.md
examples/complex)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 — + i18nIntentionally unrealistic; each targets a specific migration code path.
hc-m2m-source / hc-m2m-target — high-cardinality many-to-many. At --multiplier 100 produces ~2K sources × ~2K targets × 10 fanout = 20K+ join rows, crossing the 1000-row chunk boundary in copyRelationTableRows.${POSTGRES_PORT:-5432}${MYSQL_PORT:-3306}${MARIADB_PORT:-3307}../complex-v4/.tmp/data.db (override with SQLITE_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 via V4_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:
snapshots/postgres-<name>.sqlsnapshots/mysql-<name>.sqlsnapshots/mariadb-<name>.sqlsnapshots/sqlite-<name>.db (raw file copy; fast)DATABASE_CLIENT=mysql)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 a documentId attribute, including upload files, must have no NULL document_id after migrations. The v4 seed always creates media files so the files table 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:smoke
Full flow after yarn build (or --skip-build when dist is current):
yarn test:migrations --initial legacy --database sqlite --skip-build
You 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-buildOptional 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 ≠ 20Checkpoints: 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-xxxxx
Reports 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 via prefers-color-schemeyarn bench:seed --db <db> --multiplier <n> — wipe + boot v4 + seed + snapshot. One-time per (db, multiplier). Runtime scales with multiplier; at m=100 expect ~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 --require preload that wraps the internal migration runner logger (migrating/migrated events). Emits a result JSON to results/<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] — chained bench:run across DBs for a given multiplier. Runs under whatever Strapi version is currently checked out; label via --label.develop, seed once per (db, multiplier) you want data for.yarn bench:run --db <db> --multiplier <n> --label baseline.gh pr checkout), rebuild @strapi/database and @strapi/core.(db, multiplier) combinations, --label pr-xxxxx.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 .html directly).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 by bench.ts, exposed for debugging). The hook self-disables when this isn't set, so the --require can 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 during bench:seed / seed. Default 5, 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 server
These commands:
Note: Default ports:
5432 (override with POSTGRES_PORT)3306 (override with MYSQL_PORT)3307 by default (override with MARIADB_PORT; maps to container 3306)Set the override env var if you have a local DB already bound to the default port:
POSTGRES_PORT=5433 yarn develop:postgres
yarn develop — Start development server (defaults to PostgreSQL; requires a running DB)yarn build — Build for productionyarn start — Start production serveryarn strapi — Run Strapi CLI commandsUse the v5 seeder in this project to generate a large dataset for homepage perf testing:
yarn seed:v5
You can scale the volume with a multiplier:
yarn seed:v5 -- --multiplier 20
Or:
SEED_MULTIPLIER=20 yarn seed:v5