Skip to content

Commit 5c81d74

Browse files
committed
fixup! [ADD] util/update_table_from_dict
1 parent 27e73ac commit 5c81d74

File tree

2 files changed

+126
-49
lines changed

2 files changed

+126
-49
lines changed

src/base/tests/test_util.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -881,8 +881,8 @@ def test_parallel_execute_retry_on_serialization_failure(self):
881881
cr.execute(util.format_query(cr, "SELECT 1 FROM {}", TEST_TABLE_NAME))
882882
self.assertFalse(cr.rowcount)
883883

884-
def test_update_table_from_dict(self):
885-
TEST_TABLE_NAME = "_upgrade_update_table_from_dict_test_table"
884+
def test_update_one_col_from_dict(self):
885+
TEST_TABLE_NAME = "_upgrade_update_one_col_from_dict_test_table"
886886
N_ROWS = 10
887887

888888
cr = self._get_cr()
@@ -905,8 +905,8 @@ def test_update_table_from_dict(self):
905905
table=TEST_TABLE_NAME,
906906
)
907907
)
908-
mapping = {id: {"col1": id * 2} for id in range(1, N_ROWS + 1, 2)}
909-
util.update_table_from_dict(cr, TEST_TABLE_NAME, mapping)
908+
mapping = {id: id * 2 for id in range(1, N_ROWS + 1, 2)}
909+
util.update_table_from_dict(cr, TEST_TABLE_NAME, "col1", mapping)
910910

911911
cr.execute(
912912
util.format_query(
@@ -933,7 +933,62 @@ def test_update_table_from_dict(self):
933933
table=TEST_TABLE_NAME,
934934
)
935935
)
936-
self.assertFalse(cr.rowcount) # otherwise not all expected updates are performed
936+
self.assertFalse(cr.rowcount) # otherwise partial/incorrect updates are performed
937+
938+
def test_update_multiple_cols_from_dict(self):
939+
TEST_TABLE_NAME = "_upgrade_update_multiple_cols_from_dict_test_table"
940+
N_ROWS = 10
941+
942+
cr = self._get_cr()
943+
944+
cr.execute(
945+
util.format_query(
946+
cr,
947+
"""
948+
DROP TABLE IF EXISTS {table};
949+
950+
CREATE TABLE {table} (
951+
id SERIAL PRIMARY KEY,
952+
col1 INTEGER,
953+
col2 INTEGER,
954+
col3 INTEGER
955+
);
956+
957+
INSERT INTO {table} (col1, col2, col3) SELECT v, v, v FROM GENERATE_SERIES(1, %s) as v;
958+
"""
959+
% N_ROWS,
960+
table=TEST_TABLE_NAME,
961+
)
962+
)
963+
mapping = {id: [id * 2, id * 3] for id in range(1, N_ROWS + 1, 2)}
964+
util.update_table_from_dict(cr, TEST_TABLE_NAME, ["col1", "col2"], mapping)
965+
966+
cr.execute(
967+
util.format_query(
968+
cr,
969+
"SELECT id FROM {table} WHERE col3 != id",
970+
table=TEST_TABLE_NAME,
971+
)
972+
)
973+
self.assertFalse(cr.rowcount) # otherwise unintended column is affected
974+
975+
cr.execute(
976+
util.format_query(
977+
cr,
978+
"SELECT id FROM {table} WHERE col1 != id AND MOD(id, 2) = 0",
979+
table=TEST_TABLE_NAME,
980+
)
981+
)
982+
self.assertFalse(cr.rowcount) # otherwise unintended rows are affected
983+
984+
cr.execute(
985+
util.format_query(
986+
cr,
987+
"SELECT id FROM {table} WHERE (col1 != 2 * id OR col2 != 3 * id) AND MOD(id, 2) = 1",
988+
table=TEST_TABLE_NAME,
989+
)
990+
)
991+
self.assertFalse(cr.rowcount) # otherwise partial/incorrect updates are performed
937992

938993
def test_create_column_with_fk(self):
939994
cr = self.env.cr

src/util/pg.py

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1624,79 +1624,101 @@ def create_id_sequence(cr, table, set_as_default=True):
16241624
)
16251625

16261626

1627-
def update_table_from_dict(cr, table, mapping, key_col="id", bucket_size=DEFAULT_BUCKET_SIZE):
1627+
def update_table_from_dict(cr, table, columns, mapping, key_col="id", bucket_size=DEFAULT_BUCKET_SIZE):
16281628
"""
1629-
Update table's rows based on mapping.
1629+
Update table based on mapping.
16301630
1631-
Efficiently updates rows in a table by mapping an identifier column (`key_col`) value to the new values for the provided set of columns.
1631+
Each `mapping` entry defines the new values for the specified `columns` for the row(s) whose `key_col` value matches the key.
1632+
1633+
.. important::
1634+
1635+
`columns` can be either a string or a list of strings. Crucially the values of the provided `mapping` must have the same type
1636+
and, if a list, the same **dimensionality** and **order**. Otherwise:
1637+
1638+
.. code-block:: python
1639+
1640+
columns = ["int_col", "text_col"]
1641+
mapping = {
1642+
1: [123, "foo", True], # third value is ignored
1643+
2: [456], # text_col is set to NULL
1644+
3: ["bar", 789], # will attempt to set `int_col` to "bar" and `text_col` to 789
1645+
}
16321646
16331647
.. example::
16341648
16351649
.. code-block:: python
16361650
1651+
# single column update
16371652
util.update_table_from_dict(
16381653
cr,
16391654
"account_move",
1655+
"always_tax_eligible",
16401656
{
1641-
1: {"closing_return_id": 2, "always_tax_eligible": True},
1642-
2: {"closing_return_id": 3, "always_tax_eligible": False},
1657+
1: True,
1658+
2: False,
16431659
},
16441660
)
16451661
1646-
:param str table: the table to update
1647-
:param dict[any, dict[str, any]] mapping: mapping of `key_col` identifiers to maps of column names to their new value
1648-
1649-
.. example::
1650-
1651-
.. code-block:: python
1652-
1653-
mapping = {
1654-
1: {"col1": 123, "col2": "foo"},
1655-
2: {"col1": 456, "col2": "bar"},
1656-
}
1657-
1658-
.. warning::
1659-
1660-
All maps should have the exact same set of keys (column names). The following
1661-
example would behave unpredictably:
1662-
1663-
.. code-block:: python
1662+
# multi-column update
1663+
util.update_table_from_dict(
1664+
cr,
1665+
"account_move",
1666+
["closing_return_id", "always_tax_eligible"],
1667+
{
1668+
1: [2, True],
1669+
2: [3, False],
1670+
},
1671+
)
16641672
1665-
# WRONG
1666-
mapping = {
1667-
1: {"col1": 123, "col2": "foo"},
1668-
2: {"col1": 456},
1669-
}
1673+
.. warning::
16701674
1671-
Either resulting in `col2` updates being ignored or setting it to NULL for row 2.
1675+
As a side effect, the cursor may be committed.
16721676
1673-
:param str key_col: The column to match the key against (`id` by default)
1677+
:param str table: database's table to perform the update of
1678+
:param str | list[str] columns: table's columns to update
1679+
:param dict[any, any | list[any]] mapping: matches values of `key_col` to the new `columns` values
1680+
:param str key_col: column to match against keys of `mapping`
16741681
:param int bucket_size: maximum number of rows to update per single query
16751682
"""
1676-
if not mapping:
1677-
return
1678-
16791683
_validate_table(table)
1684+
if not columns or not mapping:
1685+
return
16801686

1681-
column_names = list(next(iter(mapping.values())).keys())
1682-
query = cr.mogrify(
1683-
format_query(
1687+
if isinstance(columns, str):
1688+
query = format_query(
16841689
cr,
16851690
"""
16861691
UPDATE {table} t
1687-
SET ({columns_list}) = ROW({values_list})
1688-
FROM JSONB_EACH(%%s) m
1692+
SET {col} = m.value::{col_type}
1693+
FROM JSONB_EACH_TEXT(%s) m
1694+
WHERE t.{key_col}::text = m.key
1695+
""",
1696+
table=table,
1697+
col=ColumnList.from_unquoted(cr, [columns]),
1698+
col_type=column_type(cr, table, columns),
1699+
key_col=key_col,
1700+
)
1701+
else:
1702+
query = format_query(
1703+
cr,
1704+
"""
1705+
UPDATE {table} t
1706+
SET ({cols}) = ROW({cols_values})
1707+
FROM JSONB_EACH(%s) m
16891708
WHERE t.{key_col}::varchar = m.key
16901709
""",
16911710
table=table,
1692-
columns_list=ColumnList.from_unquoted(cr, column_names),
1693-
values_list=sql.SQL(", ").join(
1694-
sql.SQL("(m.value->>%s)::{}").format(sql.SQL(column_type(cr, table, col))) for col in column_names
1711+
cols=ColumnList.from_unquoted(cr, columns),
1712+
cols_values=SQLStr(
1713+
", ".join(
1714+
"(m.value->>{:d})::{}".format(
1715+
col_idx, sql.Identifier(column_type(cr, table, col_name)).as_string(cr._cnx)
1716+
)
1717+
for col_idx, col_name in enumerate(columns)
1718+
)
16951719
),
16961720
key_col=key_col,
1697-
),
1698-
column_names,
1699-
)
1721+
)
17001722

17011723
if len(mapping) <= 1.1 * bucket_size:
17021724
cr.execute(query, [json.dumps(mapping)])

0 commit comments

Comments
 (0)