From f2ceef0e2f0efb2e47b5f595a80009184305a2c4 Mon Sep 17 00:00:00 2001 From: wan Date: Wed, 12 Aug 2020 15:29:15 +0000 Subject: [PATCH] [IMP] core: add models defined by a query instead of a table/view By adding a parameter `_table_query` on the Model, we can now have views that depend on the context. The query is used instead of the table name in ORM operations. This allows to pre-compute some values to improve performance, instead of storing context values in the database. Co-authored-by: william-andre Co-authored-by: rco-odoo --- odoo/addons/base/tests/test_osv.py | 19 +++++++++++ odoo/models.py | 7 ++-- odoo/modules/registry.py | 6 +++- odoo/osv/expression.py | 2 +- odoo/osv/query.py | 54 ++++++++++++++++-------------- 5 files changed, 57 insertions(+), 31 deletions(-) diff --git a/odoo/addons/base/tests/test_osv.py b/odoo/addons/base/tests/test_osv.py index 6dc11d8fd98..be284c3fd78 100644 --- a/odoo/addons/base/tests/test_osv.py +++ b/odoo/addons/base/tests/test_osv.py @@ -80,3 +80,22 @@ class QueryTestCase(BaseCase): self.assertEqual(tmp_cat_cmp_par, 'product_product__product_tmpl_id__product_category_id__56d55687') tmp_cat_stm_par = query.join(tmp_cat_stm, 'partner_id', 'res_partner', 'id', 'partner_id') self.assertEqual(tmp_cat_stm_par, 'product_product__product_tmpl_id__product_category_id__9_363fdd') + + def test_table_expression(self): + query = Query(None, 'foo') + from_clause, where_clause, where_params = query.get_sql() + self.assertEqual(from_clause, '"foo"') + + query = Query(None, 'bar', 'SELECT id FROM foo') + from_clause, where_clause, where_params = query.get_sql() + self.assertEqual(from_clause, '(SELECT id FROM foo) AS "bar"') + + query = Query(None, 'foo') + query.add_table('bar', 'SELECT id FROM foo') + from_clause, where_clause, where_params = query.get_sql() + self.assertEqual(from_clause, '"foo", (SELECT id FROM foo) AS "bar"') + + query = Query(None, 'foo') + query.join('foo', 'bar_id', 'SELECT id FROM foo', 'id', 'bar') + from_clause, where_clause, where_params = query.get_sql() + self.assertEqual(from_clause, '"foo" JOIN (SELECT id FROM foo) AS "foo__bar" ON ("foo"."bar_id" = "foo__bar"."id")') diff --git a/odoo/models.py b/odoo/models.py index f7e54cd4750..d82806b6862 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -316,6 +316,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): correspond to the last one (in the inherits list order). """ _table = None #: SQL table name used by model if :attr:`_auto` + _table_query = None #: SQL expression of the table's content (optional) _sequence = None #: SQL sequence to use for ID field _sql_constraints = [] #: SQL constraints [(name, sql_def, message)] @@ -3058,7 +3059,7 @@ Fields: cr, user, context, su = env.args # make a query object for selecting ids, and apply security rules to it - query = Query(self.env.cr, self._table) + query = Query(self.env.cr, self._table, self._table_query) self._apply_ir_rules(query, 'read') # the query may involve several tables: we need fully-qualified names @@ -3321,7 +3322,7 @@ Fields: if not self._ids: return self - query = Query(self._cr, self._table) + query = Query(self.env.cr, self._table, self._table_query) self._apply_ir_rules(query, operation) if not query.where_clause: return self @@ -4153,7 +4154,7 @@ Fields: if domain: return expression.expression(domain, self).query else: - return Query(self.env.cr, self._table) + return Query(self.env.cr, self._table, self._table_query) def _check_qorder(self, word): if not regex_order.match(word): diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index 1126b05e82a..8ccb1615113 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -491,7 +491,11 @@ class Registry(Mapping): Verify that all tables are present and try to initialize those that are missing. """ env = odoo.api.Environment(cr, SUPERUSER_ID, {}) - table2model = {model._table: name for name, model in env.items() if not model._abstract} + table2model = { + model._table: name + for name, model in env.items() + if not model._abstract and model.__class__._table_query is None + } missing_tables = set(table2model).difference(existing_tables(cr, table2model)) if missing_tables: diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index 87208bba372..aba79c3859d 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -432,7 +432,7 @@ class expression(object): self.expression = distribute_not(normalize_domain(domain)) # this object handles all the joins - self.query = Query(model.env.cr, model._table) if query is None else query + self.query = Query(model.env.cr, model._table, model._table_query) if query is None else query # parse the domain expression self.parse() diff --git a/odoo/osv/query.py b/odoo/osv/query.py index 18302fa1d70..6edb08b4a65 100644 --- a/odoo/osv/query.py +++ b/odoo/osv/query.py @@ -1,16 +1,23 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import re import warnings from zlib import crc32 from odoo.tools import lazy_property +IDENT_RE = re.compile(r'^[a-z_][a-z0-9_$]*$', re.I) -def _quote(to_quote): - if '"' not in to_quote: - return '"%s"' % to_quote - return to_quote + +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 _generate_table_alias(src_table_alias, link): @@ -44,36 +51,36 @@ class Query(object): """ Simple implementation of a query object, managing tables with aliases, join clauses (with aliases, condition and parameters), where clauses (with parameters), order, limit and offset. + + :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) """ - def __init__(self, cr, table, where_clause=None, where_params=()): + def __init__(self, cr, alias, table=None): # database cursor self._cr = cr # tables {alias: table} - self._tables = {table: table} + self._tables = {alias: table or alias} # joins {alias: (kind, table, condition, condition_params)} self._joins = {} - # holds the list of WHERE clause elements, to be joined with - # 'AND' when generating the final query - self._where_clauses = [where_clause] if where_clause else [] - - # holds the parameters for the formatting of `where_clause`, to be - # passed to psycopg's execute method. - self._where_params = list(where_params) + # holds the list of WHERE clause elements (to be joined with 'AND'), and + # the list of parameters + self._where_clauses = [] + self._where_params = [] # order, limit, offset self.order = None self.limit = None self.offset = None - def add_table(self, table, alias=None): + def add_table(self, alias, table=None): """ Add a table with a given alias to the from clause. """ - alias = alias or table assert alias not in self._tables and alias not in self._joins, "Alias %r already in %s" % (alias, str(self)) - self._tables[alias] = table + self._tables[alias] = table or alias def add_where(self, where_clause, where_params=()): """ Add a condition to the where clause. """ @@ -116,7 +123,7 @@ class Query(object): """ 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 [_quote(next(iter(self._tables))) + ".id"]), + ", ".join(args or [f'"{next(iter(self._tables))}".id']), from_clause, where_clause or "TRUE", (" ORDER BY %s" % self.order) if self.order else "", @@ -127,13 +134,11 @@ class Query(object): def get_sql(self): """ Returns (query_from, query_where, query_params). """ - tables = [] + tables = [_from_table(table, alias) for alias, table in self._tables.items()] joins = [] params = [] - for alias, table in self._tables.items(): - tables.append(f'"{table}"' if alias == table else f'"{table}" AS "{alias}"') for alias, (kind, table, condition, condition_params) in self._joins.items(): - joins.append(f'{kind} "{table}" AS "{alias}" ON ({condition})') + joins.append(f'{kind} {_from_table(table, alias)} ON ({condition})') params.extend(condition_params) from_clause = " ".join([", ".join(tables)] + joins) @@ -165,10 +170,7 @@ class Query(object): def tables(self): warnings.warn("deprecated Query.tables, use Query.get_sql() instead", DeprecationWarning) - return tuple( - f'"{table}"' if alias == table else f'"{table}" AS "{alias}"' - for alias, table in self._tables - ) + return tuple(_from_table(table, alias) for alias, table in self._tables.items()) @property def where_clause(self): @@ -184,4 +186,4 @@ class Query(object): lhs_alias, rhs_table, lhs_column, rhs_column, link = connection kind = '' if implicit else ('LEFT JOIN' if outer else 'JOIN') rhs_alias = self._join(kind, lhs_alias, lhs_column, rhs_table, rhs_column, link, extra, extra_params) - return rhs_alias, f'"{rhs_table}" AS "{rhs_alias}"' + return rhs_alias, _from_table(rhs_table, rhs_alias)