Skip to content

Commit 0cd78cb

Browse files
committed
[FIX] util/records: check for new duplicates
`replace_record_references_batch` tries to ensure there are no duplicates when replacing records. It fails when the duplicate is being introduced as a result of two ids mapping to the same target id _and_ no original conflicting record exists. closes #243 Signed-off-by: Christophe Simonis (chs) <[email protected]>
1 parent 22126d0 commit 0cd78cb

File tree

2 files changed

+83
-2
lines changed

2 files changed

+83
-2
lines changed

src/base/tests/test_util.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,62 @@ def test_adapt_domain_view(self):
389389
self.assertIn("courriel", view_search_2.arch)
390390

391391

392+
@unittest.skipUnless(
393+
util.version_gte("13.0"), "This test is incompatible with old style odoo.addons.base.maintenance.migrations.util"
394+
)
395+
class TestReplaceReferences(UnitTestCase):
396+
def setUp(self):
397+
super().setUp()
398+
self.env.cr.execute(
399+
"""
400+
CREATE TABLE dummy_model(
401+
id serial PRIMARY KEY,
402+
res_id int,
403+
res_model varchar,
404+
extra varchar,
405+
CONSTRAINT uniq_constr UNIQUE(res_id, res_model, extra)
406+
);
407+
408+
INSERT INTO dummy_model(res_model, res_id, extra)
409+
VALUES -- the target is there with same res_id
410+
('res.users', 1, 'x'),
411+
('res.partner', 1, 'x'),
412+
413+
-- two with same target and the target is there
414+
('res.users', 2, 'x'),
415+
('res.users', 3, 'x'),
416+
('res.partner', 2, 'x'),
417+
418+
-- two with same target and the target is not there
419+
('res.users', 4, 'x'),
420+
('res.users', 5, 'x'),
421+
422+
-- target is there different res_id
423+
('res.users', 6, 'x'),
424+
('res.partner', 4, 'x')
425+
"""
426+
)
427+
428+
def _ir_dummy(self, cr, bound_only=True):
429+
yield util.IndirectReference("dummy_model", "res_model", "res_id")
430+
431+
def test_replace_record_references_batch__full_unique(self):
432+
cr = self.env.cr
433+
mapping = {1: 1, 2: 2, 3: 2, 4: 3, 5: 3, 6: 4}
434+
with mock.patch("odoo.upgrade.util.records.indirect_references", self._ir_dummy):
435+
util.replace_record_references_batch(cr, mapping, "res.users", "res.partner")
436+
437+
cr.execute("SELECT res_model, res_id, extra FROM dummy_model ORDER BY res_id, res_model")
438+
data = cr.fetchall()
439+
expected = [
440+
("res.partner", 1, "x"),
441+
("res.partner", 2, "x"),
442+
("res.partner", 3, "x"),
443+
("res.partner", 4, "x"),
444+
]
445+
self.assertEqual(data, expected)
446+
447+
392448
class TestRemoveFieldDomains(UnitTestCase):
393449
@parametrize(
394450
[

src/util/records.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,7 @@ def replace_record_references_batch(cr, id_mapping, model_src, model_dst=None, r
16891689
SET {upd}
16901690
{jmap_expr_upd}
16911691
FROM _upgrade_rrr
1692+
{{extra_join}}
16921693
WHERE {whr}
16931694
AND _upgrade_rrr.old = t.{res_id}
16941695
""",
@@ -1705,19 +1706,43 @@ def replace_record_references_batch(cr, id_mapping, model_src, model_dst=None, r
17051706
if ir.res_model_id:
17061707
unique_indexes += _get_unique_indexes_with(cr, ir.table, ir.res_id, ir.res_model_id)
17071708
if unique_indexes:
1709+
query = format_query(
1710+
cr,
1711+
query,
1712+
extra_join=format_query(
1713+
cr,
1714+
"""
1715+
LEFT JOIN _upgrade_rrr AS _upgrade_rrr2
1716+
ON _upgrade_rrr.new = _upgrade_rrr2.new
1717+
AND _upgrade_rrr.old < _upgrade_rrr2.old
1718+
LEFT JOIN {table} t2
1719+
ON _upgrade_rrr2.old = t2.{res_id}
1720+
""",
1721+
table=ir.table,
1722+
res_id=ir.res_id,
1723+
),
1724+
)
17081725
conditions = []
17091726
for _, uniq_cols in unique_indexes:
17101727
uniq_cols = set(uniq_cols) - {ir.res_id, ir.res_model, ir.res_model_id} # noqa: PLW2901
17111728
conditions.append(
17121729
format_query(
17131730
cr,
1714-
"NOT EXISTS(SELECT 1 FROM {table} WHERE {res_model_whr} AND {jmap_expr} AND {where_clause})",
1731+
"""
1732+
-- there is no target already present within the same constraint
1733+
NOT EXISTS(SELECT 1 FROM {table} WHERE {res_model_whr} AND {jmap_expr} AND {where_clause})
1734+
-- there is no other entry with the same target within the same constraint
1735+
AND NOT (t2 IS NOT NULL AND {where_clause2})
1736+
""",
17151737
table=ir.table,
17161738
res_model_whr=res_model_whr,
17171739
jmap_expr=jmap_expr,
17181740
where_clause=SQLStr(" AND ".join(format_query(cr, "{0}=t.{0}", col) for col in uniq_cols))
17191741
if uniq_cols
17201742
else SQLStr("True"),
1743+
where_clause2=SQLStr(" AND ".join(format_query(cr, "t2.{0}=t.{0}", col) for col in uniq_cols))
1744+
if uniq_cols
1745+
else SQLStr("True"),
17211746
)
17221747
)
17231748
query = format_query(
@@ -1734,7 +1759,7 @@ def replace_record_references_batch(cr, id_mapping, model_src, model_dst=None, r
17341759
)
17351760
cr.execute(query, locals())
17361761
else:
1737-
fmt_query = cr.mogrify(query.format(**locals()), locals()).decode()
1762+
fmt_query = cr.mogrify(format_query(cr, query, extra_join=SQLStr("")), locals()).decode()
17381763
parallel_execute(cr, explode_query_range(cr, fmt_query, table=ir.table, alias="t"))
17391764

17401765
# reference fields

0 commit comments

Comments
 (0)