Skip to content

Commit 1ff209a

Browse files
aj-fuentesKangOl
authored andcommitted
[IMP] util/{models,pg}: check m2m tables on model rename
When renaming a model we need to check m2m tables that may need to be renamed as well. Otherwise the ORM will create a new table that would be empty. If the data is handled directly in the scripts the ignore parameter can be used to avoid warnings. Notes: * As this is a breaking change, this only done by default from Odoo saas~18.1 * From Odoo 9 the column relation_table exists in ir_model_fields * From Odoo 10 the name of m2m tables is given by the model names ordered alphabetically closes #278 Related: odoo/upgrade#7752 Signed-off-by: Christophe Simonis (chs) <[email protected]>
1 parent a09edbd commit 1ff209a

File tree

3 files changed

+96
-4
lines changed

3 files changed

+96
-4
lines changed

src/base/tests/test_util.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ def test_remove_field(self):
562562
self.assertEqual(self.export.export_fields[0].name, "full_name")
563563
self.assertEqual(self.export.export_fields[1].name, "rate_ids/name")
564564

565+
@mute_logger(util.pg._logger.name)
565566
def test_rename_model(self):
566567
util.rename_model(self.cr, "res.currency", "res.currency2")
567568
self._invalidate()
@@ -621,6 +622,7 @@ def test_remove_field(self):
621622
self.assertEqual(remaining_mappings[0].field_name, "full_name")
622623
self.assertEqual(remaining_mappings[1].field_name, "rate_ids/name")
623624

625+
@mute_logger(util.pg._logger.name)
624626
def test_rename_model(self):
625627
util.rename_model(self.cr, "res.currency", "res.currency2")
626628
util.invalidate(self.import_mapping)

src/util/models.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .helpers import _ir_values_value, _validate_model, model_of_table, table_of_model
1616
from .indirect_references import indirect_references
1717
from .inherit import for_each_inherit, inherit_parents
18-
from .misc import _cached, chunks, log_progress
18+
from .misc import _cached, chunks, log_progress, version_gte
1919
from .pg import (
2020
_get_unique_indexes_with,
2121
column_exists,
@@ -28,6 +28,7 @@
2828
get_value_or_en_translation,
2929
parallel_execute,
3030
table_exists,
31+
update_m2m_tables,
3132
view_exists,
3233
)
3334

@@ -268,13 +269,21 @@ def _replace_model_in_computed_custom_fields(cr, source, target):
268269
)
269270

270271

271-
def rename_model(cr, old, new, rename_table=True):
272+
def rename_model(cr, old, new, rename_table=True, ignored_m2ms="ALL_BEFORE_18_1"):
272273
"""
273274
Rename a model.
274275
275-
:param str old: current name of the model to rename
276-
:param str new: new name of the model to rename
276+
Updates all references to the model name in the DB.
277+
278+
If table rename is requested, from saas~18.1+, m2m table are updated too, unless
279+
ignored. In older versions, m2m tables are skipped unless an empty list is passed.
280+
281+
:param str old: current model name
282+
:param str new: new model name
277283
:param bool rename_table: whether to also rename the table of the model
284+
:param ignored_m2ms: m2m tables to skip. Defaults to `"ALL_BEFORE_18_1"`, which skips
285+
all in Odoo 18 or below, none in saa~18.1+. For all versions, if
286+
the value is not the default, skip only the specified m2m tables.
278287
"""
279288
_validate_model(old)
280289
_validate_model(new)
@@ -285,6 +294,10 @@ def rename_model(cr, old, new, rename_table=True):
285294
new_table = table_of_model(cr, new)
286295
if new_table != old_table:
287296
pg_rename_table(cr, old_table, new_table)
297+
if ignored_m2ms != "ALL_BEFORE_18_1": # explicit value, run the the update
298+
update_m2m_tables(cr, old_table, new_table, ignored_m2ms)
299+
elif version_gte("saas~18.1"): # from 18.1 we update by default
300+
update_m2m_tables(cr, old_table, new_table, ())
288301

289302
updates = [("wkf", "osv")] if table_exists(cr, "wkf") else []
290303
updates += [(ir.table, ir.res_model) for ir in indirect_references(cr) if ir.res_model]

src/util/pg.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,83 @@ def create_m2m(cr, m2m, fk1, fk2, col1=None, col2=None):
13511351
)
13521352

13531353

1354+
def update_m2m_tables(cr, old_table, new_table, ignored_m2ms=()):
1355+
"""
1356+
Update m2m table names and columns.
1357+
1358+
This function renames m2m tables still referring to `old_table`. It also updates
1359+
column names and constraints of those tables.
1360+
1361+
:param str old_table: former table name
1362+
:param str new_table: new table name
1363+
:param list(str) ignored_m2ms: explicit list of m2m tables to ignore
1364+
1365+
:meta private: exclude from online docs
1366+
"""
1367+
assert isinstance(ignored_m2ms, (list, tuple))
1368+
if old_table == new_table or not version_gte("10.0"):
1369+
return
1370+
ignored_m2ms = set(ignored_m2ms)
1371+
for orig_m2m_table in get_m2m_tables(cr, new_table):
1372+
if orig_m2m_table in ignored_m2ms:
1373+
continue
1374+
m = re.match(r"^(\w+)_{0}_rel|{0}_(\w+)_rel$".format(re.escape(old_table)), orig_m2m_table)
1375+
if m:
1376+
m2m_table = "{}_{}_rel".format(*sorted([m.group(1) or m.group(2), new_table]))
1377+
# Due to the 63 chars limit in generated constraint names, for long table names the FK
1378+
# constraint is dropped when renaming the table. We need the constraint to correctly
1379+
# identify the FK targets. The FK constraints will be dropped and recreated below.
1380+
rename_table(cr, orig_m2m_table, m2m_table, remove_constraints=False)
1381+
_logger.info("Renamed m2m table %s to %s", orig_m2m_table, m2m_table)
1382+
else:
1383+
m2m_table = orig_m2m_table
1384+
for m2m_col in get_columns(cr, m2m_table).iter_unquoted():
1385+
col_info = target_of(cr, m2m_table, m2m_col)
1386+
if not col_info or col_info[0] != new_table or col_info[1] != "id":
1387+
continue
1388+
old_col, new_col = map("{}_id".format, [old_table, new_table])
1389+
if m2m_col != old_col:
1390+
_logger.warning(
1391+
"Possibly missing rename: the column %s of m2m table %s references the table %s",
1392+
m2m_col,
1393+
m2m_table,
1394+
new_table,
1395+
)
1396+
continue
1397+
old_constraint = col_info[2]
1398+
cr.execute(
1399+
"""
1400+
SELECT c.confdeltype
1401+
FROM pg_constraint c
1402+
JOIN pg_class t
1403+
ON c.conrelid = t.oid
1404+
WHERE t.relname = %s
1405+
AND c.conname = %s
1406+
""",
1407+
[m2m_table, old_constraint],
1408+
)
1409+
on_delete = cr.fetchone()[0]
1410+
query = format_query(
1411+
cr,
1412+
"""
1413+
ALTER TABLE {m2m_table}
1414+
RENAME COLUMN {old_col} TO {new_col};
1415+
1416+
ALTER TABLE {m2m_table}
1417+
DROP CONSTRAINT {old_constraint},
1418+
ADD FOREIGN KEY ({new_col}) REFERENCES {new_table} (id) ON DELETE {del_action}
1419+
""",
1420+
m2m_table=m2m_table,
1421+
old_col=old_col,
1422+
new_col=new_col,
1423+
old_constraint=old_constraint,
1424+
new_table=new_table,
1425+
del_action=SQLStr("RESTRICT") if on_delete == "r" else SQLStr("CASCADE"),
1426+
)
1427+
cr.execute(query)
1428+
_logger.info("Renamed m2m column of table %s from %s to %s", m2m_table, old_col, new_col)
1429+
1430+
13541431
def fixup_m2m(cr, m2m, fk1, fk2, col1=None, col2=None):
13551432
if col1 is None:
13561433
col1 = "%s_id" % fk1

0 commit comments

Comments
 (0)