Magento 2 Declarative Schema: Architecture, Trust Boundaries, and Operational Checks
Replace legacy Install/Upgrade scripts with db_schema.xml: the minimal design, the whitelist trust boundary, operational checks, and the failure modes that bite in production.
14 May 2026, 11:27 UTC

Why Declarative Schema Matters
Magento 2's legacy Install/Upgrade scripts (PHP files executed on every bin/magento setup:upgrade) were flexible but error-prone. They mixed data manipulation with structural changes, made version-control diffs noisy, and made rollback fragile. Declarative schema, introduced in Magento 2.3, makes db_schema.xml the single source of truth for a module's tables, columns, indexes, and constraints. Magento compares the XML against the current database state and applies only the differences, producing a deterministic, auditable migration path.
Prerequisites and Compatibility
- Magento 2.3 or later (Open Source or Adobe Commerce). Confirm the exact version in use, because tooling flags and declarative schema behavior differ across releases.
- The module declares
etc/db_schema.xmland is registered normally viaregistration.php. - All structural changes are expressed in XML. Data transformations still require
DataPatchInterfaceclasses. - You have a staging copy of the production database for testing diffs before deploying.
The Smallest Suitable Design
The minimal viable pattern is:
- One
etc/db_schema.xmlper module. - Generate
etc/db_schema_whitelist.jsonwithbin/magento setup:db-declaration:generate-whitelist. - Commit both files to version control.
- Apply changes with
bin/magento setup:upgrade.
No PHP upgrade scripts are needed for purely structural changes. Example db_schema.xml adding a column to an existing table:
<?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="customer_address_entity" resource="default" comment="Customer Address">
<column xsi:type="text" name="custom_field" nullable="true" comment="Custom address field"/>
</table>
</schema>After editing, regenerate the whitelist from the Magento root as a user with database access:
bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_ModuleOpen the generated JSON and confirm it lists only the intended tables and columns before committing. This review step matters: regenerating the whitelist on a dirty database can silently bless destructive changes you did not intend.
Trust and Data Boundaries
Magento treats tables declared in any module's db_schema.xml as declaratively managed. All other tables are unmanaged and untouched by the diff engine. This defines the trust boundary: third-party and legacy tables are safe from declarative diffs, but they also get none of its validation benefits.
The whitelist is the explicit approval gate for destructive operations. Magento validates db_schema.xml during setup:upgrade and refuses drops of columns or tables unless those elements appear in the whitelist. Structure and data remain separate concerns: declarative schema changes structure only, so any row transformation must be a deliberate, separate data patch.
Operational Checks
- Generate the whitelist on a clean staging copy, not a development database full of experiments.
- Review the whitelist diff in version control on every change.
- Run
bin/magento setup:upgradein staging and inspectvar/logoutput for drop or type-mismatch warnings. - Compare the result against expectations:
mysql -e "SHOW CREATE TABLE customer_address_entity\G"and check the new column, its type, and nullability against the XML. - Flush caches after upgrade and smoke-test the affected storefront or admin flows.
Failure Modes and Mitigations
- Column renames lose data. The diff engine sees a rename as drop-plus-create. Mitigation: add the new column, migrate data with a patch, then remove the old column in a later release.
- Dirty whitelist generation. Regenerating on a modified database can approve unintended drops. Mitigation: generate only from a pristine baseline and review the JSON diff.
- Large-table locks. Adding a column to a large table such as
sales_ordercan lock it for an extended period duringsetup:upgrade. Mitigation: schedule a maintenance window, or split the change (add nullable, backfill, then tighten constraints). - Limited rollback.
setup:upgradeapplies forward diffs. Reverting means editing the XML back and re-running, which recreates structure but not dropped data. Mitigation: verified database backups before any destructive change.
Worked Example: Column Plus Data Migration
Goal: add a nullable custom_status column to sales_order and populate it from the existing status values. The structural half goes in db_schema.xml:
<column xsi:type="varchar" name="custom_status" length="255" nullable="true" comment="Derived status"/>The data half is a patch class implementing DataPatchInterface:
namespace Vendor\Module\Setup\Patch\Data;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\App\ResourceConnection;
class PopulateCustomStatus implements DataPatchInterface
{
private $resource;
public function __construct(ResourceConnection $resource)
{
$this->resource = $resource;
}
public function apply()
{
$connection = $this->resource->getConnection();
$connection->query('UPDATE sales_order SET custom_status = status');
}
public function getAliases() { return []; }
public static function getDependencies() { return []; }
}Run bin/magento setup:upgrade from the Magento root, then verify both halves independently: SHOW FULL COLUMNS FROM sales_order LIKE 'custom_status'; for structure, and SELECT status, custom_status FROM sales_order LIMIT 5; for data. To roll back, remove the column from the XML, regenerate the whitelist, and re-run the upgrade; the column is dropped and its data is recoverable only from backup.
Conditions That Would Change the Design
Stay with legacy Install/Upgrade scripts when you need multi-step structural logic that XML cannot express, when you must support Magento versions older than 2.3, or when a module already carries extensive legacy scripts you prefer to keep together. Otherwise the declarative pattern, paired with data patches and a reviewed whitelist, gives you a version-controlled, auditable database lifecycle with a clear boundary against accidental data loss.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.