A failed upgrade rarely fails cleanly
When setup:upgrade errors partway, it does not roll back to where it started. It leaves the database somewhere in the middle, with some schema changes applied and others not.
That half-applied state is schema integrity debt. The store may keep running, but the next upgrade trips over the inconsistency, and errors like "Base table or view not found" start appearing.
This article covers how Magento tracks schema and patch state, how to see when the database is behind the code, and how to recover from an upgrade that stopped halfway.
How Magento tracks schema and patch state
Magento records what has been applied in a few places. The setup_module table holds the schema and data version for each module, and the patch_list table records which data and schema patches have run.
Declarative schema adds another layer, where the database structure is described in db_schema.xml and compared against the live database on upgrade. The db_schema_whitelist.json file records which columns and tables Magento is allowed to manage.
When these records disagree with the actual database, the upgrade process cannot reason about what to do next, and that is where the failures come from.
Seeing when the database is behind the code
Magento can tell you directly whether the schema matches the code. One command reports the state:
bin/magento setup:db:status
It reports whether the database is up to date, or whether a schema or data upgrade is pending. A store that reports pending changes has code expecting a database structure it does not have yet.
setup:db:status reports the database is out of date on a store that is supposedly live and stable, an earlier upgrade did not finish. The store is running on a schema its code no longer fully matches.The half-applied patch problem
Data and schema patches are recorded in patch_list when they complete. A patch that failed partway can leave the database changed while the patch is not recorded, or recorded while its changes did not fully apply.
Either way, the next upgrade makes the wrong assumption. It either re-runs a patch that partly applied, causing duplicate-column or duplicate-key errors, or skips one whose changes never landed.
This is why an upgrade that "worked last time" can fail now: the failure was seeded by an earlier incomplete run that nobody noticed.
Declarative schema and the whitelist
Declarative schema compares db_schema.xml against the live database and applies the difference. The whitelist file tells it which structures are under its control, and a missing or stale whitelist causes it to ignore or mishandle changes.
When custom modules were built without generating a whitelist, their schema changes can fall outside Magento's management entirely. The setup:db-declaration:generate-whitelist command regenerates it for a module, which is part of bringing a drifted schema back under control.
A store with custom tables that no db_schema.xml declares has structure Magento does not know it owns, which is its own integrity gap.
Recovering safely
Recovery starts with a backup, because schema repair on a live database is exactly the kind of change that should be reversible. Never work through a schema inconsistency on production without one.
Run setup:upgrade against a copy first, read the exact error, and identify which module or patch is stuck. The fix is often to reconcile the patch_list or setup_module record with what the database actually contains, so the upgrade can proceed from a consistent state.
This is careful work, not a command you run and hope. The goal is to get the recorded state and the real state to agree, then let the normal upgrade process take over.
Dry-run before you commit
You do not have to run an upgrade blind to find out whether the schema will apply cleanly. Magento can preview the work without changing anything.
Running setup:upgrade --dry-run reports what the upgrade would do, which surfaces a pending schema mismatch before it becomes a half-applied one. On a store with a history of incomplete upgrades, this preview is the difference between a controlled fix and another partial run.
Pair it with a copy of production to test against. Reproducing the upgrade on a recent database copy shows exactly where it fails, so the real run on production follows a path you have already walked.
Integrity now saves the next upgrade
A half-applied schema sits quietly until the next upgrade turns it into a blocked project. Checking setup:db:status now, while nothing is on fire, is how you find it early.
Knowing whether your database matches the code, and whether past upgrades actually finished, is the difference between a routine upgrade and one that fails on the first command. That schema integrity check is a standard part of any upgrade-readiness or stability review.