Odoo upgrade error
ForeignKeyViolation: Key is not present in table (Odoo upgrade)
Last updated: October 2026
Odoo tried to create a foreign key during the upgrade and PostgreSQL found rows that point at records that no longer exist.
This page is part of our guide on how to upgrade Odoo Community Edition. It explains what triggers the error, how to fix it by hand, and what an automated migration does about it.
The error
psycopg2.errors.ForeignKeyViolation: insert or update on table "res_partner" violates foreign key constraint "res_partner_industry_id_fkey"
DETAIL: Key (industry_id)=(25) is not present in table "res_partner_industry".Table, column and id differ per database. The traceback above the message runs through check_foreign_keys and add_foreign_key. If it says "update or delete on table ... violates foreign key constraint" instead, Odoo was deleting a record that is still referenced, which is a different problem.
- Raised by
- PostgreSQL, while Odoo runs
ALTER TABLE ... ADD FOREIGN KEYfromcheck_foreign_keys - Upgrade hops
- Any (15→16 through 18→19)
- Seen when
- Loading modules during
-u all
When you see it
While each module loads, Odoo compares the many2one fields it knows about with the foreign keys that exist in PostgreSQL and adds the ones that are missing (Registry.check_foreign_keys, which calls add_foreign_key in odoo/tools/sql.py). PostgreSQL validates every existing row when a key is added. One row with a value that has no matching parent is enough to abort the statement, and with it the whole upgrade.
So the error does not mean the upgrade deleted something. It means the key was missing before the upgrade started, the orphaned rows were already there, and the upgrade is the first thing that tries to put the key back.
Root cause
We see three ways a database ends up with a missing key and orphaned rows:
- The key was never restored.
psqlandpg_restorekeep going when anADD CONSTRAINTfails, so a dump restored on top of orphaned data silently loses that key. Every delete after that point can leave new orphans behind. - A custom module redefined a standard field: it pointed a many2one at a different model, or made it non-stored. While that module is installed, the standard key does not exist. During the upgrade the custom module is no longer loaded, the standard definition returns, and Odoo adds the standard key. We traced one failed upgrade to exactly this on
res.partner.industry_id. - Records were removed with raw SQL, with database clean-up apps, or with triggers disabled, so nothing cascaded or blocked the delete.
A quick health check: count the foreign keys in the database. A database with around a hundred installed modules normally has thousands. A few hundred means many keys are missing, and you should expect this error more than once.
SELECT count(*) FROM pg_constraint WHERE contype = 'f';How to fix it
Take the table, column and parent table from the error message and list the orphaned rows. For the example above:
SELECT p.id, p.industry_id
FROM res_partner p
LEFT JOIN res_partner_industry i ON i.id = p.industry_id
WHERE p.industry_id IS NOT NULL
AND i.id IS NULL;Before you change anything: Back up the database before running any UPDATE or INSERT below, and try it on a copy first. These statements change data directly, outside the Odoo ORM.
If the column is nullable (most many2one fields are), clear the dangling reference. The row itself, here the partner, stays:
UPDATE res_partner p
SET industry_id = NULL
WHERE p.industry_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM res_partner_industry i WHERE i.id = p.industry_id);If the column is NOT NULL, do not delete the rows to get past the error: they are usually business documents. Point them at a valid parent record, or recreate the missing parent with the id the rows expect, then run the upgrade again.
Odoo stops at the first key it cannot add, so a database with several missing keys fails once per key. Repeat the check for each table and column the error names, or run the same query pattern for every many2one column before you start.
How OCU handles it
Before every upgrade step the OCU runner scans stored many2one fields, many2many relation tables and the existing foreign keys for orphaned rows. Nullable references are set to NULL. Rows of many2many relation tables that point at missing records are removed. Other NOT NULL rows are kept, never deleted, and the key Odoo expects is created as NOT VALID so the upgrade can continue and the exported dump restores cleanly.
Some keys only appear once the target version is loading, as in the custom-module case above. When Odoo fails inside add_foreign_key, the runner reads the table and column from the error, applies the same repair and retries the upgrade, up to four times per step. Every row it nulls or keeps is reported as a warning with table, column and sample ids. A database that is missing many keys can still use up those four retries; the failure then names the table and column, and the manual steps above apply.
A free test migration shows whether your database runs into this or any other upgrade error before you change anything on your production server.
Frequently asked questions
- Did the upgrade delete the missing record?
- Usually not. The error comes from PostgreSQL validating existing rows while Odoo adds a missing foreign key. The orphaned rows were already in the database before the upgrade started; the key that would have prevented them was missing.
- Can I just delete the orphaned rows?
- Only for link-table rows (many2many relations) or rows you have checked are worthless. For a nullable many2one, set the column to NULL instead. For a required column, reassign the rows or recreate the missing parent, because deleting invoices or stock moves to get an upgrade through causes far bigger problems later.
- Why does the error come back with a different table each time?
- Odoo adds foreign keys one at a time and the first failure aborts the run. If several keys are missing, each attempt stops at the next one. Fix them all before retrying by checking every many2one column for orphans.
Related
Find upgrade errors before your production run
Upload a backup and run a free test migration. You get the upgraded database, the full log and every warning, with no change to your live system.
Start your free test migration