details:   https://code.tryton.org/tryton/commit/65be67251d10
branch:    default
user:      Cédric Krier <[email protected]>
date:      Fri Dec 26 10:58:30 2025 +0100
description:
        Use array as IN operators
diffstat:

 trytond/CHANGELOG                              |   1 +
 trytond/trytond/backend/database.py            |   4 ++
 trytond/trytond/backend/postgresql/database.py |  10 +++-
 trytond/trytond/model/fields/binary.py         |   8 ++--
 trytond/trytond/model/fields/field.py          |  41 +++++++++++++++++++++-
 trytond/trytond/model/fields/many2many.py      |   8 ++--
 trytond/trytond/model/fields/many2one.py       |  25 +++++++-----
 trytond/trytond/model/modelsql.py              |  48 ++++++++++++++-----------
 8 files changed, 99 insertions(+), 46 deletions(-)

diffs (434 lines):

diff -r 7df2a71c0551 -r 65be67251d10 trytond/CHANGELOG
--- a/trytond/CHANGELOG Tue Nov 25 15:05:02 2025 +0100
+++ b/trytond/CHANGELOG Fri Dec 26 10:58:30 2025 +0100
@@ -1,3 +1,4 @@
+* Use array for ``in`` operators
 * Update to Psycopg 3
 * Add support for Python 3.14
 * Remove support for Python 3.9
diff -r 7df2a71c0551 -r 65be67251d10 trytond/trytond/backend/database.py
--- a/trytond/trytond/backend/database.py       Tue Nov 25 15:05:02 2025 +0100
+++ b/trytond/trytond/backend/database.py       Fri Dec 26 10:58:30 2025 +0100
@@ -204,6 +204,10 @@
     def refresh_materialized_view(self, connection, view_name):
         raise NotImplementedError
 
+    @classmethod
+    def has_array(cls):
+        return False
+
     def sql_type(self, type_):
         pass
 
diff -r 7df2a71c0551 -r 65be67251d10 
trytond/trytond/backend/postgresql/database.py
--- a/trytond/trytond/backend/postgresql/database.py    Tue Nov 25 15:05:02 
2025 +0100
+++ b/trytond/trytond/backend/postgresql/database.py    Fri Dec 26 10:58:30 
2025 +0100
@@ -24,12 +24,12 @@
 from sql.aggregate import Count
 from sql.conditionals import Coalesce
 from sql.functions import Function
-from sql.operators import BinaryOperator, Concat, NotEqual
+from sql.operators import Any, BinaryOperator, Concat, NotEqual
 
 from trytond import __series__, config
 from trytond.backend.database import DatabaseInterface, SQLType
 from trytond.sql.operators import RangeOperator
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice
 
 from .table import index_method
 
@@ -487,7 +487,7 @@
         table = Table(table)
         cursor = connection.cursor()
         for sub_ids in grouped_slice(ids):
-            where = reduce_ids(table.id, sub_ids)
+            where = table.id == Any(list(sub_ids))
             query = table.select(
                 Literal(1), where=where, for_=For('UPDATE', nowait=True))
             cursor.execute(*query)
@@ -899,6 +899,10 @@
                 Identifier(view_name)
                 ))
 
+    @classmethod
+    def has_array(cls):
+        return True
+
 
 def convert_json(value):
     from trytond.protocols.jsonrpc import JSONDecoder
diff -r 7df2a71c0551 -r 65be67251d10 trytond/trytond/model/fields/binary.py
--- a/trytond/trytond/model/fields/binary.py    Tue Nov 25 15:05:02 2025 +0100
+++ b/trytond/trytond/model/fields/binary.py    Fri Dec 26 10:58:30 2025 +0100
@@ -6,10 +6,10 @@
 from sql import Column, Null
 
 from trytond.filestore import filestore
-from trytond.tools import cached_property, grouped_slice, reduce_ids
+from trytond.tools import cached_property, grouped_slice
 from trytond.transaction import Transaction
 
-from .field import Field
+from .field import SQL_OPERATORS, Field
 
 logger = logging.getLogger(__name__)
 
@@ -86,7 +86,7 @@
             for sub_ids in grouped_slice(ids):
                 cursor.execute(*table.select(
                         table.id, Column(table, self.file_id),
-                        where=reduce_ids(table.id, sub_ids)
+                        where=SQL_OPERATORS['in'](table.id, sub_ids)
                         & (Column(table, self.file_id) != Null)
                         & (Column(table, self.file_id) != '')))
                 for record_id, file_id in cursor:
@@ -129,7 +129,7 @@
                 columns = [Column(table, name)]
                 values = [self.sql_format(value)]
             cursor.execute(*table.update(columns, values,
-                    where=reduce_ids(table.id, ids)))
+                    where=SQL_OPERATORS['in'](table.id, ids)))
 
     def definition(self, model, language):
         definition = super().definition(model, language)
diff -r 7df2a71c0551 -r 65be67251d10 trytond/trytond/model/fields/field.py
--- a/trytond/trytond/model/fields/field.py     Tue Nov 25 15:05:02 2025 +0100
+++ b/trytond/trytond/model/fields/field.py     Fri Dec 26 10:58:30 2025 +0100
@@ -248,14 +248,51 @@
     'not like': partial(operators.NotLike, escape='\\'),
     'ilike': partial(operators.ILike, escape='\\'),
     'not ilike': partial(operators.NotILike, escape='\\'),
-    'in': operators.In,
-    'not in': operators.NotIn,
     '<=': operators.LessEqual,
     '>=': operators.GreaterEqual,
     '<': operators.Less,
     '>': operators.Greater,
     }
 
+if backend.Database.has_array():
+    class _EqualArray(operators.Equal):
+        def __invert__(self):
+            return _NotEqualArray(self.left, operators.All(self.right.operand))
+
+    class _NotEqualArray(operators.NotEqual):
+        def __invert__(self):
+            return _EqualArray(self.left, operators.Any(self.right.operand))
+
+    def in_array(left, right):
+        if isinstance(right, (Query, Expression)):
+            return operators.In(left, right)
+        if not isinstance(right, (list, tuple)):
+            right = list(right)
+        return _EqualArray(left, operators.Any(right))
+
+    def not_in_array(left, right):
+        if isinstance(right, (Query, Expression)):
+            return operators.NotIn(left, right)
+        if not isinstance(right, (list, tuple)):
+            right = list(right)
+        return _NotEqualArray(left, operators.All(right))
+
+    SQL_OPERATORS['in'] = in_array
+    SQL_OPERATORS['not in'] = not_in_array
+else:
+    def in_(left, right):
+        if not isinstance(right, (list, tuple, Query, Expression)):
+            right = list(right)
+        return operators.In(left, right)
+
+    def not_in(left, right):
+        if not isinstance(right, (list, tuple, Query, Expression)):
+            right = list(right)
+        return operators.NotIn(left, right)
+
+    SQL_OPERATORS['in'] = in_
+    SQL_OPERATORS['not in'] = not_in
+
 
 class Field(object):
     _type = None
diff -r 7df2a71c0551 -r 65be67251d10 trytond/trytond/model/fields/many2many.py
--- a/trytond/trytond/model/fields/many2many.py Tue Nov 25 15:05:02 2025 +0100
+++ b/trytond/trytond/model/fields/many2many.py Fri Dec 26 10:58:30 2025 +0100
@@ -13,9 +13,9 @@
 from trytond.transaction import Transaction
 
 from .field import (
-    Field, context_validate, domain_method, domain_validate, get_eval_fields,
-    instanciate_values, instantiate_context, search_order_validate,
-    size_validate)
+    SQL_OPERATORS, Field, context_validate, domain_method, domain_validate,
+    get_eval_fields, instanciate_values, instantiate_context,
+    search_order_validate, size_validate)
 
 
 class Many2Many(Field):
@@ -359,7 +359,7 @@
         if not ids:
             expression = Literal(False)
         else:
-            expression = table.id.in_(ids)
+            expression = SQL_OPERATORS['in'](table.id, ids)
         if operator.startswith('not'):
             return ~expression
         return expression
diff -r 7df2a71c0551 -r 65be67251d10 trytond/trytond/model/fields/many2one.py
--- a/trytond/trytond/model/fields/many2one.py  Tue Nov 25 15:05:02 2025 +0100
+++ b/trytond/trytond/model/fields/many2one.py  Fri Dec 26 10:58:30 2025 +0100
@@ -13,12 +13,12 @@
 import trytond.config as config
 from trytond.pool import Pool
 from trytond.pyson import PYSONEncoder
-from trytond.tools import cached_property, reduce_ids
+from trytond.tools import cached_property
 from trytond.transaction import Transaction, inactive_records
 
 from .field import (
-    Field, context_validate, domain_method, instantiate_context, order_method,
-    search_order_validate)
+    SQL_OPERATORS, Field, context_validate, domain_method, instantiate_context,
+    order_method, search_order_validate)
 
 
 class Many2One(Field):
@@ -131,12 +131,13 @@
         cursor = Transaction().connection.cursor()
         table, _ = tables[None]
         name, operator, ids = domain
-        red_sql = reduce_ids(table.id, (i for i in ids if i is not None))
+        where = SQL_OPERATORS['in'](
+            table.id, [int(i) for i in ids if i is not None])
         Target = self.get_target()
         path_column = getattr(Target, self.path).sql_column(tables, Target)
         path_column = Coalesce(path_column, '')
         cursor.execute(*table.select(
-                path_column, where=red_sql,
+                path_column, where=where,
                 order_by=[CharLength(path_column).desc, path_column.asc]))
         if operator.endswith('child_of'):
             paths = set()
@@ -152,7 +153,7 @@
                 where.append(path_column.like(path + '%'))
         else:
             ids = [int(x) for path, in cursor for x in path.split('/')[:-1]]
-            where = reduce_ids(table.id, ids)
+            where = SQL_OPERATORS['in'](table.id, ids)
         if not where:
             where = Literal(False)
         if operator.startswith('not'):
@@ -163,12 +164,13 @@
         cursor = Transaction().connection.cursor()
         table, _ = tables[None]
         name, operator, ids = domain
-        red_sql = reduce_ids(table.id, (i for i in ids if i is not None))
+        where = SQL_OPERATORS['in'](
+            table.id, [int(i) for i in ids if i is not None])
         Target = self.get_target()
         left = getattr(Target, self.left).sql_column(tables, Target)
         right = getattr(Target, self.right).sql_column(tables, Target)
         cursor.execute(*table.select(
-                left, right, where=red_sql,
+                left, right, where=where,
                 order_by=[(right - left).asc, left.asc]))
         ranges = set()
         for l, r in cursor:
@@ -195,18 +197,19 @@
         target = Target.__table__()
         table, _ = tables[None]
         name, operator, ids = domain
-        red_sql = reduce_ids(target.id, (i for i in ids if i is not None))
+        where = SQL_OPERATORS['in'](
+            target.id, [int(i) for i in ids if i is not None])
 
         if operator.endswith('child_of'):
             tree = With('id', recursive=True)
-            tree.query = target.select(target.id, where=red_sql)
+            tree.query = target.select(target.id, where=where)
             tree.query |= (target
                 .join(tree, condition=Column(target, name) == tree.id)
                 .select(target.id))
         else:
             tree = With('id', name, recursive=True)
             tree.query = target.select(
-                target.id, Column(target, name), where=red_sql)
+                target.id, Column(target, name), where=where)
             tree.query |= (target
                 .join(tree, condition=target.id == Column(tree, name))
                 .select(target.id, Column(target, name)))
diff -r 7df2a71c0551 -r 65be67251d10 trytond/trytond/model/modelsql.py
--- a/trytond/trytond/model/modelsql.py Tue Nov 25 15:05:02 2025 +0100
+++ b/trytond/trytond/model/modelsql.py Fri Dec 26 10:58:30 2025 +0100
@@ -21,7 +21,7 @@
 from trytond.pyson import PYSONDecoder, PYSONEncoder
 from trytond.rpc import RPC
 from trytond.sql.functions import Range
-from trytond.tools import cursor_dict, grouped_slice, reduce_ids
+from trytond.tools import cursor_dict, grouped_slice
 from trytond.tools.domain_inversion import simplify
 from trytond.transaction import (
     Transaction, inactive_records, record_cache_size, without_check_access)
@@ -758,7 +758,7 @@
         columns = [
             Coalesce(table.write_date, table.create_date), table.id, user.name]
         for sub_ids in grouped_slice(ids):
-            where = reduce_ids(table.id, sub_ids)
+            where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
             cursor.execute(*table.join(user, 'LEFT',
                     Coalesce(table.write_uid, table.create_uid) == user.id)
                 .select(*columns, where=where, group_by=columns))
@@ -799,7 +799,7 @@
             hcolumns.append(Column(history, fname))
         for sub_ids in grouped_slice(ids):
             if not deleted:
-                where = reduce_ids(table.id, sub_ids)
+                where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
                 cursor.execute(*history.insert(hcolumns,
                         table.select(*columns, where=where)))
             else:
@@ -871,7 +871,7 @@
             else:
                 hwhere = (column_datetime < datetime)
 
-            hwhere &= reduce_ids(history.id, sub_ids)
+            hwhere &= fields.SQL_OPERATORS['in'](history.id, sub_ids)
             history_select.query.where = hwhere
 
             cursor.execute(*history_values)
@@ -885,8 +885,9 @@
             # we need to skip the deleted IDs that are all None history records
             # because they could fail the UPDATE
             if to_delete:
-                history_select.query.where &= ~history.id.in_(
-                    list(deleted_sub_ids))
+                history_select.query.where &= (
+                    fields.SQL_OPERATORS['not in'](
+                        history.id, list(deleted_sub_ids)))
 
             # Some of the sub_ids are not updated because they are not in the
             # table anymore, they should be undeleted from the value in the
@@ -908,14 +909,15 @@
                 cursor.execute(*update_query)
             if to_undelete:
                 history_select.query.where = (hwhere
-                    & history.id.in_(list(to_undelete)))
+                    & fields.SQL_OPERATORS['in'](
+                        history.id, list(to_undelete)))
                 cursor.execute(*table.insert(
                         columns,
                         history_values.select(*history_columns)))
 
         if to_delete:
             for sub_ids in grouped_slice(to_delete):
-                where = reduce_ids(table.id, sub_ids)
+                where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
                 cursor.execute(*table.delete(where=where))
             cls._insert_history(list(to_delete), True)
         if to_update:
@@ -1256,7 +1258,7 @@
             from_ = convert_from(None, tables)
             for sub_ids in grouped_slice(ids, in_max):
                 sub_ids = list(sub_ids)
-                red_sql = reduce_ids(table.id, sub_ids)
+                red_sql = fields.SQL_OPERATORS['in'](table.id, sub_ids)
                 where = red_sql
                 if history_clause:
                     where &= history_clause
@@ -1517,10 +1519,10 @@
                         update_values.append(field.sql_format(value))
 
             for sub_ids in grouped_slice(ids):
-                red_sql = reduce_ids(table.id, sub_ids)
+                where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
                 try:
-                    cursor.execute(*table.update(columns, update_values,
-                            where=red_sql))
+                    cursor.execute(*table.update(
+                            columns, update_values, where=where))
                 except (
                         backend.DatabaseIntegrityError,
                         backend.DatabaseDataError) as exception:
@@ -1591,7 +1593,8 @@
         for fname in cls._mptt_fields:
             tree_ids[fname] = []
             for sub_ids in grouped_slice(ids):
-                where = reduce_ids(Column(table, fname), sub_ids)
+                where = fields.SQL_OPERATORS['in'](
+                    Column(table, fname), sub_ids)
                 cursor.execute(*table.select(table.id, where=where))
                 tree_ids[fname] += [x[0] for x in cursor]
 
@@ -1628,12 +1631,13 @@
                         Column(table, n) for n in foreign_fields_to_clean]
                     cursor.execute(*table.update(
                             columns, [table.id] * len(foreign_fields_to_clean),
-                            where=reduce_ids(table.id, sub_ids)))
+                            where=fields.SQL_OPERATORS['in'](table.id, sub_ids)
+                            ))
 
         def get_related_records(Model, field_name, sub_ids):
             if issubclass(Model, ModelSQL):
                 foreign_table = Model.__table__()
-                foreign_red_sql = reduce_ids(
+                foreign_red_sql = fields.SQL_OPERATORS['in'](
                     Column(foreign_table, field_name), sub_ids)
                 cursor.execute(*foreign_table.select(foreign_table.id,
                         where=foreign_red_sql))
@@ -1650,7 +1654,7 @@
         for sub_ids, sub_records in zip(
                 grouped_slice(ids), grouped_slice(records)):
             sub_ids = list(sub_ids)
-            red_sql = reduce_ids(table.id, sub_ids)
+            red_sql = fields.SQL_OPERATORS['in'](table.id, sub_ids)
 
             for Model, field_name in foreign_keys_toupdate:
                 related_records = get_related_records(
@@ -1725,7 +1729,7 @@
             from_ = convert_from(None, tables)
             for sub_ids in grouped_slice(ids, in_max):
                 sub_ids = set(sub_ids)
-                where = reduce_ids(table.id, sub_ids)
+                where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
                 if history_clause:
                     where &= history_clause
                 if domain:
@@ -2114,7 +2118,7 @@
                                     where=parent.id == parent_column),
                                 ''), table.id), '/')])
             for sub_ids in grouped_slice(ids):
-                query.where = reduce_ids(table.id, sub_ids)
+                query.where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
                 cursor.execute(*query)
 
     @classmethod
@@ -2127,7 +2131,7 @@
 
         def update_path(query, column, sub_ids):
             updated = set()
-            query.where = reduce_ids(table.id, sub_ids)
+            query.where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
             cursor.execute(*query)
             for old_path, new_path in cursor:
                 if old_path == new_path:
@@ -2293,7 +2297,7 @@
                 columns.insert(0, table.id)
                 in_max = transaction.database.IN_MAX // (len(columns) + 1)
                 for sub_ids in grouped_slice(ids, in_max):
-                    where = reduce_ids(table.id, sub_ids)
+                    where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
                     if isinstance(sql, Exclude) and sql.where:
                         where &= sql.where
 
@@ -2329,9 +2333,9 @@
                         raise SQLConstraintError(gettext(error))
             elif isinstance(sql, Check):
                 for sub_ids in grouped_slice(ids):
-                    red_sql = reduce_ids(table.id, sub_ids)
+                    where = fields.SQL_OPERATORS['in'](table.id, sub_ids)
                     cursor.execute(*table.select(table.id,
-                            where=~sql.expression & red_sql,
+                            where=~sql.expression & where,
                             limit=1))
                     if cursor.fetchone():
                         raise SQLConstraintError(gettext(error))

Reply via email to