Discard-Drafts Migration (5.0.0)#
The core::5.0.0-discard-drafts migration runs automatically during a Strapi v4→v5 upgrade to backfill draft rows for every existing published entry. Before Strapi 5, Draft & Publish operated on a single-row model; v5 requires each document to have both a draft and a published version. This migration bridges the gap by creating the missing draft copies entirely at the database layer — without invoking the document service per entry — so it scales to millions of rows .
Entry point: packages/core/core/src/migrations/database/5.0.0-discard-drafts.ts, exported as discardDocumentDrafts .
Migration Stages#
The migration runs in migrateUp() as five sequential stages across all Draft & Publish content types:
| Stage | Function | What it does |
|---|---|---|
| 1 | copyPublishedEntriesToDraft | INSERT … SELECT to clone published rows into drafts; copies scalar columns and persisted join-column FKs (e.g. created_by_id); sets published_at = NULL |
| 2 | copyRelationsToDrafts | Copies join-table relations (self-referential, inbound, outbound), clones component rows (recursively), copies media and source-side morph rows |
| 3 | fixExistingDraftRelations | Converts v4 draft entries whose join-table targets point to published rows → draft rows |
| 4 | fixExistingDraftComponentRelations | Same as Stage 3 but for join tables owned by component instances |
| 4b | fixPublishedComponentRelationTargets | Ensures published entities' nested component relations point to published targets, not the newly created draft copies |
| 5 | updateJoinColumnRelations | Rewrites foreign-key columns on draft rows to point to draft targets (oneToOne, manyToOne) |
Memory safety: Per-record caches (component parent lookups, clone maps) are scoped to batches of 1,000. Schema-level caches (component metadata, join table names) are global .
Known Bugs and Fixes#
This migration has required several targeted patches since its initial release:
Crash: morphToMany relations — inverseJoinColumn undefined#
PR: #26331
Morph join tables use morphColumn rather than inverseJoinColumn. Accessing .inverseJoinColumn.name unconditionally caused a TypeError at migration time for any project using morphToMany relations. The fix added null guards (if (!joinTable.inverseJoinColumn) continue;) in 7 locations across the migration so morph tables are skipped and handed off to the dedicated morph-copy helpers .
Data integrity: polymorphic/morph relation and component cloning#
PR: #25543
Draft and published entries were sharing component IDs via duplicated *_cmps rows; cloned components were also missing their media morph rows and nested components were not fully cloned. Root cause: the migration was not recursively cloning nested component/dynamic-zone fields, and it was incorrectly copying component join-table rows that should have been handled only inside cloneComponentInstance. The fix:
- Used raw component schema attributes (not transformed DB metadata) to detect and recursively clone nested
componentanddynamiczonefields - Tracked
ClonedComponentPairsCache(originalId → clonedId) for all levels of nesting - Used that cache to copy media morph rows (
files_related_morphs) and other source-side morph rows to the newly cloned components
Data integrity: createdBy / updatedBy NULL on cloned drafts#
PR: #26461
New draft rows had NULL created_by_id / updated_by_id after migration, causing the Admin UI to show missing creator info. In Strapi 5 these are join-column foreign keys, not scalars. Stage 1 originally copied only scalar columns; Stage 5 skips non-D&P targets like admin::user. The fix updated Stage 1 to also copy persisted join-column FKs (attributes with joinColumn but no joinTable, excluding virtual relations like i18n localizations) .
Data integrity: draft relation order lost for non-D&P sources#
PR: #26851
When copyRelationsFromOtherContentTypes copied inbound join-table relations (from non-D&P content types) onto newly cloned draft targets, the inverse-order column was left NULL because v4 only stored order on the owner side. Draft relations then sorted by primary key instead of the original order. The fix introduced assignMissingOrderColumnFromFallback() to derive the missing inverse-side order from the owner-side order value during the copy .
MySQL-specific: JSON serialization failure on component INSERT#
PR: #25927
On MySQL, the mysql2 driver deserializes JSON/blocks column values on SELECT (returning JavaScript objects), but Knex does not re-serialize them on INSERT. When cloneComponentInstance tried to insert the deserialized objects directly, MySQL rejected the row. The fix added a serializeJsonColumns(row, meta) helper that stringifies any attribute with type === 'json' or 'blocks' before the INSERT. The helper is a no-op on PostgreSQL and SQLite, which return JSON columns as strings .
Key Implementation Details#
buildPublishedToDraftMap— builds the published-id → draft-id mapping used throughout; accounts for localization by keying ondocument_id:locale.cloneComponentInstance— deep-clones a component row (newdocument_id, updated timestamps, remapped FK columns), recursively clones nested components and dynamic zones, and registers pairs inClonedComponentPairsCacheso morph rows can be copied afterward .applyJoinTableOrdering— applies stable ordering to all join-table queries so relation copies are deterministic and consistent with entity-service ordering .insertRelationsWithDuplicateHandling— attempts a batch insert, then falls back to individual inserts usingINSERT IGNORE(MySQL),ON CONFLICT DO NOTHING(Postgres/SQLite), or error-code detection .getBatchSize— returns a reduced batch size (250) for SQLite due to compound-SELECT limits; uses 1,000 by default for other engines .- Morph table handling — two separate helpers handle polymorphic relations:
copyMediaMorphToDraftsForContentTypefor the shared upload plugin table (filtering byrelated_type), andcopySourceSideMorphRelationsForContentTypefor per-attribute morph tables (skipping*_cmpstables, which are handled by component cloning) . - Debug logging — enable with the
DEBUG=strapi::migration::discard-draftsenvironment variable .