Skip to content

[IMP] util/models: check m2m tables on model rename #278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/base/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ def test_remove_field(self):
self.assertEqual(self.export.export_fields[0].name, "full_name")
self.assertEqual(self.export.export_fields[1].name, "rate_ids/name")

@mute_logger(util.pg._logger.name)
def test_rename_model(self):
util.rename_model(self.cr, "res.currency", "res.currency2")
self._invalidate()
Expand Down Expand Up @@ -621,6 +622,7 @@ def test_remove_field(self):
self.assertEqual(remaining_mappings[0].field_name, "full_name")
self.assertEqual(remaining_mappings[1].field_name, "rate_ids/name")

@mute_logger(util.pg._logger.name)
def test_rename_model(self):
util.rename_model(self.cr, "res.currency", "res.currency2")
util.invalidate(self.import_mapping)
Expand Down
21 changes: 17 additions & 4 deletions src/util/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from .helpers import _ir_values_value, _validate_model, model_of_table, table_of_model
from .indirect_references import indirect_references
from .inherit import for_each_inherit, inherit_parents
from .misc import _cached, chunks, log_progress
from .misc import _cached, chunks, log_progress, version_gte
from .pg import (
_get_unique_indexes_with,
column_exists,
Expand All @@ -28,6 +28,7 @@
get_value_or_en_translation,
parallel_execute,
table_exists,
update_m2m_tables,
view_exists,
)

Expand Down Expand Up @@ -268,13 +269,21 @@ def _replace_model_in_computed_custom_fields(cr, source, target):
)


def rename_model(cr, old, new, rename_table=True):
def rename_model(cr, old, new, rename_table=True, ignored_m2ms="ALL_BEFORE_18_1"):
"""
Rename a model.

:param str old: current name of the model to rename
:param str new: new name of the model to rename
Updates all references to the model name in the DB.

If table rename is requested, from saas~18.1+, m2m table are updated too, unless
ignored. In older versions, m2m tables are skipped unless an empty list is passed.

:param str old: current model name
:param str new: new model name
:param bool rename_table: whether to also rename the table of the model
:param ignored_m2ms: m2m tables to skip. Defaults to `"ALL_BEFORE_18_1"`, which skips
all in Odoo 18 or below, none in saa~18.1+. For all versions, if
the value is not the default, skip only the specified m2m tables.
"""
_validate_model(old)
_validate_model(new)
Expand All @@ -285,6 +294,10 @@ def rename_model(cr, old, new, rename_table=True):
new_table = table_of_model(cr, new)
if new_table != old_table:
pg_rename_table(cr, old_table, new_table)
if ignored_m2ms != "ALL_BEFORE_18_1": # explicit value, run the the update
update_m2m_tables(cr, old_table, new_table, ignored_m2ms)
elif version_gte("saas~18.1"): # from 18.1 we update by default
update_m2m_tables(cr, old_table, new_table, ())

updates = [("wkf", "osv")] if table_exists(cr, "wkf") else []
updates += [(ir.table, ir.res_model) for ir in indirect_references(cr) if ir.res_model]
Expand Down
77 changes: 77 additions & 0 deletions src/util/pg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,83 @@ def create_m2m(cr, m2m, fk1, fk2, col1=None, col2=None):
)


def update_m2m_tables(cr, old_table, new_table, ignored_m2ms=()):
"""
Update m2m table names and columns.

This function renames m2m tables still referring to `old_table`. It also updates
column names and constraints of those tables.

:param str old_table: former table name
:param str new_table: new table name
:param list(str) ignored_m2ms: explicit list of m2m tables to ignore

:meta private: exclude from online docs
"""
assert isinstance(ignored_m2ms, (list, tuple))
if old_table == new_table or not version_gte("10.0"):
return
ignored_m2ms = set(ignored_m2ms)
for orig_m2m_table in get_m2m_tables(cr, new_table):
if orig_m2m_table in ignored_m2ms:
continue
m = re.match(r"^(\w+)_{0}_rel|{0}_(\w+)_rel$".format(re.escape(old_table)), orig_m2m_table)
if m:
m2m_table = "{}_{}_rel".format(*sorted([m.group(1) or m.group(2), new_table]))
# Due to the 63 chars limit in generated constraint names, for long table names the FK
# constraint is dropped when renaming the table. We need the constraint to correctly
# identify the FK targets. The FK constraints will be dropped and recreated below.
rename_table(cr, orig_m2m_table, m2m_table, remove_constraints=False)
_logger.info("Renamed m2m table %s to %s", orig_m2m_table, m2m_table)
else:
m2m_table = orig_m2m_table
for m2m_col in get_columns(cr, m2m_table).iter_unquoted():
col_info = target_of(cr, m2m_table, m2m_col)
if not col_info or col_info[0] != new_table or col_info[1] != "id":
continue
old_col, new_col = map("{}_id".format, [old_table, new_table])
if m2m_col != old_col:
_logger.warning(
"Possibly missing rename: the column %s of m2m table %s references the table %s",
m2m_col,
m2m_table,
new_table,
)
continue
old_constraint = col_info[2]
cr.execute(
"""
SELECT c.confdeltype
FROM pg_constraint c
JOIN pg_class t
ON c.conrelid = t.oid
WHERE t.relname = %s
AND c.conname = %s
""",
[m2m_table, old_constraint],
)
on_delete = cr.fetchone()[0]
query = format_query(
cr,
"""
ALTER TABLE {m2m_table}
RENAME COLUMN {old_col} TO {new_col};

ALTER TABLE {m2m_table}
DROP CONSTRAINT {old_constraint},
ADD FOREIGN KEY ({new_col}) REFERENCES {new_table} (id) ON DELETE {del_action}
""",
m2m_table=m2m_table,
old_col=old_col,
new_col=new_col,
old_constraint=old_constraint,
new_table=new_table,
del_action=SQLStr("RESTRICT") if on_delete == "r" else SQLStr("CASCADE"),
)
cr.execute(query)
_logger.info("Renamed m2m column of table %s from %s to %s", m2m_table, old_col, new_col)


def fixup_m2m(cr, m2m, fk1, fk2, col1=None, col2=None):
if col1 is None:
col1 = "%s_id" % fk1
Expand Down