diff --git a/odoo/models.py b/odoo/models.py index 3b8c760db8d..7ca689c4c35 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2808,7 +2808,7 @@ class BaseModel(metaclass=MetaModel): definition = self.get_property_definition(f"{fname}.{property_name}") property_sql = f""""{alias}"."{fname}" -> '{property_name}'""" property_type = definition.get('type') - property_alias = tools._generate_table_alias(alias, f'{fname}_{property_name}') + property_alias = query.make_alias(alias, f'{fname}_{property_name}') # JOIN on the JSON array if property_type in ('tags', 'many2many'): @@ -2819,37 +2819,46 @@ class BaseModel(metaclass=MetaModel): ELSE '[]'::jsonb END ''' - join_property = f'LEFT JOIN jsonb_array_elements({property_sql}) {property_alias}' if property_type == 'tags': # ignore invalid tags tags = [tag[0] for tag in definition.get('tags') or []] # `->>0 : convert "JSON string" into string - expr = f'{join_property} ON {property_alias}->>0 = ANY(%s::text[])' - query._raw_joins[expr] = [tags] - - elif property_type == 'many2many': + condition = SQL( + "%s->>0 = ANY(%s::text[])", + SQL.identifier(property_alias), tags, + ) + else: comodel = self.env.get(definition.get('comodel')) if comodel is None or comodel._transient or comodel._abstract: # all value are false, because the model does not exist anymore # (or is a transient model e.g.) - query._raw_joins[f'{join_property} ON FALSE'] = [] + condition = SQL("FALSE") else: # check the existences of the many2many - expr = f'{join_property} ON {property_alias}::int IN (SELECT id FROM {comodel._table})' - query._raw_joins[expr] = [] + condition = SQL( + "%s::int IN (SELECT id FROM %s)", + SQL.identifier(property_alias), SQL.identifier(comodel._table), + ) + + query.add_join( + "LEFT JOIN", + property_alias, + SQL(f"jsonb_array_elements({property_sql})"), + condition, + ) return property_alias elif property_type == 'selection': - options = definition.get('selection') or [] - options = [option[0] for option in options] + options = [option[0] for option in definition.get('selection') or ()] # check the existence of the option - expr = f''' - LEFT JOIN (SELECT unnest(%s::text[]) {property_alias}) {property_alias} - ON {property_sql}->>0 = {property_alias} - ''' - query._raw_joins[expr] = [options] + query.add_join( + "LEFT JOIN", + property_alias, + SQL("(SELECT unnest(%s::text[]) %s)", options, SQL.identifier(property_alias)), + SQL(f"{property_sql}->>0 = %s", SQL.identifier(property_alias)), + ) return property_alias diff --git a/odoo/tools/query.py b/odoo/tools/query.py index 041efce5289..29f0cddfb94 100644 --- a/odoo/tools/query.py +++ b/odoo/tools/query.py @@ -70,7 +70,6 @@ class Query(object): # joins {alias: (kind(SQL), table(SQL|None), condition(SQL))} self._joins = {} - self._raw_joins = {} # {query: params} # holds the list of WHERE conditions (to be joined with 'AND') self._where_clauses = [] @@ -83,12 +82,29 @@ class Query(object): # memoized result self._ids = None + def make_alias(self, alias: str, link: str) -> str: + """ Return an alias based on ``alias`` and ``link``. """ + return _generate_table_alias(alias, link) + def add_table(self, alias: str, table: Union[str, SQL, None] = None): """ Add a table with a given alias to the from clause. """ assert alias not in self._tables and alias not in self._joins, f"Alias {alias!r} already in {self}" self._tables[alias] = _sql_table(table) self._ids = None + def add_join(self, kind: str, alias: str, table: Union[str, SQL, None], condition: SQL): + """ Add a join clause with the given alias, table and condition. """ + sql_kind = _SQL_JOINS.get(kind.upper()) + assert sql_kind is not None, f"Invalid JOIN type {kind!r}" + assert alias not in self._tables, f"Alias {alias!r} already used" + table = _sql_table(table) + + if alias in self._joins: + assert self._joins[alias] == (sql_kind, table, condition) + else: + self._joins[alias] = (sql_kind, table, condition) + self._ids = None + def add_where(self, where_clause: Union[str, SQL], where_params=()): """ Add a condition to the where clause. """ self._where_clauses.append(SQL(where_clause, *where_params)) @@ -150,23 +166,14 @@ class Query(object): return self._join('LEFT JOIN', lhs_alias, lhs_column, rhs_table, rhs_column, link, extra, extra_params) def _join(self, kind, lhs_alias, lhs_column, rhs_table, rhs_column, link, extra=None, extra_params=()): - sql_kind = _SQL_JOINS.get(kind.upper()) - assert sql_kind is not None, f"Invalid JOIN type {kind!r}" - assert lhs_alias in self._tables or lhs_alias in self._joins, "Alias %r not in %s" % (lhs_alias, str(self)) - - rhs_alias = _generate_table_alias(lhs_alias, link) - assert rhs_alias not in self._tables, "Alias %r already in %s" % (rhs_alias, str(self)) - - if rhs_alias not in self._joins: - condition = SQL("%s = %s", SQL.identifier(lhs_alias, lhs_column), SQL.identifier(rhs_alias, rhs_column)) - if extra: - extra = SQL(extra, *extra_params) - extra = SQL(extra.code.format(lhs=lhs_alias, rhs=rhs_alias), *extra.params) - condition = SQL("%s AND %s", condition, extra) - self._joins[rhs_alias] = (sql_kind, _sql_table(rhs_table), condition) - self._ids = None - + rhs_alias = self.make_alias(lhs_alias, link) + condition = SQL("%s = %s", SQL.identifier(lhs_alias, lhs_column), SQL.identifier(rhs_alias, rhs_column)) + if extra: + extra = SQL(extra, *extra_params) + extra = SQL(extra.code.format(lhs=lhs_alias, rhs=rhs_alias), *extra.params) + condition = SQL("%s AND %s", condition, extra) + self.add_join(kind, rhs_alias, rhs_table, condition) return rhs_alias @property @@ -181,13 +188,11 @@ class Query(object): _sql_from_table(alias, table) for alias, table in self._tables.items() ) - if not (self._joins or self._raw_joins): + if not self._joins: return tables items = [tables] for alias, (kind, table, condition) in self._joins.items(): items.append(_sql_from_join(kind, alias, table, condition)) - for join_query, join_params in self._raw_joins.items(): - items.append(SQL(join_query, *join_params)) return SQL(" ").join(items) @property