From f00c7f501f1a5e1f5015154b1dac7aa56a93ca80 Mon Sep 17 00:00:00 2001 From: Edoardo Piroli Date: Tue, 4 Nov 2025 11:20:35 +0100 Subject: [PATCH] [FIX] pg: avoid dropping not null constraints in pg18 In PG18 not null constraint names follow the '{table}_{column}_not_null' pattern as opposed to the '[0-9_]+_not_null' one followed until pg17. Our utils must be adapted to cope with that, otherwise the util will attempt to drop not null constraints - failing against those of primary keys. The following traceback is achieved when trying to upgrade a new demo db in PG18 with `mrp` installed, from 17.0 to 18.0: ``` 2025-11-04 10:23:38,126 28873 ERROR test_mrp_17_18.0 odoo.sql_db: bad query: b'ALTER TABLE "mail_push" DROP CONSTRAINT IF EXISTS "mail_notification_web_push_id_not_null" ' ERROR: column "id" is in a primary key 2025-11-04 10:23:38,127 28873 WARNING test_mrp_17_18.0 odoo.modules.loading: Transient module states were reset 2025-11-04 10:23:38,129 28873 ERROR test_mrp_17_18.0 odoo.modules.registry: Failed to load registry 2025-11-04 10:23:38,129 28873 CRITICAL test_mrp_17_18.0 odoo.service.server: Failed to initialize database `test_mrp_17_18.0`. Traceback (most recent call last): File "/home/odoo/src/odoo/18.0/odoo/service/server.py", line 1366, in preload_registries registry = Registry.new(dbname, update_module=update_module) File "", line 2, in new File "/home/odoo/src/odoo/18.0/odoo/tools/func.py", line 97, in locked return func(inst, *args, **kwargs) File "/home/odoo/src/odoo/18.0/odoo/modules/registry.py", line 129, in new odoo.modules.load_modules(registry, force_demo, status, update_module) File "/home/odoo/src/odoo/18.0/odoo/modules/loading.py", line 485, in load_modules processed_modules += load_marked_modules(env, graph, File "/home/odoo/src/odoo/18.0/odoo/modules/loading.py", line 365, in load_marked_modules loaded, processed = load_module_graph( File "/home/odoo/src/odoo/18.0/odoo/modules/loading.py", line 182, in load_module_graph migrations.migrate_module(package, 'pre') File "/home/odoo/src/odoo/18.0/odoo/modules/migration.py", line 222, in migrate_module exec_script(self.cr, installed_version, pyfile, pkg.name, stage, stageformat[stage] % version) File "/home/odoo/src/odoo/18.0/odoo/modules/migration.py", line 259, in exec_script mod.migrate(cr, installed_version) File "/home/odoo/src/upgrade/migrations/mail/saas~17.1.1.16/pre-migrate.py", line 30, in migrate util.rename_model(cr, "mail.notification.web.push", "mail.push") File "/home/odoo/src/upgrade-util/src/util/models.py", line 308, in rename_model pg_rename_table(cr, old_table, new_table) File "/home/odoo/src/upgrade-util/src/util/pg.py", line 1354, in rename_table remove_constraint(cr, new_table, const, warn=False) File "/home/odoo/src/upgrade-util/src/util/pg.py", line 853, in remove_constraint cr.execute(format_query(cr, "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {} {}", table, name, cascade)) File "/home/odoo/src/odoo/18.0/odoo/sql_db.py", line 357, in execute res = self._obj.execute(query, params) psycopg2.errors.InvalidTableDefinition: column "id" is in a primary key ``` --- src/util/pg.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/util/pg.py b/src/util/pg.py index 7a5873123..da8e0b496 100644 --- a/src/util/pg.py +++ b/src/util/pg.py @@ -1333,14 +1333,14 @@ def rename_table(cr, old_table, new_table, remove_constraints=True): ) if remove_constraints: - # DELETE all constraints, except Primary/Foreign keys, they will be re-created by the ORM - # NOTE: Custom constraints will instead be lost + # DELETE all constraints, except Primary/Foreign keys/not null checks, they will be re-created by the ORM + # NOTE: Custom constraints will instead be lost, except for not null ones cr.execute( - """ + r""" SELECT constraint_name FROM information_schema.table_constraints WHERE table_name = %s - AND constraint_name !~ '^[0-9_]+_not_null$' + AND constraint_name !~ '^\w+_not_null$' AND ( constraint_type NOT IN ('PRIMARY KEY', 'FOREIGN KEY') -- For long table names the constraint name is shortened by PG to fit 63 chars, in such cases -- it's better to drop the constraint, even if it's a foreign key, and let the ORM re-create it. @@ -1353,24 +1353,29 @@ def rename_table(cr, old_table, new_table, remove_constraints=True): _logger.info("Dropping constraint %s on table %s", const, new_table) remove_constraint(cr, new_table, const, warn=False) - # rename fkeys + # rename constraints cr.execute( """ SELECT constraint_name FROM information_schema.table_constraints WHERE table_name = %s - AND constraint_type = 'FOREIGN KEY' - AND constraint_name LIKE %s + AND ( + constraint_name ~ %s + OR ( + constraint_type = 'FOREIGN KEY' + AND constraint_name LIKE %s + ) + ) """, - [new_table, old_table.replace("_", r"\_") + r"\_%"], + [new_table, "^" + re.escape(old_table) + r"_\w+_not_null$", old_table.replace("_", r"\_") + r"\_%"], ) old_table_length = len(old_table) - for (old_fkey,) in cr.fetchall(): - new_fkey = new_table + old_fkey[old_table_length:] - _logger.info("Renaming FK %r to %r", old_fkey, new_fkey) + for (old_const,) in cr.fetchall(): + new_const = new_table + old_const[old_table_length:] + _logger.info("Renaming constraint %r to %r", old_const, new_const) cr.execute( sql.SQL("ALTER TABLE {} RENAME CONSTRAINT {} TO {}").format( - sql.Identifier(new_table), sql.Identifier(old_fkey), sql.Identifier(new_fkey) + sql.Identifier(new_table), sql.Identifier(old_const), sql.Identifier(new_const) ) )