Skip to content

Commit 04ad41f

Browse files
committed
[ADD] util.inconsistencies.break_recursive_loops
Helper to break recursive loops (what a surprise!) in foreign keys on the same table, such as parent/child relations (menus, categories...) or logical dependencies (depending tasks...) [skip matt] closes #129 Related: odoo/upgrade#6411 Signed-off-by: Christophe Simonis (chs) <[email protected]>
1 parent 30b1565 commit 04ad41f

File tree

1 file changed

+96
-2
lines changed

1 file changed

+96
-2
lines changed

src/util/inconsistencies.py

+96-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55

66
from psycopg2.extensions import quote_ident
77
from psycopg2.extras import Json
8+
from psycopg2.sql import SQL
89

910
from .helpers import _validate_model, table_of_model
1011
from .misc import chunks, str2bool
11-
from .pg import get_value_or_en_translation
12-
from .report import add_to_migration_reports
12+
from .pg import format_query, get_value_or_en_translation, target_of
13+
from .report import add_to_migration_reports, get_anchor_link_to_record, html_escape
1314

1415
_logger = logging.getLogger(__name__)
1516

@@ -22,6 +23,99 @@
2223
FROM_ENV = object()
2324

2425

26+
def break_recursive_loops(cr, model, field, name_field="name"):
27+
# TODO add a variant to verify loops on m2m
28+
_validate_model(model)
29+
30+
table = table_of_model(cr, model)
31+
trgt = target_of(cr, table, field)
32+
if not trgt or trgt[:2] != (table, "id"):
33+
raise ValueError("The column `{}` is not FK on itself".format(field))
34+
35+
query = format_query(
36+
cr,
37+
"""
38+
WITH RECURSIVE __loop AS (
39+
SELECT array[{field}] AS path,
40+
False AS cycle
41+
FROM {table}
42+
WHERE {field} IS NOT NULL
43+
GROUP BY {field}
44+
UNION ALL
45+
SELECT child.{field} || curr.path AS path,
46+
child.{field} = any(curr.path) AS cycle
47+
FROM __loop AS curr
48+
JOIN {table} AS child
49+
ON child.id = curr.path[1]
50+
WHERE child.{field} IS NOT NULL
51+
AND NOT curr.cycle
52+
)
53+
SELECT path FROM __loop WHERE cycle
54+
""",
55+
table=table,
56+
field=field,
57+
)
58+
cr.execute(query)
59+
if not cr.rowcount:
60+
return
61+
62+
ids = []
63+
done = set()
64+
for (cycle,) in cr.fetchall():
65+
to_break = min(cycle[: cycle.index(cycle[0], 1)])
66+
if to_break not in done:
67+
ids.append(to_break)
68+
done.update(cycle)
69+
70+
update_query = format_query(
71+
cr,
72+
"""
73+
UPDATE {table}
74+
SET {field} = NULL
75+
WHERE id IN %s
76+
RETURNING id, {name}
77+
""",
78+
table=table,
79+
field=field,
80+
name=SQL(get_value_or_en_translation(cr, table, name_field)),
81+
)
82+
cr.execute(update_query, [tuple(ids)])
83+
bad_data = cr.fetchall()
84+
85+
query = format_query(
86+
cr,
87+
"""
88+
SELECT m.{}, f.{}
89+
FROM ir_model_fields f
90+
JOIN ir_model m
91+
ON m.id = f.model_id
92+
WHERE m.model = %s
93+
AND f.name = %s
94+
""",
95+
SQL(get_value_or_en_translation(cr, "ir_model", "name")),
96+
SQL(get_value_or_en_translation(cr, "ir_model_fields", "field_description")),
97+
)
98+
cr.execute(query, [model, field])
99+
model_label, field_label = cr.fetchone()
100+
101+
add_to_migration_reports(
102+
"""
103+
<details>
104+
<summary>
105+
The following {model} were found to be recursive. Their "{field}" field has been reset.
106+
</summary>
107+
<ul>{li}</ul>
108+
</details>
109+
""".format(
110+
model=html_escape(model_label),
111+
field=html_escape(field_label),
112+
li="".join("<li>{}</li>".format(get_anchor_link_to_record(model, id_, name)) for id_, name in bad_data),
113+
),
114+
format="html",
115+
category="Inconsistencies",
116+
)
117+
118+
25119
def verify_companies(
26120
cr, model, field_name, logger=_logger, model_company_field="company_id", comodel_company_field="company_id"
27121
):

0 commit comments

Comments
 (0)