@@ -1229,7 +1229,7 @@ class TestQueries(TransactionCase):
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT FROM "res_partner"
|
||||
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name"::text LIKE %s))
|
||||
LIMIT 1
|
||||
LIMIT %s
|
||||
) t
|
||||
''']):
|
||||
Model.search_count([('name', 'like', 'foo')], limit=1)
|
||||
@@ -1294,7 +1294,7 @@ class TestQueries(TransactionCase):
|
||||
OR ("ir_model"."model"::text ILIKE %s)
|
||||
)
|
||||
ORDER BY "ir_model"."model"
|
||||
LIMIT 100
|
||||
LIMIT %s
|
||||
''']):
|
||||
Model.name_search('foo')
|
||||
|
||||
@@ -1306,7 +1306,7 @@ class TestQueries(TransactionCase):
|
||||
AND (("ir_model"."model"::text NOT ILIKE %s) OR "ir_model"."model" IS NULL)
|
||||
)
|
||||
ORDER BY "ir_model"."model"
|
||||
LIMIT 100
|
||||
LIMIT %s
|
||||
''']):
|
||||
Model.name_search('foo', operator='not ilike')
|
||||
|
||||
@@ -1426,7 +1426,7 @@ class TestMany2one(TransactionCase):
|
||||
FROM "res_company"
|
||||
WHERE (("res_company"."active" = %s) AND ("res_company"."name"::text LIKE %s))
|
||||
ORDER BY "res_company"."id"
|
||||
LIMIT 1
|
||||
LIMIT %s
|
||||
))
|
||||
ORDER BY "res_partner"."complete_name"asc,"res_partner"."id"desc
|
||||
''']):
|
||||
|
||||
+109
-98
@@ -1,21 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
from typing import Union
|
||||
|
||||
from odoo.tools.sql import make_identifier
|
||||
|
||||
IDENT_RE = re.compile(r'^[a-z_][a-z0-9_$]*$', re.I)
|
||||
from odoo.tools.sql import make_identifier, SQL, IDENT_RE
|
||||
|
||||
|
||||
def _from_table(table, alias):
|
||||
""" Return a FROM clause element from ``table`` and ``alias``. """
|
||||
if alias == table:
|
||||
return f'"{alias}"'
|
||||
elif IDENT_RE.match(table):
|
||||
return f'"{table}" AS "{alias}"'
|
||||
else:
|
||||
return f'({table}) AS "{alias}"'
|
||||
def _sql_table(table) -> SQL:
|
||||
""" Wrap an optional table as an SQL object. """
|
||||
if isinstance(table, str):
|
||||
return SQL.identifier(table) if IDENT_RE.match(table) else SQL(f"({table})")
|
||||
return table
|
||||
|
||||
|
||||
def _sql_from_table(alias, table) -> SQL:
|
||||
""" Return a FROM clause element from ``alias`` and ``table``. """
|
||||
if table is None:
|
||||
return SQL.identifier(alias)
|
||||
return SQL("%s AS %s", table, SQL.identifier(alias))
|
||||
|
||||
|
||||
def _sql_from_join(kind, alias, table, condition) -> SQL:
|
||||
""" Return a FROM clause element for a JOIN. """
|
||||
return SQL("%s %s ON (%s)", kind, _sql_from_table(alias, table), condition)
|
||||
|
||||
|
||||
_SQL_JOINS = {
|
||||
"JOIN": SQL("JOIN"),
|
||||
"LEFT JOIN": SQL("LEFT JOIN"),
|
||||
}
|
||||
|
||||
|
||||
def _generate_table_alias(src_table_alias, link):
|
||||
@@ -45,24 +58,22 @@ class Query(object):
|
||||
|
||||
:param cr: database cursor (for lazy evaluation)
|
||||
:param alias: name or alias of the table
|
||||
:param table: if given, a table expression (identifier or query)
|
||||
:param table: a table expression (``str`` or ``SQL`` object), optional
|
||||
"""
|
||||
|
||||
def __init__(self, cr, alias, table=None):
|
||||
def __init__(self, cr, alias: str, table: Union[str, SQL, None] = None):
|
||||
# database cursor
|
||||
self._cr = cr
|
||||
|
||||
# tables {alias: table}
|
||||
self._tables = {alias: table or alias}
|
||||
# tables {alias: table(SQL|None)}
|
||||
self._tables = {alias: _sql_table(table)}
|
||||
|
||||
# joins {alias: (kind, table, condition, condition_params)}
|
||||
# joins {alias: (kind(SQL), table(SQL|None), condition(SQL))}
|
||||
self._joins = {}
|
||||
self._raw_joins = {} # {query: params}
|
||||
|
||||
# holds the list of WHERE clause elements (to be joined with 'AND'), and
|
||||
# the list of parameters
|
||||
# holds the list of WHERE conditions (to be joined with 'AND')
|
||||
self._where_clauses = []
|
||||
self._where_params = []
|
||||
|
||||
# order, limit, offset
|
||||
self.order = None
|
||||
@@ -72,16 +83,15 @@ class Query(object):
|
||||
# memoized result
|
||||
self._ids = None
|
||||
|
||||
def add_table(self, alias, table=None):
|
||||
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, "Alias %r already in %s" % (alias, str(self))
|
||||
self._tables[alias] = table or alias
|
||||
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_where(self, where_clause, where_params=()):
|
||||
def add_where(self, where_clause: Union[str, SQL], where_params=()):
|
||||
""" Add a condition to the where clause. """
|
||||
self._where_clauses.append(where_clause)
|
||||
self._where_params.extend(where_params)
|
||||
self._where_clauses.append(SQL(where_clause, *where_params))
|
||||
self._ids = None
|
||||
|
||||
def join(self, lhs_alias, lhs_column, rhs_table, rhs_column, link, extra=None, extra_params=()):
|
||||
@@ -140,88 +150,101 @@ 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 = f'"{lhs_alias}"."{lhs_column}" = "{rhs_alias}"."{rhs_column}"'
|
||||
condition = SQL("%s = %s", SQL.identifier(lhs_alias, lhs_column), SQL.identifier(rhs_alias, rhs_column))
|
||||
if extra:
|
||||
condition = condition + " AND " + extra.format(lhs=lhs_alias, rhs=rhs_alias)
|
||||
condition_params = list(extra_params)
|
||||
if kind:
|
||||
self._joins[rhs_alias] = (kind, rhs_table, condition, condition_params)
|
||||
else:
|
||||
self._tables[rhs_alias] = rhs_table
|
||||
self.add_where(condition, condition_params)
|
||||
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
|
||||
|
||||
return rhs_alias
|
||||
|
||||
def select(self, *args):
|
||||
""" Return the SELECT query as a pair ``(query_string, query_params)``. """
|
||||
from_clause, where_clause, params = self.get_sql()
|
||||
query_str = 'SELECT {} FROM {} WHERE {}{}{}{}'.format(
|
||||
", ".join(args or [f'"{next(iter(self._tables))}"."id"']),
|
||||
from_clause,
|
||||
where_clause or "TRUE",
|
||||
(" ORDER BY %s" % self.order) if self.order else "",
|
||||
(" LIMIT %d" % self.limit) if self.limit else "",
|
||||
(" OFFSET %d" % self.offset) if self.offset else "",
|
||||
)
|
||||
return query_str, params
|
||||
@property
|
||||
def table(self) -> str:
|
||||
""" Return the query's main table, i.e., the first one in the FROM clause. """
|
||||
return next(iter(self._tables))
|
||||
|
||||
def subselect(self, *args):
|
||||
@property
|
||||
def from_clause(self) -> SQL:
|
||||
""" Return the FROM clause of ``self``, without the FROM keyword. """
|
||||
tables = SQL(", ").join(
|
||||
_sql_from_table(alias, table)
|
||||
for alias, table in self._tables.items()
|
||||
)
|
||||
if not (self._joins or self._raw_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
|
||||
def where_clause(self) -> SQL:
|
||||
""" Return the WHERE condition of ``self``, without the WHERE keyword. """
|
||||
return SQL(" AND ").join(self._where_clauses)
|
||||
|
||||
def is_empty(self):
|
||||
""" Return whether the query is known to return nothing. """
|
||||
return self._ids == ()
|
||||
|
||||
def select(self, *args: Union[str, SQL]) -> SQL:
|
||||
""" Return the SELECT query as an ``SQL`` object. """
|
||||
sql_args = map(SQL, args) if args else [SQL.identifier(self.table, 'id')]
|
||||
return SQL(
|
||||
"%s%s%s%s%s%s",
|
||||
SQL("SELECT %s", SQL(", ").join(sql_args)),
|
||||
SQL(" FROM %s", self.from_clause),
|
||||
SQL(" WHERE %s", self.where_clause) if self._where_clauses else SQL(),
|
||||
SQL(" ORDER BY %s", SQL(self.order)) if self.order else SQL(),
|
||||
SQL(" LIMIT %s", self.limit) if self.limit else SQL(),
|
||||
SQL(" OFFSET %s", self.offset) if self.offset else SQL(),
|
||||
)
|
||||
|
||||
def subselect(self, *args: Union[str, SQL]) -> SQL:
|
||||
""" Similar to :meth:`.select`, but for sub-queries.
|
||||
This one avoids the ORDER BY clause when possible,
|
||||
and includes parentheses around the subquery.
|
||||
"""
|
||||
if self._ids is not None and not args:
|
||||
# inject the known result instead of the subquery
|
||||
return "%s", [self._ids or (None,)]
|
||||
return SQL("%s", self._ids or (None,))
|
||||
|
||||
if self.limit or self.offset:
|
||||
# in this case, the ORDER BY clause is necessary
|
||||
query_str, params = self.select(*args)
|
||||
return f"({query_str})", params
|
||||
return SQL("(%s)", self.select(*args))
|
||||
|
||||
from_clause, where_clause, params = self.get_sql()
|
||||
query_str = '(SELECT {} FROM {} WHERE {})'.format(
|
||||
", ".join(args or [f'"{next(iter(self._tables))}"."id"']),
|
||||
from_clause,
|
||||
where_clause or "TRUE",
|
||||
sql_args = map(SQL, args) if args else [SQL.identifier(self.table, 'id')]
|
||||
return SQL(
|
||||
"(%s%s%s)",
|
||||
SQL("SELECT %s", SQL(", ").join(sql_args)),
|
||||
SQL(" FROM %s", self.from_clause),
|
||||
SQL(" WHERE %s", self.where_clause) if self._where_clauses else SQL(),
|
||||
)
|
||||
return query_str, params
|
||||
|
||||
def is_empty(self):
|
||||
""" Return whether the query is known to return nothing. """
|
||||
return self._ids == ()
|
||||
|
||||
def get_sql(self):
|
||||
""" Returns (query_from, query_where, query_params). """
|
||||
tables = [_from_table(table, alias) for alias, table in self._tables.items()]
|
||||
joins = []
|
||||
params = []
|
||||
for alias, (kind, table, condition, condition_params) in self._joins.items():
|
||||
joins.append(f'{kind} {_from_table(table, alias)} ON ({condition})')
|
||||
params.extend(condition_params)
|
||||
|
||||
for join_query, query_params in self._raw_joins.items():
|
||||
joins.append(join_query)
|
||||
params.extend(query_params)
|
||||
|
||||
from_clause = " ".join([", ".join(tables)] + joins)
|
||||
where_clause = " AND ".join(self._where_clauses)
|
||||
return from_clause, where_clause, params + self._where_params
|
||||
from_string, from_params = self.from_clause
|
||||
where_string, where_params = self.where_clause
|
||||
return from_string, where_string, from_params + where_params
|
||||
|
||||
def get_result_ids(self):
|
||||
""" Return the result of ``self.select()`` as a tuple of ids. The result
|
||||
is memoized for future use, which avoids making the same query twice.
|
||||
"""
|
||||
if self._ids is None:
|
||||
query_str, params = self.select()
|
||||
self._cr.execute(query_str, params)
|
||||
self._cr.execute(self.select())
|
||||
self._ids = tuple(row[0] for row in self._cr.fetchall())
|
||||
return self._ids
|
||||
|
||||
@@ -242,20 +265,20 @@ class Query(object):
|
||||
# FROM "stuff"
|
||||
# JOIN (SELECT * FROM unnest(%s) WITH ORDINALITY) AS "stuff__ids"
|
||||
# ON ("stuff"."id" = "stuff__ids"."unnest")
|
||||
# WHERE TRUE
|
||||
# ORDER BY "stuff__ids"."ordinality"
|
||||
alias = self.join(
|
||||
next(iter(self._tables)), 'id',
|
||||
'SELECT * FROM unnest(%s) WITH ORDINALITY', 'unnest',
|
||||
'ids', extra_params=[list(ids)],
|
||||
self.table, 'id',
|
||||
SQL('(SELECT * FROM unnest(%s) WITH ORDINALITY)', list(ids)), 'unnest',
|
||||
'ids',
|
||||
)
|
||||
self.order = f'"{alias}"."ordinality"'
|
||||
self.order = SQL.identifier(alias, 'ordinality')
|
||||
else:
|
||||
self.add_where(f'"{next(iter(self._tables))}"."id" IN %s', [ids])
|
||||
self.add_where(SQL("%s IN %s", SQL.identifier(self.table, 'id'), ids))
|
||||
self._ids = ids
|
||||
|
||||
def __str__(self):
|
||||
return '<osv.Query: %r with params: %r>' % self.select()
|
||||
sql = self.select()
|
||||
return f"<Query: {sql.code!r} with params: {sql.params!r}>"
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.get_result_ids())
|
||||
@@ -264,24 +287,12 @@ class Query(object):
|
||||
if self._ids is None:
|
||||
if self.limit or self.offset:
|
||||
# optimization: generate a SELECT FROM, and then count the rows
|
||||
query_str, params = self.select('')
|
||||
query_str = f'SELECT COUNT(*) FROM ({query_str}) t'
|
||||
sql = SQL("SELECT COUNT(*) FROM (%s) t", self.select(""))
|
||||
else:
|
||||
query_str, params = self.select('COUNT(*)')
|
||||
self._cr.execute(query_str, params)
|
||||
sql = self.select('COUNT(*)')
|
||||
self._cr.execute(sql)
|
||||
return self._cr.fetchone()[0]
|
||||
return len(self.get_result_ids())
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.get_result_ids())
|
||||
|
||||
#
|
||||
# deprecated attributes and methods
|
||||
#
|
||||
@property
|
||||
def where_clause(self):
|
||||
return tuple(self._where_clauses)
|
||||
|
||||
@property
|
||||
def where_clause_params(self):
|
||||
return tuple(self._where_params)
|
||||
|
||||
Reference in New Issue
Block a user