[IMP] core: introduce _order_to_sql() to replace _generate_order_by()
The new method should be used to generate an SQL object that represents how to order by a field in an SQL query. We introduced the auxiliary method _order_field_to_sql() so that one can specify some SQL for ordering by a given field with a simple method override. Part-of: odoo/odoo#138019
This commit is contained in:
@@ -686,7 +686,8 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
# get the where clause
|
||||
query = self._where_calc(list(self.env.context.get('domain_cumulated_balance') or []))
|
||||
order_string = ", ".join(self._generate_order_by_inner(self._table, self.env.context.get('order_cumulated_balance'), query, reverse_direction=True))
|
||||
sql_order = self._order_to_sql(self.env.context.get('order_cumulated_balance'), query, reverse=True)
|
||||
order_string = self.env.cr.mogrify(sql_order).decode()
|
||||
from_clause, where_clause, where_clause_params = query.get_sql()
|
||||
sql = """
|
||||
SELECT account_move_line.id, SUM(account_move_line.balance) OVER (
|
||||
|
||||
@@ -8,11 +8,12 @@ from datetime import timedelta
|
||||
|
||||
from odoo import api, Command, fields, models, _, _lt
|
||||
from odoo.addons.rating.models import rating_data
|
||||
from odoo.tools.misc import get_lang
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import get_lang, SQL
|
||||
from .project_update import STATUS_COLOR
|
||||
from .project_task import CLOSED_STATES
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
_name = "project.project"
|
||||
_description = "Project"
|
||||
@@ -552,29 +553,15 @@ class Project(models.Model):
|
||||
analytic_accounts_to_delete.unlink()
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None):
|
||||
if not order:
|
||||
return super()._search(domain, offset, limit, order, access_rights_uid)
|
||||
new_order, item_index, desc = [], -1, False
|
||||
for index, order_item in enumerate(order.split(',')):
|
||||
order_item_list = order_item.strip().lower().split(' ')
|
||||
if order_item_list[0] == 'is_favorite':
|
||||
item_index = index
|
||||
desc = order_item_list[-1] == 'desc'
|
||||
else:
|
||||
new_order.append(order_item)
|
||||
query = super()._search(domain, offset, limit, ', '.join(new_order), access_rights_uid)
|
||||
if item_index != -1:
|
||||
query_order_list = query.order.split(',') if query.order else []
|
||||
query_order_list.insert(item_index, f"""
|
||||
"project_project"."id" IN (
|
||||
SELECT project_id
|
||||
FROM project_favorite_user_rel
|
||||
WHERE user_id = {self.env.uid}
|
||||
){" DESC" * desc}""")
|
||||
query.order = ', '.join(query_order_list)
|
||||
return query
|
||||
def _order_field_to_sql(self, alias, field_name, direction, nulls, query):
|
||||
if field_name == 'is_favorite':
|
||||
sql_field = SQL(
|
||||
"%s IN (SELECT project_id FROM project_favorite_user_rel WHERE user_id = %s)",
|
||||
SQL.identifier(alias, 'id'), self.env.uid,
|
||||
)
|
||||
return SQL("%s %s %s", sql_field, direction, nulls)
|
||||
|
||||
return super()._order_field_to_sql(alias, field_name, direction, nulls, query)
|
||||
|
||||
def message_subscribe(self, partner_ids=None, subtype_ids=None):
|
||||
"""
|
||||
|
||||
@@ -123,7 +123,7 @@ class WebsiteForum(WebsiteProfile):
|
||||
# retro-compatibility for V8 and google links
|
||||
try:
|
||||
sorting = werkzeug.urls.url_unquote_plus(sorting)
|
||||
Post._generate_order_by(sorting, None)
|
||||
Post._order_to_sql(sorting, None)
|
||||
except (UserError, ValueError):
|
||||
sorting = False
|
||||
|
||||
|
||||
@@ -1281,7 +1281,7 @@ class TestQueries(TransactionCase):
|
||||
SELECT "res_partner_title"."id"
|
||||
FROM "res_partner_title"
|
||||
WHERE (COALESCE("res_partner_title"."name"->>%s, "res_partner_title"."name"->>'en_US') like %s)
|
||||
ORDER BY COALESCE("res_partner_title"."name"->>'fr_FR', "res_partner_title"."name"->>'en_US')
|
||||
ORDER BY COALESCE("res_partner_title"."name"->>%s, "res_partner_title"."name"->>'en_US')
|
||||
''']):
|
||||
Model.search([('name', 'like', 'foo')])
|
||||
|
||||
|
||||
+1
-1
@@ -4766,7 +4766,7 @@ class Many2many(_RelationalMulti):
|
||||
comodel._flush_search(domain, order=comodel._order)
|
||||
query = comodel._where_calc(domain)
|
||||
comodel._apply_ir_rules(query, 'read')
|
||||
query.order = comodel._generate_order_by(comodel._order, query).replace(' ORDER BY ', '')
|
||||
query.order = comodel._order_to_sql(comodel._order, query)
|
||||
|
||||
# join with many2many relation table
|
||||
sql_id1 = SQL.identifier(self.relation, self.column1)
|
||||
|
||||
+113
-90
@@ -5103,98 +5103,122 @@ class BaseModel(metaclass=MetaModel):
|
||||
if domain:
|
||||
expression.expression(domain, self.sudo(), self._table, query)
|
||||
|
||||
@api.model
|
||||
def _generate_m2o_order_by(self, alias, order_field, query, reverse_direction, seen):
|
||||
def _order_to_sql(self, order: str, query: Query, alias: (str | None) = None,
|
||||
reverse: bool = False) -> SQL:
|
||||
""" Return an :class:`SQL` object that represents the given ORDER BY
|
||||
clause, without the ORDER BY keyword.
|
||||
"""
|
||||
Add possibly missing JOIN to ``query`` and generate the ORDER BY clause for m2o fields,
|
||||
either native m2o fields or function/related fields that are stored, including
|
||||
intermediate JOINs for inheritance if required.
|
||||
|
||||
:return: the qualified field name to use in an ORDER BY clause to sort by ``order_field``
|
||||
"""
|
||||
field = self._fields[order_field]
|
||||
if field.inherited:
|
||||
# also add missing joins for reaching the table containing the m2o field
|
||||
qualified_field = self._inherits_join_calc(alias, order_field, query)
|
||||
alias, order_field = qualified_field.replace('"', '').split('.', 1)
|
||||
field = field.base_field
|
||||
|
||||
assert field.type == 'many2one', 'Invalid field passed to _generate_m2o_order_by()'
|
||||
if not field.store:
|
||||
_logger.debug("Many2one function/related fields must be stored "
|
||||
"to be used as ordering fields! Ignoring sorting for %s.%s",
|
||||
self._name, order_field)
|
||||
order = order or self._order
|
||||
if not order:
|
||||
return []
|
||||
self._check_qorder(order)
|
||||
|
||||
# figure out the applicable order_by for the m2o
|
||||
dest_model = self.env[field.comodel_name]
|
||||
m2o_order = dest_model._order
|
||||
if not regex_order.match(m2o_order):
|
||||
# _order is complex, can't use it here, so we default to _rec_name
|
||||
m2o_order = dest_model._rec_name
|
||||
alias = alias or self._table
|
||||
|
||||
# Join the dest m2o table if it's not joined yet. We use [LEFT] OUTER join here
|
||||
# as we don't want to exclude results that have NULL values for the m2o
|
||||
dest_alias = query.left_join(alias, order_field, dest_model._table, 'id', order_field)
|
||||
return dest_model._generate_order_by_inner(dest_alias, m2o_order, query,
|
||||
reverse_direction, seen)
|
||||
|
||||
@api.model
|
||||
def _generate_order_by_inner(self, alias, order_spec, query, reverse_direction=False, seen=None):
|
||||
if seen is None:
|
||||
seen = set()
|
||||
self._check_qorder(order_spec)
|
||||
|
||||
order_by_elements = []
|
||||
for order_part in order_spec.split(','):
|
||||
terms = []
|
||||
for order_part in order.split(','):
|
||||
order_match = regex_order.match(order_part)
|
||||
order_field = order_match['field']
|
||||
order_direction = (order_match['direction'] or '').upper()
|
||||
order_nulls = (order_match['nulls'] or '').upper()
|
||||
if reverse_direction:
|
||||
order_direction = 'ASC' if order_direction == 'DESC' else 'DESC'
|
||||
if order_nulls:
|
||||
order_nulls = 'NULLS LAST' if order_nulls == 'NULLS FIRST' else 'NULLS FIRST'
|
||||
do_reverse = order_direction == 'DESC'
|
||||
|
||||
field = self._fields.get(order_field)
|
||||
if not field:
|
||||
raise ValueError("Invalid field %r on model %r" % (order_field, self._name))
|
||||
field_name = order_match['field']
|
||||
|
||||
property_name = order_match['property']
|
||||
if property_name:
|
||||
check_property_field_value_name(property_name)
|
||||
if field.type != 'properties':
|
||||
raise ValueError(f'Order a property ({property_name!r}) on a non-properties field ({field.name!r})')
|
||||
field_name = f"{field_name}.{property_name}"
|
||||
|
||||
if order_field == 'id':
|
||||
order_by_elements.append(f'"{alias}"."{order_field}" {order_direction} {order_nulls}')
|
||||
else:
|
||||
if field.inherited:
|
||||
field = field.base_field
|
||||
if field.store and field.type == 'many2one':
|
||||
key = (field.model_name, field.comodel_name, order_field)
|
||||
if order_nulls:
|
||||
qname = self._inherits_join_calc(alias, order_field, query)
|
||||
if order_nulls == 'NULLS LAST':
|
||||
order_by_elements.append(f"{qname} IS NULL")
|
||||
elif order_nulls == 'NULLS FIRST':
|
||||
order_by_elements.append(f"{qname} IS NOT NULL")
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
order_by_elements += self._generate_m2o_order_by(alias, order_field, query, do_reverse, seen)
|
||||
elif field.store and field.column_type:
|
||||
qualifield_name = self._inherits_join_calc(alias, order_field, query)
|
||||
if field.type == 'boolean':
|
||||
qualifield_name = "COALESCE(%s, false)" % qualifield_name
|
||||
elif field.type == 'properties' and property_name:
|
||||
qualifield_name = f"({qualifield_name} -> '{property_name}')"
|
||||
order_by_elements.append(f"{qualifield_name} {order_direction} {order_nulls}")
|
||||
else:
|
||||
_logger.warning("Model %r cannot be sorted on field %r (not a column)", self._name, order_field)
|
||||
continue # ignore non-readable or "non-joinable" fields
|
||||
direction = (order_match['direction'] or '').upper()
|
||||
nulls = (order_match['nulls'] or '').upper()
|
||||
if reverse:
|
||||
direction = 'ASC' if direction == 'DESC' else 'DESC'
|
||||
if nulls:
|
||||
nulls = 'NULLS LAST' if nulls == 'NULLS FIRST' else 'NULLS FIRST'
|
||||
|
||||
return order_by_elements
|
||||
sql_direction = SQL(direction) if direction in ('ASC', 'DESC') else SQL()
|
||||
sql_nulls = SQL(nulls) if nulls in ('NULLS FIRST', 'NULLS LAST') else SQL()
|
||||
|
||||
term = self._order_field_to_sql(alias, field_name, sql_direction, sql_nulls, query)
|
||||
if term:
|
||||
terms.append(term)
|
||||
|
||||
return SQL(", ").join(terms)
|
||||
|
||||
def _order_field_to_sql(self, alias: str, field_name: str, direction: SQL,
|
||||
nulls: SQL, query: Query) -> SQL:
|
||||
""" Return an :class:`SQL` object that represents the ordering by the
|
||||
given field.
|
||||
|
||||
:param direction: one of ``SQL("ASC")``, ``SQL("DESC")``, ``SQL()``
|
||||
:param nulls: one of ``SQL("NULLS FIRST")``, ``SQL("NULLS LAST")``, ``SQL()``
|
||||
"""
|
||||
full_name = field_name
|
||||
property_name = None
|
||||
if '.' in field_name:
|
||||
field_name, property_name = field_name.split('.', 1)
|
||||
|
||||
field = self._fields.get(field_name)
|
||||
if not field:
|
||||
raise ValueError(f"Invalid field {field_name!r} on model {self._name!r}")
|
||||
|
||||
if property_name and field.type != 'properties':
|
||||
raise ValueError(f'Order a property ({property_name!r}) on a non-properties field ({field_name!r})')
|
||||
|
||||
if field.inherited:
|
||||
# delegate to the parent model via a join
|
||||
parent_model = self.env[field.related_field.model_name]
|
||||
parent_fname = field.related.split('.')[0]
|
||||
parent_alias = query.make_alias(alias, parent_fname)
|
||||
query.add_join('LEFT JOIN', parent_alias, parent_model._table, SQL(
|
||||
"%s = %s",
|
||||
self._field_to_sql(alias, parent_fname, query),
|
||||
SQL.identifier(parent_alias, 'id'),
|
||||
))
|
||||
return parent_model._order_field_to_sql(parent_alias, full_name, direction, nulls, query)
|
||||
|
||||
if not (field.store and field.column_type):
|
||||
_logger.warning("Model %r cannot be sorted on field %r (not a column)", self._name, field_name)
|
||||
return
|
||||
|
||||
if field.type == 'many2one':
|
||||
seen = self.env.context.get('__m2o_order_seen', ())
|
||||
if field in seen:
|
||||
return
|
||||
self = self.with_context(__m2o_order_seen=frozenset((field, *seen)))
|
||||
|
||||
# instead of ordering by the field's raw value, use the comodel's
|
||||
# order on many2one values
|
||||
terms = []
|
||||
if nulls.code == 'NULLS FIRST':
|
||||
terms.append(SQL("%s IS NOT NULL", self._field_to_sql(alias, field_name, query)))
|
||||
elif nulls.code == 'NULLS LAST':
|
||||
terms.append(SQL("%s IS NULL", self._field_to_sql(alias, field_name, query)))
|
||||
|
||||
# LEFT JOIN the comodel table, in order to include NULL values, too
|
||||
comodel = self.env[field.comodel_name]
|
||||
coalias = query.make_alias(alias, field_name)
|
||||
query.add_join('LEFT JOIN', coalias, comodel._table, SQL(
|
||||
"%s = %s",
|
||||
self._field_to_sql(alias, field_name, query),
|
||||
SQL.identifier(coalias, 'id'),
|
||||
))
|
||||
|
||||
# figure out the applicable order_by for the m2o
|
||||
coorder = comodel._order
|
||||
if not regex_order.match(coorder):
|
||||
# _order is complex, can't use it here, so we default to _rec_name
|
||||
coorder = comodel._rec_name
|
||||
|
||||
# delegate the order to the comodel
|
||||
reverse = direction.code == 'DESC'
|
||||
term = comodel._order_to_sql(coorder, query, alias=coalias, reverse=reverse)
|
||||
if term:
|
||||
terms.append(term)
|
||||
return SQL(", ").join(terms)
|
||||
|
||||
sql_field = self._field_to_sql(alias, field_name, query)
|
||||
if field.type == 'boolean':
|
||||
sql_field = SQL("COALESCE(%s, FALSE)", sql_field)
|
||||
elif field.type == 'properties' and property_name:
|
||||
sql_field = SQL("(%s -> %s)", sql_field, property_name)
|
||||
|
||||
return SQL("%s %s %s", sql_field, direction, nulls)
|
||||
|
||||
@api.model
|
||||
def _generate_order_by(self, order_spec, query):
|
||||
@@ -5203,14 +5227,13 @@ class BaseModel(metaclass=MetaModel):
|
||||
a comma-separated list of valid field names, optionally followed by an ASC or DESC direction.
|
||||
|
||||
:raise ValueError in case order_spec is malformed
|
||||
"""
|
||||
order_by_clause = ''
|
||||
order_spec = order_spec or self._order
|
||||
if order_spec:
|
||||
order_by_elements = self._generate_order_by_inner(self._table, order_spec, query)
|
||||
if order_by_elements:
|
||||
order_by_clause = ",".join(order_by_elements)
|
||||
|
||||
.. deprecated:: 17.0
|
||||
Deprecated method, use _order_to_sql() instead
|
||||
"""
|
||||
warnings.warn("Deprecated method _generate_order_by(), _order_to_sql() instead", DeprecationWarning, 2)
|
||||
sql = self._order_to_sql(order_spec, query)
|
||||
order_by_clause = self.env.cr.mogrify(sql).decode()
|
||||
return order_by_clause and (' ORDER BY %s ' % order_by_clause) or ''
|
||||
|
||||
@api.model
|
||||
@@ -5339,7 +5362,7 @@ class BaseModel(metaclass=MetaModel):
|
||||
self._apply_ir_rules(query, 'read')
|
||||
|
||||
if order:
|
||||
query.order = self._generate_order_by(order, query).replace('ORDER BY ', '')
|
||||
query.order = self._order_to_sql(order, query)
|
||||
query.limit = limit
|
||||
query.offset = offset
|
||||
|
||||
|
||||
+10
-2
@@ -73,7 +73,7 @@ class Query(object):
|
||||
self._where_clauses = []
|
||||
|
||||
# order, limit, offset
|
||||
self.order = None
|
||||
self._order = None
|
||||
self.limit = None
|
||||
self.offset = None
|
||||
|
||||
@@ -140,6 +140,14 @@ class Query(object):
|
||||
self.add_join('LEFT JOIN', rhs_alias, rhs_table, condition)
|
||||
return rhs_alias
|
||||
|
||||
@property
|
||||
def order(self) -> SQL | None:
|
||||
return self._order
|
||||
|
||||
@order.setter
|
||||
def order(self, value: SQL | str | None):
|
||||
self._order = SQL(value) if value is not None else None
|
||||
|
||||
@property
|
||||
def table(self) -> str:
|
||||
""" Return the query's main table, i.e., the first one in the FROM clause. """
|
||||
@@ -176,7 +184,7 @@ class Query(object):
|
||||
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(" ORDER BY %s", 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(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user