ARTICLE

Magento Declarative Schema: Using db_schema.xml to Change Tables Without InstallData or Setup Scripts

Magento Declarative Schema: Using db_schema.xml to Change Tables Without InstallData or Setup Scripts

Magento declarative schema lets you define a module’s database tables in a single db_schema.xml file that states the final desired structure, instead of writing step-by-step InstallSchema and UpgradeSchema PHP scripts. Magento compares your declaration to the live database and generates only the changes needed, which makes schema changes simpler, safer, and far easier to review.

This is one of the quieter but more consequential shifts in modern Magento development. If your team still thinks in version-numbered upgrade scripts, you are maintaining code the platform wants to generate for you. This guide covers how declarative schema works, how to write db_schema.xml, the whitelist that trips up most developers, and where PHP patches still belong.

What declarative schema is, and what it replaces

Before declarative schema, every schema change lived in imperative PHP. You wrote an InstallSchema class for the initial structure and an UpgradeSchema class with a chain of version checks (if version < 1.0.1, add this column; if version < 1.0.2, add that index). Over a module’s life, that file became a growing archive of every change ever made, and reading the current table structure meant mentally replaying the whole history.

Declarative schema inverts the model. You describe the end state in XML. On setup:upgrade, Magento reads your declaration, inspects the actual database, computes the difference, and applies only what is missing or changed. As the Adobe declarative schema overview puts it, developers declare the final desired state and the system adjusts to it without redundant operations. Adobe has confirmed that upgrade scripts are being phased out in favor of declarative schema, so this is the direction, not an option.

One important constraint: once a module is converted to declarative schema, it cannot be reverted to upgrade scripts. The move is one-way, so convert deliberately.

The structure of db_schema.xml

The file lives at app/code/Vendor/Module/etc/db_schema.xml. It declares tables, their columns, constraints, and indexes. A minimal example:

<?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_entity" resource="default" engine="innodb"
           comment="Vendor Module Entity Table">
        <column xsi:type="int" name="entity_id" padding="10" unsigned="true"
                nullable="false" identity="true" comment="Entity ID"/>
        <column xsi:type="varchar" name="name" nullable="false" length="255"
                comment="Entity Name"/>
        <column xsi:type="int" name="customer_id" padding="10" unsigned="true"
                nullable="true" comment="Customer ID"/>
        <column xsi:type="timestamp" name="created_at" on_update="false"
                default="CURRENT_TIMESTAMP" comment="Created At"/>
        <constraint xsi:type="primary" referenceId="PRIMARY">
            <column name="entity_id"/>
        </constraint>
        <constraint xsi:type="foreign" referenceId="VENDOR_MODULE_CUSTOMER_ID"
                    table="vendor_module_entity" column="customer_id"
                    referenceTable="customer_entity" referenceColumn="entity_id"
                    onDelete="CASCADE"/>
        <index referenceId="VENDOR_MODULE_NAME" indexType="btree">
            <column name="name"/>
        </index>
    </table>
</schema>

The pieces to understand:

  • <table> carries name, resource (usually default), engine (usually innodb), and a comment.
  • <column> sets its type through xsi:type (int, varchar, text, timestamp, decimal, boolean, smallint, and others). Attributes depend on the type: integers use padding and unsigned; varchar uses length; decimals use precision and scale. nullable, identity (auto-increment), and default apply broadly.
  • <constraint xsi:type="primary"> declares the primary key and wraps the key column.
  • <constraint xsi:type="foreign"> declares a foreign key with referenceTable, referenceColumn, and onDelete behavior such as CASCADE or SET NULL.
  • <index> takes a referenceId and an indexType of btree, fulltext, or hash.

Because the file is the current structure rather than a history of edits, a reviewer can read one file and know exactly what the module’s tables look like. That reviewability is a real maintenance win, and it pairs well with the dependency discipline we describe in Magento 2 Composer and dependency management. If you want idiomatic references, the Magento core modules are themselves declared this way; browsing the db_schema.xml files in the Magento 2 source on GitHub is the fastest way to see real column types, constraints, and naming conventions in practice.

The whitelist: why drops need db_schema_whitelist.json

This is the step that catches developers off guard. If you remove a column or table from db_schema.xml and run setup:upgrade, Magento does not drop it from the database. By design, declarative schema will not delete a table, column, or key unless it is recorded in a file called db_schema_whitelist.json, which sits alongside db_schema.xml in the module’s etc folder.

The whitelist is a record of every table, column, and key your module’s declarative schema has ever created. It exists to protect backward compatibility: Magento will only drop a database object that appears in the whitelist, so it never removes something another module or a manual change might depend on. A column can be dropped only if it is present in db_schema_whitelist.json.

You generate the whitelist with:

bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_Module

Without a module name, it generates whitelists for all modules. The workflow is: edit db_schema.xml, regenerate the whitelist, then run setup:upgrade. The practical rule is to regenerate the whitelist for every release that changes db_schema.xml, and commit it alongside the schema file. Renames are a special case, because the old name has to remain in the old whitelist for Magento to reconcile the change correctly.

Skipping whitelist regeneration is the number one reason “my column will not drop” tickets exist. The column is not stuck; Magento is correctly refusing to drop something it was never told it owns.

The commands that apply and preview changes

A small set of commands covers the declarative schema workflow. Full syntax is in the Adobe Commerce CLI reference.

  • bin/magento setup:upgrade reads every module’s db_schema.xml, diffs it against the database, and applies the changes. This is what actually alters tables.
  • bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_Module builds or updates the whitelist from the current declaration.
  • bin/magento setup:db:status reports whether the database is up to date with the declared schema, which is useful in CI and before a deploy.
  • bin/magento setup:upgrade --dry-run=1 generates the DDL that would run and logs it to var/log/dry-run-installation.log without touching the database. Run this before any production schema change so you can read the exact SQL first.

That dry run is the habit that separates careful teams from the rest. Reading the generated DDL before it executes on a production database is cheap insurance, and it belongs inside a disciplined release process like the one in zero-downtime Magento deploys with Deployer.

A worked example: adding a column safely

Theory is easy; the first real change is where teams stumble. Say you need to add an indexed status column to an existing vendor_module_entity table on a production store, and backfill a default for existing rows. Here is the full, safe sequence.

First, declare the new column and its index in db_schema.xml. You are editing the end state, not writing an “add column” instruction:

<column xsi:type="smallint" name="status" padding="5" unsigned="true"
        nullable="false" default="1" comment="Status"/>
<index referenceId="VENDOR_MODULE_STATUS" indexType="btree">
    <column name="status"/>
</index>

Second, regenerate the whitelist so Magento records the new column and index as objects it owns:

bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_Module

Third, dry-run the change and read the generated SQL before it touches anything:

bin/magento setup:upgrade --dry-run=1

Open var/log/dry-run-installation.log and confirm the ALTER TABLE is what you expect. On a large table, this is where you catch a change that would lock the table for minutes, and decide to run it in a maintenance window or with an online schema change tool instead.

Fourth, apply it with bin/magento setup:upgrade. The column exists now, with its default applied to new and existing rows because you set default="1".

Fifth, if the correct value for existing rows depends on data rather than a flat default, write a data patch to backfill it. Declarative schema gave every row the default; a patch is what sets the right value per row. That division (structure in XML, data in a patch) is the whole model in one example, and it is why the two systems ship together.

This sequence looks like more steps than the old “write one PHP method” approach, but each step is cheap, and the dry run plus whitelist discipline is exactly what stops a routine column add from becoming a production incident.

Declarative schema vs the old approach

Aspect InstallSchema / UpgradeSchema Declarative schema
What you write Imperative PHP, step by step Declarative XML, end state
Reading current structure Replay the full version history Read one file
Applying changes Runs every step in order Diffs and applies only the delta
Dropping columns/tables Explicit PHP DDL Requires the whitelist
Rollback Manual reverse scripts Not revertable once converted
Future support Being phased out The supported direction

The upside is clear, but note the asymmetry: the move is one-directional and drops are gated by the whitelist. Both are features, not bugs. They exist to stop a careless schema edit from silently destroying data.

Where PHP patches still belong

Declarative schema handles structure. It does not handle data, and it does not handle every exotic schema operation. That is where patches come in, and the two systems are meant to work side by side.

  • Data patches populate or transform data: seeding a lookup table, backfilling a new column, migrating values. These remain PHP classes implementing the patch interface, and they can depend on schema being in place first.
  • Schema patches handle complex structural work that XML cannot express cleanly, such as triggers, stored procedures, or data migration that needs DDL.

We go deep on the data side in writing Magento data patches that replace InstallData and UpgradeData. The mental model to keep: declarative schema says what the tables look like; patches say what goes in them and handle the edge cases. Choosing the right tool for a given change is the same judgment we apply to picking a customization point, covered in plugins vs observers vs preferences.

Safe-change practices for production

A few habits keep declarative schema changes from biting you:

  1. Always dry-run first. Read var/log/dry-run-installation.log before applying anything to a production database.
  2. Regenerate and commit the whitelist whenever db_schema.xml changes, or your drops will silently not happen.
  3. Treat large tables carefully. Adding or altering a column on a huge table can lock it. Know your row counts and plan disruptive changes around low-traffic windows, the same way you would any heavy migration.
  4. Check setup:db:status in CI so a deploy never ships code that expects a schema the database does not have.
  5. Convert once, deliberately. Because the move off upgrade scripts is permanent, convert a module as a clean, reviewed change rather than mid-feature.

These are standard practice on the builds we ship across Magento and Adobe Commerce and on Hyva frontends, where a clean schema layer underneath keeps the storefront stable through upgrades. The same engineering discipline carries over to the other platforms we build on, including Shopify, Shopware, and BigCommerce, and it is backed by the wider partner and integration ecosystem a production store relies on.

Bemeir builds and maintains Magento modules to this standard every day. You can learn more about Bemeir or start from the Bemeir homepage.

FAQ

Does removing a column from db_schema.xml delete it from the database?

Not by itself. Declarative schema only drops a table, column, or key if it is listed in db_schema_whitelist.json. Remove the element from db_schema.xml, regenerate the whitelist with setup:db-declaration:generate-whitelist, then run setup:upgrade. If you skip the whitelist step, the object stays in the database.

What is db_schema_whitelist.json for?

It is a record of the tables, columns, and keys your module’s declarative schema has created, stored in the module’s etc folder. Magento uses it to decide what it is allowed to drop, which protects backward compatibility. You regenerate it for any release that changes db_schema.xml and commit it with the schema file.

Can I go back to InstallSchema and UpgradeSchema after converting?

No. Converting a module to declarative schema is a one-way change; it cannot be reverted to upgrade scripts. Plan the conversion as a deliberate, reviewed step rather than something done casually mid-feature.

How do I preview schema changes before they run?

Use bin/magento setup:upgrade --dry-run=1. It generates the DDL that would execute and writes it to var/log/dry-run-installation.log without changing the database. Reading that log before applying changes to production is strongly recommended, especially for alterations to large tables.

Do I still need data patches with declarative schema?

Yes. Declarative schema defines table structure; it does not insert, migrate, or transform data. Data patches handle that, and schema patches handle complex operations like triggers and stored procedures. The two systems are designed to be used together.

Let us help you get started on a project with Magento Declarative Schema: Using db_schema.xml to Change Tables Without InstallData or Setup Scripts and leverage our partnership to your fullest advantage. Fill out the contact form below to get started.

more articles about ecommerce

Read on the latest with Shopify, Magento, eCommerce topics and more.