Magento 2 Declarative Schema: Replace Upgrade Scripts with Idempotent XML
Magento 2 Declarative Schema replaces procedural upgrade scripts with idempotent XML declarations. This guide covers the mechanism, a worked example for a custom order table, whitelist protection, data patches, and practical limits like type changes and large-table locks.
15 Jul 2025, 09:38 UTC

The problem with procedural upgrade scripts
Magento 1 and early Magento 2 modules relied on versioned PHP classes (UpgradeSchema, InstallSchema) that ran once per version. This approach creates three practical problems: scripts accumulate over time, ordering dependencies between modules become fragile, and rolling back a failed deployment requires manual SQL. Declarative Schema, introduced in Magento 2.3, solves this by letting you declare the desired database state in etc/db_schema.xml. During bin/magento setup:upgrade, Magento compares that declaration against the live database (via information_schema) and applies the minimal diff in a single transaction per module. The result is idempotent, CI/CD-friendly migrations that can be reviewed like code.
How the mechanism works
When setup:upgrade runs, Magento\Framework\Setup\Declaration\Schema\Declaration\SchemaBuilder parses every module's db_schema.xml, builds an in-memory representation of the target schema, reads the current schema from information_schema, computes a diff, and emits CREATE, ALTER, or DROP statements. Those statements execute inside a transaction so a failure leaves the database unchanged. Because the diff is calculated fresh each run, you never need to track which script has already executed — the XML is the source of truth.
Worked example: a custom order attribute table
Suppose you need to store a custom field per order. Create app/code/Vendor/Module/etc/db_schema.xml with the following content. The table name follows the vendor_module_ prefix convention to avoid collisions.
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="vendor_module_custom_order_data" resource="default" engine="innodb" comment="Custom order attributes">
<column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/>
<column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" comment="Reference to sales_order"/>
<column xsi:type="varchar" name="custom_field" nullable="true" length="255" comment="Custom value"/>
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="entity_id"/>
</constraint>
<constraint xsi:type="foreign" referenceId="VENDOR_MODULE_CUSTOM_ORDER_DATA_ORDER_ID_SALES_ORDER_ENTITY_ID" table="vendor_module_custom_order_data" column="order_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="CASCADE"/>
<index referenceId="VENDOR_MODULE_CUSTOM_ORDER_DATA_ORDER_ID" indexType="btree">
<column name="order_id"/>
</index>
</table>
</schema>
Run the upgrade from the Magento root directory (requires file-system owner permissions):
bin/magento setup:upgrade
Verify the table was created:
mysql -u$db_user -p$db_pass $db_name -e "SHOW CREATE TABLE vendor_module_custom_order_data\G"
You should see the three columns, the primary key on entity_id, a foreign key to sales_order.entity_id with ON DELETE CASCADE, and a secondary index on order_id. No PHP upgrade class was written.
Protecting existing data with the whitelist
If you later remove a column from the XML, Declarative Schema will drop it on the next setup:upgrade — irreversibly. To prevent accidental loss, Magento maintains a module-scoped whitelist at etc/db_schema_whitelist.json. After you intentionally delete a column or table from the XML, generate the whitelist:
bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_Module
Commit the updated db_schema_whitelist.json. The file lists every table and column the module currently owns; entries present in the whitelist but absent from the XML are preserved. If you skip this step, the column disappears on the next deployment. Risk: the whitelist is per-module. If two modules modify the same table, each must maintain its own whitelist entries to avoid conflicts.
Data migrations stay separate
Declarative Schema handles structure only. When you need to transform existing data — for example, splitting a full_name column into first_name and last_name — implement a DataPatchInterface class. Data patches run once per version, are recorded in the patch_list table, and can contain arbitrary PHP/SQL. They complement the declarative approach rather than replacing it.
Common mistakes and limits
Changing column types
Switching a varchar to text or altering numeric precision may not be an implicit cast. Declarative Schema will issue an ALTER TABLE ... MODIFY COLUMN, but MySQL may truncate or convert data in ways you didn't anticipate. Pair the XML change with a data patch that migrates values safely, then verify with a staging deploy.
Cross-module foreign keys
The example above references sales_order, a core table. If you reference a table from another third-party module, declare the dependency in etc/module.xml so the referenced module installs first:
<sequence>
<module name="Other_Vendor_OtherModule"/>
</sequence>
Without the sequence, setup:upgrade may attempt to create the foreign key before the target table exists, causing a failure.
Large schema diffs on production
Adding an index or changing a column on a table with millions of rows can lock the table for minutes or hours. Declarative Schema does not automatically use online schema change tools. For zero-downtime alterations, consider splitting the change across releases (add column → backfill via patch → add index) or running pt-online-schema-change manually outside the Magento deployment pipeline.
Unsupported objects
Triggers, stored procedures, custom SQL functions, and view definitions cannot be expressed in db_schema.xml. These still require a data patch or a direct SQL execution in a Setup\Patch\Schema\PatchSchemaInterface implementation.
Verification checklist
- Create a test module with the XML above, run
setup:upgrade, and confirm the table exists withSHOW CREATE TABLE. - Add a column to the XML, run
setup:upgradeagain, and verify the column appears without data loss. - Remove a column from the XML, run
generate-whitelist, runsetup:upgrade, and verify the column is dropped. - Check
var/log/system.logfor "Declarative Schema" messages during upgrade to observe diff calculation. - Review the generated
db_schema_whitelist.jsonto ensure it lists all tables and columns the module owns. - Test a cross-module foreign key by creating two modules with dependent tables and verifying installation order via
module.xmlsequence.
When to adopt
Declarative Schema is the recommended approach for all new modules and schema changes in Magento 2.4.x and Adobe Commerce. Migrate existing UpgradeSchema classes incrementally: keep the old scripts for already-deployed versions, write new changes in db_schema.xml, and let the diff engine handle the rest. The whitelist gives you a safety net, and the separation of schema and data patches keeps deployments predictable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.