From d9c7febf49931172b76b229b84f8ce19afac172f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulius=20Sladkevi=C4=8Dius?= Date: Wed, 24 Oct 2012 13:03:21 +0300 Subject: [PATCH 01/39] Fixed sorting for m2o bzr revid: paulius@hacbee.com-20121024100321-tqjm4pyk9y392qur --- openerp/osv/orm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 4c3330c97b7..72c394debdb 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -4766,6 +4766,7 @@ class BaseModel(object): """ if context is None: context = {} + order = order or self._order self.check_access_rights(cr, access_rights_uid or user, 'read') # For transient models, restrict acces to the current user, except for the super-user From 6c453fe4044d5444213ce5e65f98e375f8bf4bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 27 Nov 2012 15:31:37 +0100 Subject: [PATCH 02/39] [IMP] Needaction: couters are not counter with the menu anymore. Server-side: removed needaction_counter field on menys. Added get_needaction_data method that calculates the needaction counters. Added a limit on needaction, because this allows to limitate the search without impacting the result (having 2386 unread entries or 99+ is basically equivalent). bzr revid: tde@openerp.com-20121127143137-k152ail3m2trlobl --- openerp/addons/base/ir/ir_needaction.py | 3 ++- openerp/addons/base/ir/ir_ui_menu.py | 29 +++++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index db133ad9904..57b324de3b9 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -21,6 +21,7 @@ from osv import osv + class ir_needaction_mixin(osv.AbstractModel): '''Mixin class for objects using the need action feature. @@ -60,4 +61,4 @@ class ir_needaction_mixin(osv.AbstractModel): dom = self._needaction_domain_get(cr, uid, context=context) if not dom: return 0 - return self.search(cr, uid, (domain or []) +dom, context=context, count=True) + return self.search(cr, uid, (domain or []) + dom, limit=100, context=context, count=True) diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index 43ece8c0868..ef9ed3b7bf3 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -249,7 +249,7 @@ class ir_ui_menu(osv.osv): icon_image = False if icon_path: try: - icon_file = tools.file_open(icon_path,'rb') + icon_file = tools.file_open(icon_path, 'rb') icon_image = base64.encodestring(icon_file.read()) finally: icon_file.close() @@ -265,17 +265,26 @@ class ir_ui_menu(osv.osv): return res - def _get_needaction(self, cr, uid, ids, field_names, args, context=None): + def _get_needaction_enabled(self, cr, uid, ids, field_names, args, context=None): + res = dict.fromkeys(ids, False) + for menu in self.browse(cr, uid, ids, context=context): + if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model: + obj = self.pool.get(menu.action.res_model) + if obj and obj._needaction: + res[menu.id] = True + return res + + def get_needaction_data(self, cr, uid, ids, context=None): res = {} for menu in self.browse(cr, uid, ids, context=context): res[menu.id] = { 'needaction_enabled': False, 'needaction_counter': False, } - if menu.action and menu.action.type in ('ir.actions.act_window','ir.actions.client') and menu.action.res_model: + if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model: obj = self.pool.get(menu.action.res_model) if obj and obj._needaction: - if menu.action.type=='ir.actions.act_window': + if menu.action.type == 'ir.actions.act_window': dom = menu.action.domain and eval(menu.action.domain, {'uid': uid}) or [] else: dom = eval(menu.action.params_store or '{}', {'uid': uid}).get('domain') @@ -286,7 +295,7 @@ class ir_ui_menu(osv.osv): _columns = { 'name': fields.char('Menu', size=64, required=True, translate=True), 'sequence': fields.integer('Sequence'), - 'child_id' : fields.one2many('ir.ui.menu', 'parent_id','Child IDs'), + 'child_id': fields.one2many('ir.ui.menu', 'parent_id', 'Child IDs'), 'parent_id': fields.many2one('ir.ui.menu', 'Parent Menu', select=True), 'groups_id': fields.many2many('res.groups', 'ir_ui_menu_group_rel', 'menu_id', 'gid', 'Groups', help="If you have groups, the visibility of this menu will be based on these groups. "\ @@ -296,11 +305,13 @@ class ir_ui_menu(osv.osv): 'icon': fields.selection(tools.icons, 'Icon', size=64), 'icon_pict': fields.function(_get_icon_pict, type='char', size=32), 'web_icon': fields.char('Web Icon File', size=128), - 'web_icon_hover':fields.char('Web Icon File (hover)', size=128), + 'web_icon_hover': fields.char('Web Icon File (hover)', size=128), 'web_icon_data': fields.function(_get_image_icon, string='Web Icon Image', type='binary', readonly=True, store=True, multi='icon'), - 'web_icon_hover_data':fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'), - 'needaction_enabled': fields.function(_get_needaction, string='Target model uses the need action mechanism', type='boolean', help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.', multi='_get_needaction'), - 'needaction_counter': fields.function(_get_needaction, string='Number of actions the user has to perform', type='integer', help='If the target model uses the need action mechanism, this field gives the number of actions the current user has to perform.', multi='_get_needaction'), + 'web_icon_hover_data': fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'), + 'needaction_enabled': fields.function(_get_needaction_enabled, + type='boolean', store=True, + string='Target model uses the need action mechanism', + help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.'), 'action': fields.function(_action, fnct_inv=_action_inv, type='reference', string='Action', selection=[ From c1c5a852312df8bd34836cc91bff90c42d2ad296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 27 Nov 2012 16:01:59 +0100 Subject: [PATCH 03/39] [CLEAN] expression.py: renamed some variables of expression; cleaned a bit the coding style (mainly spaces after commas). bzr revid: tde@openerp.com-20121127150159-su9x00m7tc8tc6dj --- openerp/osv/expression.py | 93 ++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 2b2d33a496e..bb2fce62b21 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -362,11 +362,24 @@ class expression(object): """ def __init__(self, cr, uid, exp, table, context): + """ Initialize expression object and automatically parse the expression + right after initialization. + + :param exp: expression (using domain ('foo', '=', 'bar' format)) + :param table: root table object + + :attr dict leaf_to_table: used to store the table to use for the + sql generation, according to the domain leaf. + structure: { [leaf index]: table object } + :attr set table_aliases: set of aliases. + :attr list joins: list of join conditions, such as (res_country_state."id" = res_partner."state_id") + :attr root_table: root table, set by parse() + """ self.has_unaccent = openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent - self.__field_tables = {} # used to store the table to use for the sql generation. key = index of the leaf - self.__all_tables = set() - self.__joins = [] - self.__main_table = None # 'root' table. set by parse() + self.leaf_to_table = {} + self.table_aliases = set() + self.joins = [] + self.root_table = None # assign self.__exp with the normalized, parsed domain. self.parse(cr, uid, distribute_not(normalize(exp)), table, context) @@ -378,8 +391,8 @@ class expression(object): def parse(self, cr, uid, exp, table, context): """ transform the leaves of the expression """ self.__exp = exp - self.__main_table = table - self.__all_tables.add(table) + self.root_table = table + self.table_aliases.add(table) def child_of_domain(left, ids, left_model, parent=None, prefix=''): """Returns a domain implementing the child_of operator for [(left,child_of,ids)], @@ -419,7 +432,7 @@ class expression(object): return list(value) i = -1 - while i + 1','<=','>='] ) and 'in' or operator + operator = (operator in ['<', '>', '<=', '>=']) and 'in' or operator - dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in'} + dict_op = {'not in': '!=', 'in': '=', '=': 'in', '!=': 'not in'} if isinstance(right, tuple): right = list(right) - if (not isinstance(right, list)) and operator in ['not in','in']: + if (not isinstance(right, list)) and operator in ['not in', 'in']: operator = dict_op[operator] - elif isinstance(right, list) and operator in ['!=','=']: #for domain (FIELD,'=',['value1','value2']) + elif isinstance(right, list) and operator in ['!=', '=']: # for domain (FIELD,'=',['value1','value2']) operator = dict_op[operator] res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)] if operator in NEGATIVE_TERM_OPERATORS: - res_ids.append(False) # TODO this should not be appended if False was in 'right' + res_ids.append(False) # TODO this should not be appended if False was in 'right' return (left, 'in', res_ids) # resolve string-based m2o criterion into IDs if isinstance(right, basestring) or \ - right and isinstance(right, (tuple,list)) and all(isinstance(item, basestring) for item in right): + right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right): self.__exp[i] = _get_expression(field_obj, cr, uid, left, right, operator, context=context) - else: + else: # right == [] or right == False and all other cases are handled by __leaf_to_sql() pass @@ -640,7 +653,7 @@ class expression(object): if field.translate: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') - sql_operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator) + sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator) if need_wildcard: right = '%%%s%%' % right @@ -651,13 +664,13 @@ class expression(object): ' AND type = %s' instr = ' %s' #Covering in,not in operators with operands (%s,%s) ,etc. - if sql_operator in ['in','not in']: + if sql_operator in ['in', 'not in']: instr = ','.join(['%s'] * len(right)) - subselect += ' AND value ' + sql_operator + ' ' +" (" + instr + ")" \ + subselect += ' AND value ' + sql_operator + ' ' + " (" + instr + ")" \ ') UNION (' \ ' SELECT id' \ ' FROM "' + working_table._table + '"' \ - ' WHERE "' + left + '" ' + sql_operator + ' ' +" (" + instr + "))" + ' WHERE "' + left + '" ' + sql_operator + ' ' + " (" + instr + "))" else: subselect += ' AND value ' + sql_operator + instr + \ ') UNION (' \ @@ -729,11 +742,11 @@ class expression(object): elif not check_nulls and operator == 'not in': query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left) elif check_nulls and operator == 'not in': - query = '(%s AND %s."%s" IS NOT NULL)' % (query, table._table, left) # needed only for TRUE. - else: # Must not happen + query = '(%s AND %s."%s" IS NOT NULL)' % (query, table._table, left) # needed only for TRUE. + else: # Must not happen raise ValueError("Invalid domain term %r" % (leaf,)) - elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '='): + elif right == False and (left in table._columns) and table._columns[left]._type == "boolean" and (operator == '='): query = '(%s."%s" IS NULL or %s."%s" = false )' % (table._table, left, table._table, left) params = [] @@ -741,7 +754,7 @@ class expression(object): query = '%s."%s" IS NULL ' % (table._table, left) params = [] - elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '!='): + elif right == False and (left in table._columns) and table._columns[left]._type == "boolean" and (operator == '!='): query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table._table, left, table._table, left) params = [] @@ -764,7 +777,7 @@ class expression(object): else: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') - sql_operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator) + sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator) if left in table._columns: format = need_wildcard and '%s' or table._columns[left]._symbol_set[0] @@ -775,7 +788,7 @@ class expression(object): elif left in MAGIC_COLUMNS: query = "(%s.\"%s\" %s %%s)" % (table._table, left, sql_operator) params = right - else: # Must not happen + else: # Must not happen raise ValueError("Invalid field %r in domain term %r" % (left, leaf)) add_null = False @@ -798,14 +811,13 @@ class expression(object): params = [params] return (query, params) - def to_sql(self): stack = [] params = [] # Process the domain from right to left, using a stack, to generate a SQL expression. for i, e in reverse_enumerate(self.__exp): if is_leaf(e, internal=True): - table = self.__field_tables.get(i, self.__main_table) + table = self.leaf_to_table.get(i, self.root_table) q, p = self.__leaf_to_sql(e, table) params.insert(0, p) stack.append(q) @@ -819,13 +831,12 @@ class expression(object): assert len(stack) == 1 query = stack[0] - joins = ' AND '.join(self.__joins) + joins = ' AND '.join(self.joins) if joins: query = '(%s) AND %s' % (joins, query) return (query, flatten(params)) def get_tables(self): - return ['"%s"' % t._table for t in self.__all_tables] + return ['"%s"' % t._table for t in self.table_aliases] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - From e147a5483f378eaa53f148017e289801897e8f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 27 Nov 2012 16:11:13 +0100 Subject: [PATCH 04/39] [CLEAN] expression.py: table_aliases now holds a list of aliases (names), and table_aliases_mapping does the mapping between a name and a table. The parsing algorithm now uses some accessors instead of directly accessing the data structures. bzr revid: tde@openerp.com-20121127151113-zregov1i2isoll9z --- openerp/osv/expression.py | 47 ++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index bb2fce62b21..58f9defccd4 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -371,28 +371,56 @@ class expression(object): :attr dict leaf_to_table: used to store the table to use for the sql generation, according to the domain leaf. structure: { [leaf index]: table object } - :attr set table_aliases: set of aliases. + :attr set table_aliases: set of aliases. Previously this attribute + was a set of table objects; now that joins generation is included + into the expression parsing, it holds aliases, and a mapping + exist linking aliases to tables. + :attr dict table_aliases_mapping: mapping alias -> table object :attr list joins: list of join conditions, such as (res_country_state."id" = res_partner."state_id") :attr root_table: root table, set by parse() """ self.has_unaccent = openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent self.leaf_to_table = {} self.table_aliases = set() + self.table_aliases_mapping = {} self.joins = [] self.root_table = None # assign self.__exp with the normalized, parsed domain. self.parse(cr, uid, distribute_not(normalize(exp)), table, context) - # TODO used only for osv_memory - @property - def exp(self): - return self.__exp[:] + # TDE note: this seems not to be used anymore, commenting + # # TODO used only for osv_memory + # @property + # def exp(self): + # return self.__exp[:] + + def _has_table_alias(self, alias): + return alias in self.table_aliases + + def _get_table_from_alias(self, alias): + return self.table_aliases_mapping.get(alias) + + def _get_full_alias(self, alias): + if not self._get_table_from_alias(alias): + return False + return '%s as %s' % (self._get_table_from_alias(alias)._table, alias) + + def _add_table_alias(self, alias, table): + if not self._has_table_alias(alias): + self.table_aliases.add(alias) + self.table_aliases_mapping[alias] = table + else: + raise ValueError("Already existing alias %s for table %s, trying to set it for table %s" % (alias, self._get_table_from_alias(alias)._table, table._table)) + + def get_tables(self): + """ Returns the list of tables for SQL queries, like select from ... """ + return ['"%s"' % item for item in self.table_aliases] def parse(self, cr, uid, exp, table, context): """ transform the leaves of the expression """ self.__exp = exp self.root_table = table - self.table_aliases.add(table) + self._add_table_alias(table._table, table) def child_of_domain(left, ids, left_model, parent=None, prefix=''): """Returns a domain implementing the child_of operator for [(left,child_of,ids)], @@ -460,9 +488,9 @@ class expression(object): self.leaf_to_table[i] = working_table break next_table = working_table.pool.get(working_table._inherit_fields[field_path[0]][0]) - if next_table not in self.table_aliases: + if not self._has_table_alias(next_table._table): self.joins.append('%s."%s"=%s."%s"' % (next_table._table, 'id', working_table._table, working_table._inherits[next_table._name])) - self.table_aliases.add(next_table) + self._add_table_alias(next_table._table, next_table) working_table = next_table # Or (try to) directly extract the field. else: @@ -836,7 +864,4 @@ class expression(object): query = '(%s) AND %s' % (joins, query) return (query, flatten(params)) - def get_tables(self): - return ['"%s"' % t._table for t in self.table_aliases] - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 1bda6f44db6895f9ce6264c400066c0a1f0caa2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 27 Nov 2012 16:34:32 +0100 Subject: [PATCH 05/39] [ADD] fields.py: added _auto_join attribute. This attribute will be used to automatically generate join queries, instead of doing sub-queries returning an 'id in' domain. bzr revid: tde@openerp.com-20121127153432-3ttqm8pmgk3l7a77 --- openerp/osv/fields.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index c8cf5909ecc..41a15b757e3 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -67,6 +67,7 @@ class _column(object): """ _classic_read = True _classic_write = True + _auto_join = False _prefetch = True _properties = False _type = 'unknown' From 8163c0027b5935e5d1903a78be901a3c519e42f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 27 Nov 2012 17:06:27 +0100 Subject: [PATCH 06/39] [IMP] [TEST] orm: updated order_by generation by adding the root table name to the order by condition generated based on self._order. Added a mockup of where_calc in test_expression, to be able to track generated Query objects. Added a first test on a one2many query, to track current behavior. bzr revid: tde@openerp.com-20121127160627-qs1tupvgr8ypexoo --- openerp/addons/base/tests/test_expression.py | 81 +++++++++++++++++++- openerp/osv/orm.py | 12 ++- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index 5690cff443a..0f9241bce86 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -1,10 +1,21 @@ import unittest2 - +from openerp.osv.orm import BaseModel import openerp.tests.common as common + class test_expression(common.TransactionCase): - def test_in_not_in_m2m(self): + def _reinit_mock(self): + self.query_list = list() + + def _mock_base_model_where_calc(self, model, *args, **kwargs): + """ Mock build_email to be able to test its values. Store them into + some internal variable for latter processing. """ + self.query_list.append(self._base_model_where_calc(model, *args, **kwargs)) + # return the lastly stored query, the one the ORM wants to perform + return self.query_list[-1] + + def test_00_in_not_in_m2m(self): registry, cr, uid = self.registry, self.cr, self.uid @@ -70,3 +81,69 @@ class test_expression(common.TransactionCase): # self.assertTrue(a not in with_any_other_than_a, "Search for category_id with any other than cat_a failed (1).") # self.assertTrue(ab in with_any_other_than_a, "Search for category_id with any other than cat_a failed (2).") + def test_10_auto_join(self): + registry, cr, uid = self.registry, self.cr, self.uid + + # Mock BaseModel._where_calc(), to be able to proceed to some tests about generated expression + self._reinit_mock() + self._base_model_where_calc = BaseModel._where_calc + BaseModel._where_calc = lambda model, cr, uid, args, context: self._mock_base_model_where_calc(model, cr, uid, args, context) + + # Get models + partner_obj = registry('res.partner') + state_obj = registry('res.country.state') + bank_obj = registry('res.partner.bank') + + # Get test columns + state_id_col = partner_obj._columns.get('state_id') # many2one on res.partner to res.country.state + child_ids_col = partner_obj._columns.get('child_ids') # one2many on res.partner to res.partner + bank_ids_col = partner_obj._columns.get('bank_ids') # one2many on res.partner to res.partner.bank + country_id_col = state_obj._columns.get('country_id') # many2one on res.country.state on res.country + + # Get the first bank account type to be able to create a res.partner.bank + bank_type = bank_obj._bank_type_get(cr, uid)[0] + + # Create demo data: partners and bank object + p_a = partner_obj.create(cr, uid, {'name': 'test__A'}) + p_b = partner_obj.create(cr, uid, {'name': 'test__B'}) + p_aa = partner_obj.create(cr, uid, {'name': 'test__AA', 'parent_id': p_a}) + p_ab = partner_obj.create(cr, uid, {'name': 'test__AB', 'parent_id': p_a}) + b_a = bank_obj.create(cr, uid, {'name': '__bank_test_a', 'state': bank_type[0], 'partner_id': p_a, 'acc_number': '1234'}) + + # ---------------------------------------- + # Test2: one2many + # ---------------------------------------- + + name_test = 'test_a' + + # Do: one2many without _auto_join + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, [('bank_ids.name', 'like', name_test)]) + # Test result + self.assertEqual(set(partner_ids), set([p_a]), 'one2many without join failed') + # Test produced queries + self.assertEqual(len(self.query_list), 3, + "_auto_join off: ('bank_ids.name', 'like', '..') should produce 3 queries (1 in res_partner_bank, 1 on res_partner with active, 1 on res_partner)") + sql_query = self.query_list[0].get_sql() + self.assertIn('res_partner_bank', sql_query[0], "_auto_join off: ('bank_ids.name', 'like', '..') first query should be done in res_partner_bank") + self.assertIn('(res_partner_bank."name" like %s)', sql_query[1], "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect where condition") + self.assertEqual(set(['%' + name_test + '%']), set(sql_query[2]), "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect parameter") + sql_query = self.query_list[2].get_sql() + self.assertIn('res_partner', sql_query[0], "_auto_join off: ('bank_ids.name', 'like', '..') third query should be done in res_partner") + self.assertIn('(res_partner."id" in (%s))', sql_query[1], "_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect where condition") + self.assertEqual(set([p_a]), set(sql_query[2]), "_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect parameter") + + # ---------------------------------------- + # Test2: many2one + # ---------------------------------------- + + # ---------------------------------------- + # Test2: more complex tests + # ---------------------------------------- + + # Remove mocks and modifications + bank_ids_col._auto_join = False + BaseModel._where_calc = self._base_model_where_calc + +if __name__ == '__main__': + unittest2.main() diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 6a1f917caa1..4120168cc41 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -4712,7 +4712,17 @@ class BaseModel(object): :raise" except_orm in case order_spec is malformed """ - order_by_clause = self._order + def _split_order(order, table): + """ from name, id asc make "table"."id", "table."."name" asc """ + order_list = [] + for elem in order.split(','): + subelems = elem.strip().split(' ') + if len(subelems) == 1: + subelems.append(' ') + order_list.append('"%s"."%s" %s' % (table, subelems[0], subelems[1])) + return order_list + + order_by_clause = ','.join(_split_order(self._order, self._table)) if order_spec: order_by_elements = [] self._check_qorder(order_spec) From 53efed664d44560ce9ba507eb08622594622164b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 27 Nov 2012 17:13:54 +0100 Subject: [PATCH 07/39] [IMP] expression.py: _leaf_to_sql now uses a table_alias instead of table._table for query generation. Currently table_alias equals table._table, so the behavior has not changed. bzr revid: tde@openerp.com-20121127161354-fskwsmfvx3j8nto6 --- openerp/osv/expression.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 58f9defccd4..31a10d508f9 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -724,6 +724,8 @@ class expression(object): assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in table._all_columns \ or left in MAGIC_COLUMNS, "Invalid field %r in domain term %r" % (left, leaf) + table_alias = table._table + if leaf == TRUE_LEAF: query = 'TRUE' params = [] @@ -733,7 +735,7 @@ class expression(object): params = [] elif operator == 'inselect': - query = '(%s."%s" in (%s))' % (table._table, left, right[0]) + query = '(%s."%s" in (%s))' % (table_alias, left, right[0]) params = right[1] elif operator in ['in', 'not in']: @@ -745,7 +747,7 @@ class expression(object): r = 'NOT NULL' if right else 'NULL' else: r = 'NULL' if right else 'NOT NULL' - query = '(%s."%s" IS %s)' % (table._table, left, r) + query = '(%s."%s" IS %s)' % (table_alias, left, r) params = [] elif isinstance(right, (list, tuple)): params = right[:] @@ -760,34 +762,34 @@ class expression(object): instr = ','.join(['%s'] * len(params)) else: instr = ','.join([table._columns[left]._symbol_set[0]] * len(params)) - query = '(%s."%s" %s (%s))' % (table._table, left, operator, instr) + query = '(%s."%s" %s (%s))' % (table_alias, left, operator, instr) else: # The case for (left, 'in', []) or (left, 'not in', []). query = 'FALSE' if operator == 'in' else 'TRUE' if check_nulls and operator == 'in': - query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left) + query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left) elif not check_nulls and operator == 'not in': - query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left) + query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left) elif check_nulls and operator == 'not in': - query = '(%s AND %s."%s" IS NOT NULL)' % (query, table._table, left) # needed only for TRUE. + query = '(%s AND %s."%s" IS NOT NULL)' % (query, table_alias, left) # needed only for TRUE. else: # Must not happen raise ValueError("Invalid domain term %r" % (leaf,)) elif right == False and (left in table._columns) and table._columns[left]._type == "boolean" and (operator == '='): - query = '(%s."%s" IS NULL or %s."%s" = false )' % (table._table, left, table._table, left) + query = '(%s."%s" IS NULL or %s."%s" = false )' % (table_alias, left, table_alias, left) params = [] elif (right is False or right is None) and (operator == '='): - query = '%s."%s" IS NULL ' % (table._table, left) + query = '%s."%s" IS NULL ' % (table_alias, left) params = [] elif right == False and (left in table._columns) and table._columns[left]._type == "boolean" and (operator == '!='): - query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table._table, left, table._table, left) + query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table_alias, left, table_alias, left) params = [] elif (right is False or right is None) and (operator == '!='): - query = '%s."%s" IS NOT NULL' % (table._table, left) + query = '%s."%s" IS NOT NULL' % (table_alias, left) params = [] elif (operator == '=?'): @@ -800,7 +802,7 @@ class expression(object): query, params = self.__leaf_to_sql((left, '=', right), table) elif left == 'id': - query = '%s.id %s %%s' % (table._table, operator) + query = '%s.id %s %%s' % (table_alias, operator) params = right else: @@ -810,11 +812,11 @@ class expression(object): if left in table._columns: format = need_wildcard and '%s' or table._columns[left]._symbol_set[0] if self.has_unaccent and sql_operator in ('ilike', 'not ilike'): - query = '(unaccent(%s."%s") %s unaccent(%s))' % (table._table, left, sql_operator, format) + query = '(unaccent(%s."%s") %s unaccent(%s))' % (table_alias, left, sql_operator, format) else: - query = '(%s."%s" %s %s)' % (table._table, left, sql_operator, format) + query = '(%s."%s" %s %s)' % (table_alias, left, sql_operator, format) elif left in MAGIC_COLUMNS: - query = "(%s.\"%s\" %s %%s)" % (table._table, left, sql_operator) + query = "(%s.\"%s\" %s %%s)" % (table_alias, left, sql_operator) params = right else: # Must not happen raise ValueError("Invalid field %r in domain term %r" % (left, leaf)) @@ -833,7 +835,7 @@ class expression(object): params = table._columns[left]._symbol_set[1](right) if add_null: - query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left) + query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left) if isinstance(params, basestring): params = [params] From 48138c9d26a535bce3100b2d595177e437b75a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 27 Nov 2012 17:30:14 +0100 Subject: [PATCH 08/39] [IMP] [DOC] expression.py: renamed field_obj to relational_table, that make more sens to me. Added comments in the code to understand the algorithm. Please note that currently, no logic has been altered. bzr revid: tde@openerp.com-20121127163014-5ww5mre6j7opmzp1 --- openerp/osv/expression.py | 93 ++++++++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 25 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 31a10d508f9..22e49d42b8a 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -417,7 +417,23 @@ class expression(object): return ['"%s"' % item for item in self.table_aliases] def parse(self, cr, uid, exp, table, context): - """ transform the leaves of the expression """ + """ Transform the leaves of the expression + + For each element in the expression + 1. validity check: operator or True / False or a valid leaf + 2. TDE FIXME: TO COMPLETE + + Some var explanation for those who have 2 bytes of brain cache memory like me and that cannot remember 32^16 similar variable names. + :var obj working_table: table object, table containing the field (the name provided in the left operand) + :var list field_path: left operand seen as a path (foo.bar -> [foo, bar]) + :var string field_table: working_table._table + :var obj relational_table: relational table of a field (field._obj) + ex: res_partner.bank_ids -> res_partner_bank + :var obj field_obj: deleted var, now renamed to relational_table + + :param exp: expression (domain) + :param table: table object + """ self.__exp = exp self.root_table = table self._add_table_alias(table._table, table) @@ -445,7 +461,7 @@ class expression(object): return ids + recursive_children(ids2, model, parent_field) return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))] - def to_ids(value, field_obj): + def to_ids(value, relational_table): """Normalize a single id or name, or a list of those, into a list of ids""" names = [] if isinstance(value, basestring): @@ -453,7 +469,7 @@ class expression(object): if value and isinstance(value, (tuple, list)) and isinstance(value[0], basestring): names = value if names: - return flatten([[x[0] for x in field_obj.name_search(cr, uid, n, [], 'ilike', context=context, limit=None)] \ + return flatten([[x[0] for x in relational_table.name_search(cr, uid, n, [], 'ilike', context=context, limit=None)] \ for n in names]) elif isinstance(value, (int, long)): return [value] @@ -462,11 +478,16 @@ class expression(object): i = -1 while i + 1 < len(self.__exp): i += 1 + + # 0 Preparation + # - Check validity of current element of expression (operator OR True/False leaf OR leaf) + # - Normalize the leaf's operator + # - Set working variables + + # check validity e = self.__exp[i] if is_operator(e) or e == TRUE_LEAF or e == FALSE_LEAF: continue - - # check if the expression is valid if not is_leaf(e): raise ValueError("Invalid term %r in domain expression %r" % (e, exp)) @@ -475,11 +496,13 @@ class expression(object): self.__exp[i] = e left, operator, right = e + # working variables working_table = table # The table containing the field (the name provided in the left operand) field_path = left.split('.', 1) - # If the field is _inherits'd, search for the working_table, - # and extract the field. + # 1 Handle inherits fields: replace by a join + # - Search for the working_table and extract the field + field = None if field_path[0] in table._inherit_fields: while True: @@ -496,6 +519,11 @@ class expression(object): else: field = working_table._columns.get(field_path[0]) + # 2 Field not found + # - ('id', 'child_of', ids) and continue the processing OR + # - field in magic columns (ex: id) and continue the processing OR + # - raise an error + if not field: if left == 'id' and operator == 'child_of': ids2 = to_ids(right, table) @@ -509,14 +537,29 @@ class expression(object): raise ValueError("Invalid field %r in domain expression %r" % (left, exp)) continue - field_obj = table.pool.get(field._obj) + # 3 Field found + # - get relational table, existing for relational fields + # - if domain is a path (ex: ('partner_id.name', '=', 'foo')): + # replace all the expression by a normalized equivalent domain + # - find the related ids: partner_id.name='foo' -> res_partner.search(('name', '=', 'foo'))) + # - many2one: leaf becomes directly ('partner_id', 'in', 'partner_ids') + # - one2many: + # - search on current table where partner_id is in partner_ids + # - leaf becomes ('id', 'in', ids) + # - get out of current leaf is field is not a property field + # - if domain is not a path + # - handle function fields + # - handle one2many, many2many and many2one fields + # - other fields: handle datetime and translatable fields + + relational_table = table.pool.get(field._obj) if len(field_path) > 1: if field._type == 'many2one': - right = field_obj.search(cr, uid, [(field_path[1], operator, right)], context=context) + right = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) self.__exp[i] = (field_path[0], 'in', right) # Making search easier when there is a left operand as field.o2m or field.m2m if field._type in ['many2many', 'one2many']: - right = field_obj.search(cr, uid, [(field_path[1], operator, right)], context=context) + right = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) right1 = table.search(cr, uid, [(field_path[0], 'in', right)], context=dict(context, active_test=False)) self.__exp[i] = ('id', 'in', right1) @@ -552,9 +595,9 @@ class expression(object): elif field._type == 'one2many': # Applying recursivity on field(one2many) if operator == 'child_of': - ids2 = to_ids(right, field_obj) + ids2 = to_ids(right, relational_table) if field._obj != working_table._name: - dom = child_of_domain(left, ids2, field_obj, prefix=field._obj) + dom = child_of_domain(left, ids2, relational_table, prefix=field._obj) else: dom = child_of_domain('id', ids2, working_table, parent=left) self.__exp = self.__exp[:i] + dom + self.__exp[i + 1:] @@ -564,7 +607,7 @@ class expression(object): if right is not False: if isinstance(right, basestring): - ids2 = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, context=context, limit=None)] + ids2 = [x[0] for x in relational_table.name_search(cr, uid, right, [], operator, context=context, limit=None)] if ids2: operator = 'in' else: @@ -578,7 +621,7 @@ class expression(object): call_null = False self.__exp[i] = FALSE_LEAF else: - ids2 = select_from_where(cr, field._fields_id, field_obj._table, 'id', ids2, operator) + ids2 = select_from_where(cr, field._fields_id, relational_table._table, 'id', ids2, operator) if ids2: call_null = False o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' @@ -586,26 +629,26 @@ class expression(object): if call_null: o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - self.__exp[i] = ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, field_obj._table)) + self.__exp[i] = ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)) elif field._type == 'many2many': rel_table, rel_id1, rel_id2 = field._sql_names(working_table) #FIXME if operator == 'child_of': def _rec_convert(ids): - if field_obj == table: + if relational_table == table: return ids return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator) - ids2 = to_ids(right, field_obj) - dom = child_of_domain('id', ids2, field_obj) - ids2 = field_obj.search(cr, uid, dom, context=context) + ids2 = to_ids(right, relational_table) + dom = child_of_domain('id', ids2, relational_table) + ids2 = relational_table.search(cr, uid, dom, context=context) self.__exp[i] = ('id', 'in', _rec_convert(ids2)) else: call_null_m2m = True if right is not False: if isinstance(right, basestring): - res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, context=context)] + res_ids = [x[0] for x in relational_table.name_search(cr, uid, right, [], operator, context=context)] if res_ids: operator = 'in' else: @@ -631,14 +674,14 @@ class expression(object): elif field._type == 'many2one': if operator == 'child_of': - ids2 = to_ids(right, field_obj) + ids2 = to_ids(right, relational_table) if field._obj != working_table._name: - dom = child_of_domain(left, ids2, field_obj, prefix=field._obj) + dom = child_of_domain(left, ids2, relational_table, prefix=field._obj) else: dom = child_of_domain('id', ids2, working_table, parent=left) self.__exp = self.__exp[:i] + dom + self.__exp[i + 1:] else: - def _get_expression(field_obj, cr, uid, left, right, operator, context=None): + def _get_expression(relational_table, cr, uid, left, right, operator, context=None): if context is None: context = {} c = context.copy() @@ -653,14 +696,14 @@ class expression(object): operator = dict_op[operator] elif isinstance(right, list) and operator in ['!=', '=']: # for domain (FIELD,'=',['value1','value2']) operator = dict_op[operator] - res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)] + res_ids = [x[0] for x in relational_table.name_search(cr, uid, right, [], operator, limit=None, context=c)] if operator in NEGATIVE_TERM_OPERATORS: res_ids.append(False) # TODO this should not be appended if False was in 'right' return (left, 'in', res_ids) # resolve string-based m2o criterion into IDs if isinstance(right, basestring) or \ right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right): - self.__exp[i] = _get_expression(field_obj, cr, uid, left, right, operator, context=context) + self.__exp[i] = _get_expression(relational_table, cr, uid, left, right, operator, context=context) else: # right == [] or right == False and all other cases are handled by __leaf_to_sql() pass From 07f26441bbce36885de14fadf893f03ce7736dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 27 Nov 2012 17:37:58 +0100 Subject: [PATCH 09/39] [IMP] expression.parse: improved code about extracting inherits\'d fields. I do not like while True statements, I prefer having a clear condition. Behavior should still be the same as before. bzr revid: tde@openerp.com-20121127163758-tddhbglygcbkvzed --- openerp/osv/expression.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 22e49d42b8a..d2de370a459 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -500,24 +500,21 @@ class expression(object): working_table = table # The table containing the field (the name provided in the left operand) field_path = left.split('.', 1) - # 1 Handle inherits fields: replace by a join - # - Search for the working_table and extract the field + # 1 Extract field + # - Try to directly extract the field + # - Handle inherits fields: replace by a join, find the final new + # working table and extract the field - field = None + field = working_table._columns.get(field_path[0]) if field_path[0] in table._inherit_fields: - while True: - field = working_table._columns.get(field_path[0]) - if field: - self.leaf_to_table[i] = working_table - break + while not field: next_table = working_table.pool.get(working_table._inherit_fields[field_path[0]][0]) if not self._has_table_alias(next_table._table): self.joins.append('%s."%s"=%s."%s"' % (next_table._table, 'id', working_table._table, working_table._inherits[next_table._name])) self._add_table_alias(next_table._table, next_table) working_table = next_table - # Or (try to) directly extract the field. - else: - field = working_table._columns.get(field_path[0]) + field = working_table._columns.get(field_path[0]) + self.leaf_to_table[i] = working_table # 2 Field not found # - ('id', 'child_of', ids) and continue the processing OR From 873d6cd5b13118a6ebc7c1e1906d8c79ca196ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 27 Nov 2012 17:45:31 +0100 Subject: [PATCH 10/39] [CLEAN] expression.py: self.__exp -> self.exp, because too much underscores kill my eyes. Added a bit of comments. Please note that the behavior has still not changed. bzr revid: tde@openerp.com-20121127164531-hbsyhshmefdw0f93 --- openerp/osv/expression.py | 69 ++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index d2de370a459..1a2fd0cc0ca 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -385,14 +385,14 @@ class expression(object): self.table_aliases_mapping = {} self.joins = [] self.root_table = None - # assign self.__exp with the normalized, parsed domain. + # assign self.exp with the normalized, parsed domain. self.parse(cr, uid, distribute_not(normalize(exp)), table, context) # TDE note: this seems not to be used anymore, commenting # # TODO used only for osv_memory # @property # def exp(self): - # return self.__exp[:] + # return self.exp[:] def _has_table_alias(self, alias): return alias in self.table_aliases @@ -434,7 +434,7 @@ class expression(object): :param exp: expression (domain) :param table: table object """ - self.__exp = exp + self.exp = exp self.root_table = table self._add_table_alias(table._table, table) @@ -476,7 +476,7 @@ class expression(object): return list(value) i = -1 - while i + 1 < len(self.__exp): + while i + 1 < len(self.exp): i += 1 # 0 Preparation @@ -485,7 +485,7 @@ class expression(object): # - Set working variables # check validity - e = self.__exp[i] + e = self.exp[i] if is_operator(e) or e == TRUE_LEAF or e == FALSE_LEAF: continue if not is_leaf(e): @@ -493,7 +493,7 @@ class expression(object): # normalize the leaf's operator e = normalize_leaf(*e) - self.__exp[i] = e + self.exp[i] = e left, operator, right = e # working variables @@ -517,15 +517,16 @@ class expression(object): self.leaf_to_table[i] = working_table # 2 Field not found - # - ('id', 'child_of', ids) and continue the processing OR - # - field in magic columns (ex: id) and continue the processing OR + # - ('id', 'child_of', ids): replace the leaf by a computed domain + # after searching and continue the processing OR + # - field in magic columns (ex: id): continue the processing OR # - raise an error if not field: if left == 'id' and operator == 'child_of': ids2 = to_ids(right, table) dom = child_of_domain(left, ids2, working_table) - self.__exp = self.__exp[:i] + dom + self.__exp[i + 1:] + self.exp = self.exp[:i] + dom + self.exp[i + 1:] else: # field could not be found in model columns, it's probably invalid, unless # it's one of the _log_access special fields @@ -553,12 +554,12 @@ class expression(object): if len(field_path) > 1: if field._type == 'many2one': right = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) - self.__exp[i] = (field_path[0], 'in', right) + self.exp[i] = (field_path[0], 'in', right) # Making search easier when there is a left operand as field.o2m or field.m2m if field._type in ['many2many', 'one2many']: right = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) right1 = table.search(cr, uid, [(field_path[0], 'in', right)], context=dict(context, active_test=False)) - self.__exp[i] = ('id', 'in', right1) + self.exp[i] = ('id', 'in', right1) if not isinstance(field, fields.property): continue @@ -568,7 +569,7 @@ class expression(object): if not field._fnct_search: # the function field doesn't provide a search function and doesn't store # values in the database, so we must ignore it : we generate a dummy leaf - self.__exp[i] = TRUE_LEAF + self.exp[i] = TRUE_LEAF _logger.error( "The field '%s' (%s) can not be searched: non-stored " "function field without fnct_search", @@ -577,16 +578,16 @@ class expression(object): if _logger.isEnabledFor(logging.DEBUG): _logger.debug(''.join(traceback.format_stack())) else: - subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context) + subexp = field.search(cr, uid, table, left, [self.exp[i]], context=context) if not subexp: - self.__exp[i] = TRUE_LEAF + self.exp[i] = TRUE_LEAF else: # we assume that the expression is valid # we create a dummy leaf for forcing the parsing of the resulting expression - self.__exp[i] = AND_OPERATOR - self.__exp.insert(i + 1, TRUE_LEAF) + self.exp[i] = AND_OPERATOR + self.exp.insert(i + 1, TRUE_LEAF) for j, se in enumerate(subexp): - self.__exp.insert(i + 2 + j, se) + self.exp.insert(i + 2 + j, se) # else, the value of the field is store in the database, so we search on it elif field._type == 'one2many': @@ -597,7 +598,7 @@ class expression(object): dom = child_of_domain(left, ids2, relational_table, prefix=field._obj) else: dom = child_of_domain('id', ids2, working_table, parent=left) - self.__exp = self.__exp[:i] + dom + self.__exp[i + 1:] + self.exp = self.exp[:i] + dom + self.exp[i + 1:] else: call_null = True @@ -616,17 +617,17 @@ class expression(object): if operator in ['like', 'ilike', 'in', '=']: #no result found with given search criteria call_null = False - self.__exp[i] = FALSE_LEAF + self.exp[i] = FALSE_LEAF else: ids2 = select_from_where(cr, field._fields_id, relational_table._table, 'id', ids2, operator) if ids2: call_null = False o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - self.__exp[i] = ('id', o2m_op, ids2) + self.exp[i] = ('id', o2m_op, ids2) if call_null: o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - self.__exp[i] = ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)) + self.exp[i] = ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)) elif field._type == 'many2many': rel_table, rel_id1, rel_id2 = field._sql_names(working_table) @@ -640,7 +641,7 @@ class expression(object): ids2 = to_ids(right, relational_table) dom = child_of_domain('id', ids2, relational_table) ids2 = relational_table.search(cr, uid, dom, context=context) - self.__exp[i] = ('id', 'in', _rec_convert(ids2)) + self.exp[i] = ('id', 'in', _rec_convert(ids2)) else: call_null_m2m = True if right is not False: @@ -657,17 +658,17 @@ class expression(object): if operator in ['like', 'ilike', 'in', '=']: #no result found with given search criteria call_null_m2m = False - self.__exp[i] = FALSE_LEAF + self.exp[i] = FALSE_LEAF else: operator = 'in' # operator changed because ids are directly related to main object else: call_null_m2m = False m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - self.__exp[i] = ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]) + self.exp[i] = ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]) if call_null_m2m: m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - self.__exp[i] = ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)) + self.exp[i] = ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)) elif field._type == 'many2one': if operator == 'child_of': @@ -676,7 +677,7 @@ class expression(object): dom = child_of_domain(left, ids2, relational_table, prefix=field._obj) else: dom = child_of_domain('id', ids2, working_table, parent=left) - self.__exp = self.__exp[:i] + dom + self.__exp[i + 1:] + self.exp = self.exp[:i] + dom + self.exp[i + 1:] else: def _get_expression(relational_table, cr, uid, left, right, operator, context=None): if context is None: @@ -700,7 +701,7 @@ class expression(object): # resolve string-based m2o criterion into IDs if isinstance(right, basestring) or \ right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right): - self.__exp[i] = _get_expression(relational_table, cr, uid, left, right, operator, context=context) + self.exp[i] = _get_expression(relational_table, cr, uid, left, right, operator, context=context) else: # right == [] or right == False and all other cases are handled by __leaf_to_sql() pass @@ -708,16 +709,16 @@ class expression(object): else: # other field type # add the time part to datetime field when it's not there: - if field._type == 'datetime' and self.__exp[i][2] and len(self.__exp[i][2]) == 10: + if field._type == 'datetime' and self.exp[i][2] and len(self.exp[i][2]) == 10: - self.__exp[i] = list(self.__exp[i]) + self.exp[i] = list(self.exp[i]) if operator in ('>', '>='): - self.__exp[i][2] += ' 00:00:00' + self.exp[i][2] += ' 00:00:00' elif operator in ('<', '<='): - self.__exp[i][2] += ' 23:59:59' + self.exp[i][2] += ' 23:59:59' - self.__exp[i] = tuple(self.__exp[i]) + self.exp[i] = tuple(self.exp[i]) if field.translate: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') @@ -753,7 +754,7 @@ class expression(object): right, ] - self.__exp[i] = ('id', 'inselect', (subselect, params)) + self.exp[i] = ('id', 'inselect', (subselect, params)) def __leaf_to_sql(self, leaf, table): left, operator, right = leaf @@ -885,7 +886,7 @@ class expression(object): stack = [] params = [] # Process the domain from right to left, using a stack, to generate a SQL expression. - for i, e in reverse_enumerate(self.__exp): + for i, e in reverse_enumerate(self.exp): if is_leaf(e, internal=True): table = self.leaf_to_table.get(i, self.root_table) q, p = self.__leaf_to_sql(e, table) From 9b196638cb22a1b7d4e2a9a91b5ce1f1f2380e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 27 Nov 2012 18:01:17 +0100 Subject: [PATCH 11/39] [IMP] expression: replaced field paths now use an alias.field leaf style to prepare the inclusion of joins. In leaf_to_sql, handle this case accordingly. bzr revid: tde@openerp.com-20121127170117-qlhfs0r1ned5uz3c --- openerp/osv/expression.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 1a2fd0cc0ca..947d392065e 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -536,7 +536,9 @@ class expression(object): continue # 3 Field found - # - get relational table, existing for relational fields + # - update working variables + # - get relational table that exists for relational fields + # - prepare the alias for tables, that can be the table name if only one-level # - if domain is a path (ex: ('partner_id.name', '=', 'foo')): # replace all the expression by a normalized equivalent domain # - find the related ids: partner_id.name='foo' -> res_partner.search(('name', '=', 'foo'))) @@ -545,21 +547,23 @@ class expression(object): # - search on current table where partner_id is in partner_ids # - leaf becomes ('id', 'in', ids) # - get out of current leaf is field is not a property field - # - if domain is not a path + # - if domain is not a path: handle some leaf replacement / tweaking # - handle function fields # - handle one2many, many2many and many2one fields # - other fields: handle datetime and translatable fields relational_table = table.pool.get(field._obj) + alias = working_table._table + if len(field_path) > 1: if field._type == 'many2one': right = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) - self.exp[i] = (field_path[0], 'in', right) + self.exp[i] = (alias + '.' + field_path[0], 'in', right) # Making search easier when there is a left operand as field.o2m or field.m2m if field._type in ['many2many', 'one2many']: right = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) right1 = table.search(cr, uid, [(field_path[0], 'in', right)], context=dict(context, active_test=False)) - self.exp[i] = ('id', 'in', right1) + self.exp[i] = (alias + '.id', 'in', right1) if not isinstance(field, fields.property): continue @@ -763,9 +767,20 @@ class expression(object): assert operator in (TERM_OPERATORS + ('inselect',)), \ "Invalid operator %r in domain term %r" % (operator, leaf) assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in table._all_columns \ - or left in MAGIC_COLUMNS, "Invalid field %r in domain term %r" % (left, leaf) + or left in MAGIC_COLUMNS \ + or ('.' in left and self._has_table_alias(left.split('.')[0])), \ + "Invalid field %r in domain term %r" % (left, leaf) - table_alias = table._table + if not leaf in (TRUE_LEAF, FALSE_LEAF) and '.' in left: + # leaf still contains '.' -> should be aliases (alias.field) + # update table with alias, and left with field + leaf_path = left.split('.') + assert len(leaf_path) == 2, "Invalid leaf with alias %r in leaf %r" % (left, leaf) + table_alias = leaf_path[0] + table = self._get_table_from_alias(table_alias) + left = leaf_path[1] + else: + table_alias = table._table if leaf == TRUE_LEAF: query = 'TRUE' From 0544565a57db056005bb098ffc92513e5b084159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 28 Nov 2012 10:42:34 +0100 Subject: [PATCH 12/39] [REV] ir_ui_menu: temporarily set needaction_enabled column back to non-stored, to be able to use the branch on internal test database with needaction, without requiring too many hand-made modifications. bzr revid: tde@openerp.com-20121128094234-ux06ludum7ub59u5 --- openerp/addons/base/ir/ir_ui_menu.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index ef9ed3b7bf3..f236845dfc7 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -309,7 +309,8 @@ class ir_ui_menu(osv.osv): 'web_icon_data': fields.function(_get_image_icon, string='Web Icon Image', type='binary', readonly=True, store=True, multi='icon'), 'web_icon_hover_data': fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'), 'needaction_enabled': fields.function(_get_needaction_enabled, - type='boolean', store=True, + type='boolean', + # store=True, string='Target model uses the need action mechanism', help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.'), 'action': fields.function(_action, fnct_inv=_action_inv, From ac587f68fec384730e4fbb748cd3662659037660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 28 Nov 2012 10:50:42 +0100 Subject: [PATCH 13/39] [IMP] expression.py: added support of _auto_join in parse. The purpose is to add join clauses to queries instead of replacing many2one or one2many fields by id in ... equivalent leafs. This should lessen the number of executed queries, as well as the time required for some queries like mailboxes queries. Updated the generation of full table aliases when generating SQL, to avoid having a table having it own name as alias. bzr revid: tde@openerp.com-20121128095042-x5rzgwydv0g9uy0m --- openerp/osv/expression.py | 47 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 947d392065e..4f2ae3989b9 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -403,7 +403,11 @@ class expression(object): def _get_full_alias(self, alias): if not self._get_table_from_alias(alias): return False - return '%s as %s' % (self._get_table_from_alias(alias)._table, alias) + table_name = self._get_table_from_alias(alias)._table + if table_name == alias: + return '"%s"' % alias + else: + return '"%s" as %s' % (table_name, alias) def _add_table_alias(self, alias, table): if not self._has_table_alias(alias): @@ -414,7 +418,7 @@ class expression(object): def get_tables(self): """ Returns the list of tables for SQL queries, like select from ... """ - return ['"%s"' % item for item in self.table_aliases] + return [self._get_full_alias(item) for item in self.table_aliases] def parse(self, cr, uid, exp, table, context): """ Transform the leaves of the expression @@ -540,6 +544,7 @@ class expression(object): # - get relational table that exists for relational fields # - prepare the alias for tables, that can be the table name if only one-level # - if domain is a path (ex: ('partner_id.name', '=', 'foo')): + # TDE TO ADD: once fully implemented, explain auto_join # replace all the expression by a normalized equivalent domain # - find the related ids: partner_id.name='foo' -> res_partner.search(('name', '=', 'foo'))) # - many2one: leaf becomes directly ('partner_id', 'in', 'partner_ids') @@ -555,6 +560,44 @@ class expression(object): relational_table = table.pool.get(field._obj) alias = working_table._table + while len(field_path) > 1 and field._auto_join: + if not field._type in ['many2one', 'one2many']: + raise '_auto_join attribute on something else than a many2one or one2many is currently not implemented... crashing' + + previous_alias = alias + alias = alias + '__' + field_path[0] + if not self._get_table_from_alias(alias): + self._add_table_alias(alias, relational_table) + if field._type == 'many2one': + self.joins.append('%s."%s"=%s."%s"' % (alias, 'id', previous_alias, field_path[0])) + elif field._type == 'one2many': + self.joins.append('%s."%s"=%s."%s"' % (alias, field._fields_id, previous_alias, 'id')) + self.exp[i] = (alias + '.' + field_path[1], self.exp[i][1], self.exp[i][2]) + + # udpate working variables + field_path = field_path[1].split('.', 1) + working_table = relational_table + field = working_table._columns.get(field_path[0]) + + if not field: + if left == 'id' and operator == 'child_of': + ids2 = to_ids(right, table) + dom = child_of_domain(left, ids2, working_table) + self.exp = self.exp[:i] + dom + self.exp[i + 1:] + else: + # field could not be found in model columns, it's probably invalid, unless + # it's one of the _log_access special fields + # TODO: make these fields explicitly available in self.columns instead! + if field_path[0] not in MAGIC_COLUMNS: + raise ValueError("Invalid field %r in domain expression %r" % (left, exp)) + break + + # moved on top + relational_table = table.pool.get(field._obj) + + if not field: + continue + if len(field_path) > 1: if field._type == 'many2one': right = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) From 4be660369d6c00e53f5c0a102d13399916851a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 28 Nov 2012 12:04:28 +0100 Subject: [PATCH 14/39] [DOC] ir_ui_menu: added some comments to the methods related to needaction. bzr revid: tde@openerp.com-20121128110428-lebsx4s3zlmgdhxj --- openerp/addons/base/ir/ir_ui_menu.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index f236845dfc7..fa4e5b2b540 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -266,6 +266,8 @@ class ir_ui_menu(osv.osv): return res def _get_needaction_enabled(self, cr, uid, ids, field_names, args, context=None): + """ needaction_enabled: tell whether the menu has a related action + that uses the needaction mechanism. """ res = dict.fromkeys(ids, False) for menu in self.browse(cr, uid, ids, context=context): if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model: @@ -275,6 +277,11 @@ class ir_ui_menu(osv.osv): return res def get_needaction_data(self, cr, uid, ids, context=None): + """ Return for each menu entry of ids : + - if it uses the needaction mechanism (needaction_enabled) + - the needaction counter of the related action, taking into account + the action domain + """ res = {} for menu in self.browse(cr, uid, ids, context=context): res[menu.id] = { @@ -310,7 +317,7 @@ class ir_ui_menu(osv.osv): 'web_icon_hover_data': fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'), 'needaction_enabled': fields.function(_get_needaction_enabled, type='boolean', - # store=True, + # store=True, TDE TODO: uncomment after internal tests string='Target model uses the need action mechanism', help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.'), 'action': fields.function(_action, fnct_inv=_action_inv, From a853b9f2ad72136780d02402b01e107d2ece77dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 28 Nov 2012 15:00:42 +0100 Subject: [PATCH 15/39] [TEST] Added some tests about the _auto_join feature. bzr revid: tde@openerp.com-20121128140042-4r58wnzahk0io3lw --- openerp/addons/base/tests/test_expression.py | 277 +++++++++++++++++-- 1 file changed, 252 insertions(+), 25 deletions(-) diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index 0f9241bce86..1eb22ccba43 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -15,8 +15,19 @@ class test_expression(common.TransactionCase): # return the lastly stored query, the one the ORM wants to perform return self.query_list[-1] - def test_00_in_not_in_m2m(self): + def setUp(self): + super(test_expression, self).setUp() + # Mock BaseModel._where_calc(), to be able to proceed to some tests about generated expression + self._reinit_mock() + self._base_model_where_calc = BaseModel._where_calc + BaseModel._where_calc = lambda model, cr, uid, args, context: self._mock_base_model_where_calc(model, cr, uid, args, context) + def tearDown(self): + # Remove mocks + BaseModel._where_calc = self._base_model_where_calc + super(test_expression, self).tearDown() + + def test_00_in_not_in_m2m(self): registry, cr, uid = self.registry, self.cr, self.uid # Create 4 partners with no category, or one or two categories (out of two categories). @@ -84,34 +95,36 @@ class test_expression(common.TransactionCase): def test_10_auto_join(self): registry, cr, uid = self.registry, self.cr, self.uid - # Mock BaseModel._where_calc(), to be able to proceed to some tests about generated expression - self._reinit_mock() - self._base_model_where_calc = BaseModel._where_calc - BaseModel._where_calc = lambda model, cr, uid, args, context: self._mock_base_model_where_calc(model, cr, uid, args, context) - # Get models partner_obj = registry('res.partner') state_obj = registry('res.country.state') bank_obj = registry('res.partner.bank') # Get test columns - state_id_col = partner_obj._columns.get('state_id') # many2one on res.partner to res.country.state - child_ids_col = partner_obj._columns.get('child_ids') # one2many on res.partner to res.partner - bank_ids_col = partner_obj._columns.get('bank_ids') # one2many on res.partner to res.partner.bank - country_id_col = state_obj._columns.get('country_id') # many2one on res.country.state on res.country + partner_state_id_col = partner_obj._columns.get('state_id') # many2one on res.partner to res.country.state + partner_parent_id_col = partner_obj._columns.get('parent_id') # many2one on res.partner to res.partner + partner_child_ids_col = partner_obj._columns.get('child_ids') # one2many on res.partner to res.partner + partner_bank_ids_col = partner_obj._columns.get('bank_ids') # one2many on res.partner to res.partner.bank + state_country_id_col = state_obj._columns.get('country_id') # many2one on res.country.state on res.country # Get the first bank account type to be able to create a res.partner.bank bank_type = bank_obj._bank_type_get(cr, uid)[0] + # Get country/state data + country_us_id = registry('res.country').search(cr, uid, [('code', 'like', 'US')])[0] + state_ids = registry('res.country.state').search(cr, uid, [('country_id', '=', country_us_id)], limit=2) # Create demo data: partners and bank object - p_a = partner_obj.create(cr, uid, {'name': 'test__A'}) - p_b = partner_obj.create(cr, uid, {'name': 'test__B'}) - p_aa = partner_obj.create(cr, uid, {'name': 'test__AA', 'parent_id': p_a}) - p_ab = partner_obj.create(cr, uid, {'name': 'test__AB', 'parent_id': p_a}) - b_a = bank_obj.create(cr, uid, {'name': '__bank_test_a', 'state': bank_type[0], 'partner_id': p_a, 'acc_number': '1234'}) + p_a = partner_obj.create(cr, uid, {'name': 'test__A', 'state_id': state_ids[0]}) + p_b = partner_obj.create(cr, uid, {'name': 'test__B', 'state_id': state_ids[1]}) + p_aa = partner_obj.create(cr, uid, {'name': 'test__AA', 'parent_id': p_a, 'state_id': state_ids[0]}) + p_ab = partner_obj.create(cr, uid, {'name': 'test__AB', 'parent_id': p_a, 'state_id': state_ids[1]}) + p_ba = partner_obj.create(cr, uid, {'name': 'test__BA', 'parent_id': p_b, 'state_id': state_ids[0]}) + b_aa = bank_obj.create(cr, uid, {'name': '__bank_test_a', 'state': bank_type[0], 'partner_id': p_aa, 'acc_number': '1234'}) + b_ab = bank_obj.create(cr, uid, {'name': '__bank_test_b', 'state': bank_type[0], 'partner_id': p_ab, 'acc_number': '5678'}) + b_ba = bank_obj.create(cr, uid, {'name': '__bank_test_b', 'state': bank_type[0], 'partner_id': p_ba, 'acc_number': '9876'}) # ---------------------------------------- - # Test2: one2many + # Test1: one2many # ---------------------------------------- name_test = 'test_a' @@ -120,29 +133,243 @@ class test_expression(common.TransactionCase): self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('bank_ids.name', 'like', name_test)]) # Test result - self.assertEqual(set(partner_ids), set([p_a]), 'one2many without join failed') + self.assertEqual(set(partner_ids), set([p_aa]), + "_auto_join off: ('bank_ids.name', 'like', '..'): incorrect result") # Test produced queries self.assertEqual(len(self.query_list), 3, "_auto_join off: ('bank_ids.name', 'like', '..') should produce 3 queries (1 in res_partner_bank, 1 on res_partner with active, 1 on res_partner)") sql_query = self.query_list[0].get_sql() - self.assertIn('res_partner_bank', sql_query[0], "_auto_join off: ('bank_ids.name', 'like', '..') first query should be done in res_partner_bank") - self.assertIn('(res_partner_bank."name" like %s)', sql_query[1], "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect where condition") - self.assertEqual(set(['%' + name_test + '%']), set(sql_query[2]), "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect parameter") + self.assertIn('res_partner_bank', sql_query[0], + "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect main table") + self.assertIn('res_partner_bank."name" like %s', sql_query[1], + "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect where condition") + self.assertEqual(set(['%' + name_test + '%']), set(sql_query[2]), + "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect parameter") sql_query = self.query_list[2].get_sql() - self.assertIn('res_partner', sql_query[0], "_auto_join off: ('bank_ids.name', 'like', '..') third query should be done in res_partner") - self.assertIn('(res_partner."id" in (%s))', sql_query[1], "_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect where condition") - self.assertEqual(set([p_a]), set(sql_query[2]), "_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect parameter") + self.assertIn('res_partner', sql_query[0], + "_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect main table") + self.assertIn('res_partner."id" in (%s)', sql_query[1], + "_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect where condition") + self.assertEqual(set([p_aa]), set(sql_query[2]), + "_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect parameter") + + # Do: cascaded one2many without _auto_join + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, [('child_ids.bank_ids.id', 'in', [b_aa, b_ba])]) + # Test result + self.assertEqual(set(partner_ids), set([p_a, p_b]), + "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]): incorrect result") + # Test produced queries + self.assertEqual(len(self.query_list), 5, + "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 5 queries (1 in res_partner_bank, 2 on res_partner with active, 2 on res_partner, childs then parents)") + + # Do: one2many with _auto_join + partner_bank_ids_col._auto_join = True + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, [('bank_ids.name', 'like', 'test_a')]) + # Test result + self.assertEqual(set(partner_ids), set([p_aa]), + "_auto_join on: ('bank_ids.name', 'like', '..') incorrect result") + # # Test produced queries + self.assertEqual(len(self.query_list), 1, + "_auto_join on: ('bank_ids.name', 'like', '..') should produce 1 query") + sql_query = self.query_list[0].get_sql() + self.assertIn('"res_partner"', sql_query[0], + "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect main table") + self.assertIn('"res_partner_bank" as res_partner__bank_ids', sql_query[0], + "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect join") + self.assertIn('res_partner__bank_ids."name" like %s', sql_query[1], + "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect where condition") + self.assertIn('res_partner__bank_ids."partner_id"=res_partner."id"', sql_query[1], + "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect join condition") + self.assertEqual(set(['%' + name_test + '%']), set(sql_query[2]), + "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect parameter") + + # Do: one2many with _auto_join, test final leaf is an id + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, [('bank_ids.id', 'in', [b_aa, b_ab])]) + # Test result + self.assertEqual(set(partner_ids), set([p_aa, p_ab]), + "_auto_join on: ('bank_ids.id', 'in', [..]) incorrect result") + # # Test produced queries + self.assertEqual(len(self.query_list), 1, + "_auto_join on: ('bank_ids.id', 'in', [..]) should produce 1 query") + sql_query = self.query_list[0].get_sql() + self.assertIn('"res_partner"', sql_query[0], + "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect main table") + self.assertIn('"res_partner_bank" as res_partner__bank_ids', sql_query[0], + "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect join") + self.assertIn('res_partner__bank_ids."id" in (%s,%s)', sql_query[1], + "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect where condition") + self.assertIn('res_partner__bank_ids."partner_id"=res_partner."id"', sql_query[1], + "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect join condition") + self.assertEqual(set([b_aa, b_ab]), set(sql_query[2]), + "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect parameter") + + # Do: 2 cascaded one2many with _auto_join, test final leaf is an id + partner_child_ids_col._auto_join = True + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, [('child_ids.bank_ids.id', 'in', [b_aa, b_ba])]) + # Test result + self.assertEqual(set(partner_ids), set([p_a, p_b]), + "_auto_join on: ('child_ids.bank_ids.id', 'not in', [..]): incorrect result") + # # Test produced queries + self.assertEqual(len(self.query_list), 1, + "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) should produce 1 query") + sql_query = self.query_list[0].get_sql() + self.assertIn('"res_partner"', sql_query[0], + "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) incorrect main table") + self.assertIn('"res_partner" as res_partner__child_ids', sql_query[0], + "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join") + self.assertIn('"res_partner_bank" as res_partner__child_ids__bank_ids', sql_query[0], + "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join") + self.assertIn('res_partner__child_ids__bank_ids."id" in (%s,%s)', sql_query[1], + "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect where condition") + self.assertIn('res_partner__child_ids."parent_id"=res_partner."id"', sql_query[1], + "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join condition") + self.assertIn('res_partner__child_ids__bank_ids."partner_id"=res_partner__child_ids."id"', sql_query[1], + "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join condition") + self.assertEqual(set([b_aa, b_ba]), set(sql_query[2]), + "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect parameter") # ---------------------------------------- # Test2: many2one # ---------------------------------------- + name_test = 'US' + + # Do: many2one without _auto_join + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) + # Test result: at least our added data + demo data + self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), + "_auto_join off: ('state_id.country_id.code', 'like', '..') incorrect result") + # Test produced queries + self.assertEqual(len(self.query_list), 3, + "_auto_join off: ('state_id.country_id.code', 'like', '..') should produce 3 queries (1 on res_country, 1 on res_country_state, 1 on res_partner)") + + # Do: many2one with 1 _auto_join on the first many2one + partner_state_id_col._auto_join = True + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) + # Test result: at least our added data + demo data + self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), + "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') incorrect result") + # Test produced queries + self.assertEqual(len(self.query_list), 2, + "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') should produce 2 query") + sql_query = self.query_list[0].get_sql() + self.assertIn('"res_country"', sql_query[0], + "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect main table") + self.assertIn('res_country."code" like %s', sql_query[1], + "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect where condition") + self.assertEqual(['%' + name_test + '%'], sql_query[2], + "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect parameter") + sql_query = self.query_list[1].get_sql() + self.assertIn('"res_partner"', sql_query[0], + "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect main table") + self.assertIn('"res_country_state" as res_partner__state_id', sql_query[0], + "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect join") + self.assertIn('res_partner__state_id."country_id" in (%s)', sql_query[1], + "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect where condition") + self.assertIn('(res_partner__state_id."id"=res_partner."state_id"', sql_query[1], + "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect join condition") + + # Do: many2one with 1 _auto_join on the second many2one + partner_state_id_col._auto_join = False + state_country_id_col._auto_join = True + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) + # Test result: at least our added data + demo data + self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), + "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') incorrect result") + # Test produced queries + self.assertEqual(len(self.query_list), 2, + "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') should produce 2 query") + # -- first query + sql_query = self.query_list[0].get_sql() + self.assertIn('"res_country_state"', sql_query[0], + "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect main table") + self.assertIn('"res_country" as res_country_state__country_id', sql_query[0], + "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect join") + self.assertIn('res_country_state__country_id."code" like %s', sql_query[1], + "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect where condition") + self.assertIn('res_country_state__country_id."id"=res_country_state."country_id"', sql_query[1], + "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect join condition") + self.assertEqual(['%' + name_test + '%'], sql_query[2], + "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect parameter") + # -- second query + sql_query = self.query_list[1].get_sql() + self.assertIn('"res_partner"', sql_query[0], + "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect main table") + self.assertIn('res_partner."state_id" in', sql_query[1], + "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect where condition") + + # Do: many2one with 2 _auto_join + partner_state_id_col._auto_join = True + state_country_id_col._auto_join = True + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) + # Test result: at least our added data + demo data + self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), + "_auto_join on: ('state_id.country_id.code', 'like', '..') incorrect result") + # Test produced queries + self.assertEqual(len(self.query_list), 1, + "_auto_join on: ('state_id.country_id.code', 'like', '..') should produce 1 query") + sql_query = self.query_list[0].get_sql() + self.assertIn('"res_partner"', sql_query[0], + "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect main table") + self.assertIn('"res_country_state" as res_partner__state_id', sql_query[0], + "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join") + self.assertIn('"res_country" as res_partner__state_id__country_id', sql_query[0], + "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join") + self.assertIn('res_partner__state_id__country_id."code" like %s', sql_query[1], + "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect where condition") + self.assertIn('res_partner__state_id."id"=res_partner."state_id"', sql_query[1], + "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join condition") + self.assertIn('res_partner__state_id__country_id."id"=res_partner__state_id."country_id"', sql_query[1], + "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join condition") + self.assertEqual(['%' + name_test + '%'], sql_query[2], + "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect parameter") + # ---------------------------------------- - # Test2: more complex tests + # Test3: result-based tests # ---------------------------------------- + partner_bank_ids_col._auto_join = False + partner_child_ids_col._auto_join = False + partner_state_id_col._auto_join = False + partner_parent_id_col._auto_join = False + state_country_id_col._auto_join = False + + # Do: ('child_ids.state_id.country_id.code', 'like', '..') without _auto_join + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, [('child_ids.state_id.country_id.code', 'like', name_test)]) + # Test result: at least our added data + demo data + self.assertTrue(set([p_a, p_b]).issubset(set(partner_ids)), + "_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result") + # Test produced queries + self.assertEqual(len(self.query_list), 5, + "_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') number of queries incorrect") + + # Do: ('child_ids.state_id.country_id.code', 'like', '..') with _auto_join + partner_child_ids_col._auto_join = True + partner_state_id_col._auto_join = True + state_country_id_col._auto_join = True + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, [('child_ids.state_id.country_id.code', 'like', name_test)]) + # Test result: at least our added data + demo data + self.assertTrue(set([p_a, p_b]).issubset(set(partner_ids)), + "_auto_join on: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result") + # Test produced queries + self.assertEqual(len(self.query_list), 1, + "_auto_join on: ('child_ids.state_id.country_id.code', 'like', '..') number of queries incorrect") + # Remove mocks and modifications - bank_ids_col._auto_join = False + partner_bank_ids_col._auto_join = False + partner_child_ids_col._auto_join = False + partner_state_id_col._auto_join = False + state_country_id_col._auto_join = False BaseModel._where_calc = self._base_model_where_calc if __name__ == '__main__': From 47fda8226445590d604a2899457a25e290eda8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 28 Nov 2012 19:12:58 +0100 Subject: [PATCH 16/39] [FIX] expression.parse: fixed indentation error preventing leafs to be correctly replaced in auto_join. bzr revid: tde@openerp.com-20121128181258-cgmm7wqgv657iptb --- openerp/osv/expression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 4f2ae3989b9..5fb18fb8cc1 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -572,7 +572,7 @@ class expression(object): self.joins.append('%s."%s"=%s."%s"' % (alias, 'id', previous_alias, field_path[0])) elif field._type == 'one2many': self.joins.append('%s."%s"=%s."%s"' % (alias, field._fields_id, previous_alias, 'id')) - self.exp[i] = (alias + '.' + field_path[1], self.exp[i][1], self.exp[i][2]) + self.exp[i] = (alias + '.' + field_path[1], self.exp[i][1], self.exp[i][2]) # udpate working variables field_path = field_path[1].split('.', 1) From 37a949e30afc3cb7b604a33c535ace79e62d5490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 29 Nov 2012 16:28:52 +0100 Subject: [PATCH 17/39] [DOC] First draft of doc on _auto_join, to at least have something. bzr revid: tde@openerp.com-20121129152852-akslcby3ics61vuz --- doc/06_misc.rst | 1 + doc/06_misc_auto_join.rst | 64 ++++++++++++++++++++ openerp/addons/base/tests/test_expression.py | 4 +- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 doc/06_misc_auto_join.rst diff --git a/doc/06_misc.rst b/doc/06_misc.rst index c82022b812e..5a4ae7e008a 100644 --- a/doc/06_misc.rst +++ b/doc/06_misc.rst @@ -10,3 +10,4 @@ Miscellanous 06_misc_need_action_specs.rst 06_misc_user_img_specs.rst 06_misc_import.rst + 06_misc_auto_join.rst diff --git a/doc/06_misc_auto_join.rst b/doc/06_misc_auto_join.rst new file mode 100644 index 00000000000..dab24f1bf09 --- /dev/null +++ b/doc/06_misc_auto_join.rst @@ -0,0 +1,64 @@ +.. _performing_joins_in_select: + +Perfoming joins in select +========================= + +.. versionadded:: 7.0 + +Starting with OpenERP 7.0, an ``_auto_join`` attribute is added on *many2one* and +*one2many* fields. The purpose is to allow the automatic generation of joins in +select queries. This attribute is set to False by default, therefore not changing +the default behavior of those fields. It is not recommended to use this attribute +unless you understand the limitations of the feature. + +Without ``_auto_join``, the behavior of expression.parse() is the same as before. +Leafs holding a path beginning with many2one or one2many fields perform a search +on the relational table. The result is then used to replace the leaf content. +For example, if you have on res.partner a domain like ``[('bank_ids.name', +'like', 'foo')]`` with bank_ids linking to res.partner.bank, 3 queries will be +performed : + +- 1 on res_partner_bank, with domain ``[('name', '=', 'foo')]``, that returns a + list of (res.partner.bank) bids +- 1 on res_partner, with a domain ``['bank_ids', 'in', bids)]``, that returns a + list of (res.partner) pids +- 1 on res_partner, with a domain ``[('id', 'in', pids)]`` + +When the _auto_join attribute is True, it will perform a select on res_partner +as well as on res_partner_bank. + +- the relational table will be accessed through an alias: ``'"res_partner_bank" + as res_partner__bank_ids`` +- the relational table will have a join condition on the main table: + ``res_partner__bank_ids."partner_id"=res_partner."id"`` +- the condition will be written on the relational table: + ``res_partner__bank_ids."name" = 'foo'`` + +This job is performed in expression.parse(). For leafs containing a path, it +checks whether the first item of the path is a *many2one* or *one2many* field +with the ``auto_join`` attribute set. If set, it adds a join query and recursively +analyzes the remaining of the leaf, going back to the normal behavior when +not reaching an ``_auto_join`` field. The sql condition created from the leaf +will be updated to take into account the table aliases. + +Chaining _auto_join allows to reduce the number of queries performed, and to +avoid having too long ``('id', 'in', ids)`` replacement leafs in domains. +However, severe limitations exist on this feature that limits its current use as +of version 7.0. **This feature is therefore considered as experimental, and used +to speedup some precise bottlenecks in OpenERP**. + +List of known issues and limitations: + +- using _auto_join bypasses the business logic; no name search is performed, only + direct matches between ids using join conditions +- ir.rules are not taken into account when performing the _auto_join. +- support of active_test is not asserted +- support of translation is not asserted +- support of _auto_join leading to function fields + +Typical use in OpenERP 7.0: + +- in mail module: notification_ids field on mail_message, allowing to speedup + the display of the various mailboxes +- in mail module: message_ids field on mail_thread, allowing to speedup the + display of needaction counters and documents having unread messages diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index 1eb22ccba43..d3fe9de3efd 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -137,7 +137,7 @@ class test_expression(common.TransactionCase): "_auto_join off: ('bank_ids.name', 'like', '..'): incorrect result") # Test produced queries self.assertEqual(len(self.query_list), 3, - "_auto_join off: ('bank_ids.name', 'like', '..') should produce 3 queries (1 in res_partner_bank, 1 on res_partner with active, 1 on res_partner)") + "_auto_join off: ('bank_ids.name', 'like', '..') should produce 3 queries (1 in res_partner_bank, 2 on res_partner)") sql_query = self.query_list[0].get_sql() self.assertIn('res_partner_bank', sql_query[0], "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect main table") @@ -161,7 +161,7 @@ class test_expression(common.TransactionCase): "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]): incorrect result") # Test produced queries self.assertEqual(len(self.query_list), 5, - "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 5 queries (1 in res_partner_bank, 2 on res_partner with active, 2 on res_partner, childs then parents)") + "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 5 queries (1 in res_partner_bank, 4 on res_partner)") # Do: one2many with _auto_join partner_bank_ids_col._auto_join = True From 3c8e01ec7f3ba68f2adb0ea29e08b65a23be0e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 29 Nov 2012 16:57:31 +0100 Subject: [PATCH 18/39] [IMP] ir_ui_menu: needaction_enabled is back to store True. bzr revid: tde@openerp.com-20121129155731-xk5cm9yvzbuyxpdc --- openerp/addons/base/ir/ir_ui_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index fa4e5b2b540..7d5915c89a8 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -317,7 +317,7 @@ class ir_ui_menu(osv.osv): 'web_icon_hover_data': fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'), 'needaction_enabled': fields.function(_get_needaction_enabled, type='boolean', - # store=True, TDE TODO: uncomment after internal tests + store=True, string='Target model uses the need action mechanism', help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.'), 'action': fields.function(_action, fnct_inv=_action_inv, From 681d7832341c4b39fb331455bf4fd53be9746cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 29 Nov 2012 17:50:32 +0100 Subject: [PATCH 19/39] [IMP] expression.parse: added some asserts about non supported cases. bzr revid: tde@openerp.com-20121129165032-nnlnf1k0iwal24ny --- openerp/osv/expression.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 5fb18fb8cc1..b13586f5311 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -561,8 +561,8 @@ class expression(object): alias = working_table._table while len(field_path) > 1 and field._auto_join: - if not field._type in ['many2one', 'one2many']: - raise '_auto_join attribute on something else than a many2one or one2many is currently not implemented... crashing' + assert field._type in ['many2one', 'one2many'], \ + '_auto_join attribute on something else than a many2one or one2many is currently not supported' previous_alias = alias alias = alias + '__' + field_path[0] @@ -625,6 +625,8 @@ class expression(object): if _logger.isEnabledFor(logging.DEBUG): _logger.debug(''.join(traceback.format_stack())) else: + assert working_table == table, \ + 'function fields can not be contained in paths using _auto_join' subexp = field.search(cr, uid, table, left, [self.exp[i]], context=context) if not subexp: self.exp[i] = TRUE_LEAF From 9e6c2805abe603818ba112471bd1221fce7eeedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 4 Dec 2012 15:26:50 +0100 Subject: [PATCH 20/39] [REF] [IMP] expression.parse: refactored the main parsing loop, now using source and result stacks. Leaf elements are encapsulated into a specific object, allowing to delegate the join condition formation and aliases generation, as well as working_table management to them. The main parsing loop is now a loop taking a leaf, doing one action on it, and putting the result back to be analyzed or in the results. This allows to avoid having while loops inside a main while loop with some weird corner effects. bzr revid: tde@openerp.com-20121204142650-gkhmjdeu5upi25kp --- openerp/addons/base/ir/ir_rule.py | 4 +- openerp/addons/base/tests/test_expression.py | 104 ++- openerp/osv/expression.py | 912 +++++++++++-------- openerp/osv/orm.py | 9 +- openerp/osv/query.py | 4 +- openerp/tests/test_expression.py | 11 +- 6 files changed, 650 insertions(+), 394 deletions(-) diff --git a/openerp/addons/base/ir/ir_rule.py b/openerp/addons/base/ir/ir_rule.py index a4341436158..ba4665ddd69 100644 --- a/openerp/addons/base/ir/ir_rule.py +++ b/openerp/addons/base/ir/ir_rule.py @@ -52,7 +52,7 @@ class ir_rule(osv.osv): eval_context = self._eval_context(cr, uid) for rule in self.browse(cr, uid, ids, context): if rule.domain_force: - res[rule.id] = expression.normalize(eval(rule.domain_force, eval_context)) + res[rule.id] = expression.normalize_domain(eval(rule.domain_force, eval_context)) else: res[rule.id] = [] return res @@ -130,7 +130,7 @@ class ir_rule(osv.osv): for rule in self.browse(cr, SUPERUSER_ID, rule_ids): # read 'domain' as UID to have the correct eval context for the rule. rule_domain = self.read(cr, uid, rule.id, ['domain'])['domain'] - dom = expression.normalize(rule_domain) + dom = expression.normalize_domain(rule_domain) for group in rule.groups: if group in user.groups_id: group_domains.setdefault(group, []).append(dom) diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index d3fe9de3efd..5286e1c5657 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -92,7 +92,30 @@ class test_expression(common.TransactionCase): # self.assertTrue(a not in with_any_other_than_a, "Search for category_id with any other than cat_a failed (1).") # self.assertTrue(ab in with_any_other_than_a, "Search for category_id with any other than cat_a failed (2).") - def test_10_auto_join(self): + def test_10_expression_parse(self): + # TDE note: those tests have been added when refactoring the expression.parse() method. + # They come in addition to the already existing test_osv_expression.yml; maybe some tests + # will be a bit redundant + registry, cr, uid = self.registry, self.cr, self.uid + users_obj = registry('res.users') + + # Create users + a = users_obj.create(cr, uid, {'name': 'test_A', 'login': 'test_A'}) + b1 = users_obj.create(cr, uid, {'name': 'test_B', 'login': 'test_B'}) + b1_user = users_obj.browse(cr, uid, [b1])[0] + b2 = users_obj.create(cr, uid, {'name': 'test_B2', 'login': 'test_B2', 'parent_id': b1_user.partner_id.id}) + + # Test1: simple inheritance + user_ids = users_obj.search(cr, uid, [('name', 'like', 'test')]) + self.assertEqual(set(user_ids), set([a, b1, b2]), 'searching through inheritance failed') + user_ids = users_obj.search(cr, uid, [('name', '=', 'test_B')]) + self.assertEqual(set(user_ids), set([b1]), 'searching through inheritance failed') + + # Test2: inheritance + relational fields + user_ids = users_obj.search(cr, uid, [('child_ids.name', 'like', 'test_B')]) + self.assertEqual(set(user_ids), set([b1]), 'searching through inheritance failed') + + def test_20_auto_join(self): registry, cr, uid = self.registry, self.cr, self.uid # Get models @@ -132,6 +155,9 @@ class test_expression(common.TransactionCase): # Do: one2many without _auto_join self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('bank_ids.name', 'like', name_test)]) + for query in self.query_list: + print query + print '----------------------' # Test result self.assertEqual(set(partner_ids), set([p_aa]), "_auto_join off: ('bank_ids.name', 'like', '..'): incorrect result") @@ -141,14 +167,14 @@ class test_expression(common.TransactionCase): sql_query = self.query_list[0].get_sql() self.assertIn('res_partner_bank', sql_query[0], "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect main table") - self.assertIn('res_partner_bank."name" like %s', sql_query[1], + self.assertIn('"res_partner_bank"."name" like %s', sql_query[1], "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect where condition") self.assertEqual(set(['%' + name_test + '%']), set(sql_query[2]), "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect parameter") sql_query = self.query_list[2].get_sql() self.assertIn('res_partner', sql_query[0], "_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect main table") - self.assertIn('res_partner."id" in (%s)', sql_query[1], + self.assertIn('"res_partner"."id" in (%s)', sql_query[1], "_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect where condition") self.assertEqual(set([p_aa]), set(sql_query[2]), "_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect parameter") @@ -156,6 +182,9 @@ class test_expression(common.TransactionCase): # Do: cascaded one2many without _auto_join self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('child_ids.bank_ids.id', 'in', [b_aa, b_ba])]) + for query in self.query_list: + print query + print '----------------------' # Test result self.assertEqual(set(partner_ids), set([p_a, p_b]), "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]): incorrect result") @@ -167,6 +196,8 @@ class test_expression(common.TransactionCase): partner_bank_ids_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('bank_ids.name', 'like', 'test_a')]) + # for query in self.query_list: + # print query # Test result self.assertEqual(set(partner_ids), set([p_aa]), "_auto_join on: ('bank_ids.name', 'like', '..') incorrect result") @@ -178,9 +209,9 @@ class test_expression(common.TransactionCase): "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect main table") self.assertIn('"res_partner_bank" as res_partner__bank_ids', sql_query[0], "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect join") - self.assertIn('res_partner__bank_ids."name" like %s', sql_query[1], + self.assertIn('"res_partner__bank_ids"."name" like %s', sql_query[1], "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect where condition") - self.assertIn('res_partner__bank_ids."partner_id"=res_partner."id"', sql_query[1], + self.assertIn('res_partner."id"=res_partner__bank_ids."partner_id"', sql_query[1], "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect join condition") self.assertEqual(set(['%' + name_test + '%']), set(sql_query[2]), "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect parameter") @@ -188,6 +219,9 @@ class test_expression(common.TransactionCase): # Do: one2many with _auto_join, test final leaf is an id self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('bank_ids.id', 'in', [b_aa, b_ab])]) + for query in self.query_list: + print query + print '----------------------' # Test result self.assertEqual(set(partner_ids), set([p_aa, p_ab]), "_auto_join on: ('bank_ids.id', 'in', [..]) incorrect result") @@ -199,9 +233,9 @@ class test_expression(common.TransactionCase): "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect main table") self.assertIn('"res_partner_bank" as res_partner__bank_ids', sql_query[0], "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect join") - self.assertIn('res_partner__bank_ids."id" in (%s,%s)', sql_query[1], + self.assertIn('"res_partner__bank_ids"."id" in (%s,%s)', sql_query[1], "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect where condition") - self.assertIn('res_partner__bank_ids."partner_id"=res_partner."id"', sql_query[1], + self.assertIn('res_partner."id"=res_partner__bank_ids."partner_id"', sql_query[1], "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect join condition") self.assertEqual(set([b_aa, b_ab]), set(sql_query[2]), "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect parameter") @@ -210,6 +244,9 @@ class test_expression(common.TransactionCase): partner_child_ids_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('child_ids.bank_ids.id', 'in', [b_aa, b_ba])]) + for query in self.query_list: + print query + print '----------------------' # Test result self.assertEqual(set(partner_ids), set([p_a, p_b]), "_auto_join on: ('child_ids.bank_ids.id', 'not in', [..]): incorrect result") @@ -223,11 +260,11 @@ class test_expression(common.TransactionCase): "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join") self.assertIn('"res_partner_bank" as res_partner__child_ids__bank_ids', sql_query[0], "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join") - self.assertIn('res_partner__child_ids__bank_ids."id" in (%s,%s)', sql_query[1], + self.assertIn('"res_partner__child_ids__bank_ids"."id" in (%s,%s)', sql_query[1], "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect where condition") - self.assertIn('res_partner__child_ids."parent_id"=res_partner."id"', sql_query[1], + self.assertIn('res_partner."id"=res_partner__child_ids."parent_id"', sql_query[1], "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join condition") - self.assertIn('res_partner__child_ids__bank_ids."partner_id"=res_partner__child_ids."id"', sql_query[1], + self.assertIn('res_partner__child_ids."id"=res_partner__child_ids__bank_ids."partner_id"', sql_query[1], "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join condition") self.assertEqual(set([b_aa, b_ba]), set(sql_query[2]), "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect parameter") @@ -241,6 +278,9 @@ class test_expression(common.TransactionCase): # Do: many2one without _auto_join self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) + for query in self.query_list: + print query + print '----------------------' # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), "_auto_join off: ('state_id.country_id.code', 'like', '..') incorrect result") @@ -252,6 +292,9 @@ class test_expression(common.TransactionCase): partner_state_id_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) + for query in self.query_list: + print query + print '----------------------' # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') incorrect result") @@ -261,7 +304,7 @@ class test_expression(common.TransactionCase): sql_query = self.query_list[0].get_sql() self.assertIn('"res_country"', sql_query[0], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect main table") - self.assertIn('res_country."code" like %s', sql_query[1], + self.assertIn('"res_country"."code" like %s', sql_query[1], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect where condition") self.assertEqual(['%' + name_test + '%'], sql_query[2], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect parameter") @@ -270,9 +313,9 @@ class test_expression(common.TransactionCase): "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect main table") self.assertIn('"res_country_state" as res_partner__state_id', sql_query[0], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect join") - self.assertIn('res_partner__state_id."country_id" in (%s)', sql_query[1], + self.assertIn('"res_partner__state_id"."country_id" in (%s)', sql_query[1], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect where condition") - self.assertIn('(res_partner__state_id."id"=res_partner."state_id"', sql_query[1], + self.assertIn('res_partner."state_id"=res_partner__state_id."id"', sql_query[1], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect join condition") # Do: many2one with 1 _auto_join on the second many2one @@ -280,6 +323,9 @@ class test_expression(common.TransactionCase): state_country_id_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) + for query in self.query_list: + print query + print '----------------------' # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') incorrect result") @@ -292,9 +338,9 @@ class test_expression(common.TransactionCase): "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect main table") self.assertIn('"res_country" as res_country_state__country_id', sql_query[0], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect join") - self.assertIn('res_country_state__country_id."code" like %s', sql_query[1], + self.assertIn('"res_country_state__country_id"."code" like %s', sql_query[1], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect where condition") - self.assertIn('res_country_state__country_id."id"=res_country_state."country_id"', sql_query[1], + self.assertIn('res_country_state."country_id"=res_country_state__country_id."id"', sql_query[1], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect join condition") self.assertEqual(['%' + name_test + '%'], sql_query[2], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect parameter") @@ -302,7 +348,7 @@ class test_expression(common.TransactionCase): sql_query = self.query_list[1].get_sql() self.assertIn('"res_partner"', sql_query[0], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect main table") - self.assertIn('res_partner."state_id" in', sql_query[1], + self.assertIn('"res_partner"."state_id" in', sql_query[1], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect where condition") # Do: many2one with 2 _auto_join @@ -310,6 +356,8 @@ class test_expression(common.TransactionCase): state_country_id_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) + for query in self.query_list: + print query # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), "_auto_join on: ('state_id.country_id.code', 'like', '..') incorrect result") @@ -323,11 +371,11 @@ class test_expression(common.TransactionCase): "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join") self.assertIn('"res_country" as res_partner__state_id__country_id', sql_query[0], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join") - self.assertIn('res_partner__state_id__country_id."code" like %s', sql_query[1], + self.assertIn('"res_partner__state_id__country_id"."code" like %s', sql_query[1], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect where condition") - self.assertIn('res_partner__state_id."id"=res_partner."state_id"', sql_query[1], + self.assertIn('res_partner."state_id"=res_partner__state_id."id"', sql_query[1], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join condition") - self.assertIn('res_partner__state_id__country_id."id"=res_partner__state_id."country_id"', sql_query[1], + self.assertIn('res_partner__state_id."country_id"=res_partner__state_id__country_id."id"', sql_query[1], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join condition") self.assertEqual(['%' + name_test + '%'], sql_query[2], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect parameter") @@ -345,6 +393,9 @@ class test_expression(common.TransactionCase): # Do: ('child_ids.state_id.country_id.code', 'like', '..') without _auto_join self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('child_ids.state_id.country_id.code', 'like', name_test)]) + for query in self.query_list: + print query + print '----------------------' # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b]).issubset(set(partner_ids)), "_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result") @@ -358,6 +409,9 @@ class test_expression(common.TransactionCase): state_country_id_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('child_ids.state_id.country_id.code', 'like', name_test)]) + for query in self.query_list: + print query + print '----------------------' # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b]).issubset(set(partner_ids)), "_auto_join on: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result") @@ -365,12 +419,12 @@ class test_expression(common.TransactionCase): self.assertEqual(len(self.query_list), 1, "_auto_join on: ('child_ids.state_id.country_id.code', 'like', '..') number of queries incorrect") - # Remove mocks and modifications - partner_bank_ids_col._auto_join = False - partner_child_ids_col._auto_join = False - partner_state_id_col._auto_join = False - state_country_id_col._auto_join = False - BaseModel._where_calc = self._base_model_where_calc + # # Remove mocks and modifications + # partner_bank_ids_col._auto_join = False + # partner_child_ids_col._auto_join = False + # partner_state_id_col._auto_join = False + # state_country_id_col._auto_join = False + # BaseModel._where_calc = self._base_model_where_calc if __name__ == '__main__': unittest2.main() diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index b13586f5311..2474e6665cd 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -136,10 +136,9 @@ start the server specifying the ``--unaccent`` flag. import logging import traceback -from openerp.tools import flatten, reverse_enumerate -import fields import openerp.modules from openerp.osv.orm import MAGIC_COLUMNS +import openerp.tools as tools #.apidoc title: Domain Expressions @@ -174,7 +173,12 @@ FALSE_DOMAIN = [FALSE_LEAF] _logger = logging.getLogger(__name__) -def normalize(domain): + +# -------------------------------------------------- +# Generic domain manipulation +# -------------------------------------------------- + +def normalize_domain(domain): """Returns a normalized version of ``domain_expr``, where all implicit '&' operators have been made explicit. One property of normalized domain expressions is that they can be easily combined together as if they were single domain components. @@ -190,13 +194,14 @@ def normalize(domain): result[0:0] = [AND_OPERATOR] # put an extra '&' in front expected = 1 result.append(token) - if isinstance(token, (list, tuple)): # domain term + if isinstance(token, (list, tuple)): # domain term expected -= 1 else: expected += op_arity.get(token, 0) - 1 assert expected == 0 return result + def combine(operator, unit, zero, domains): """Returns a new domain expression where all domain components from ``domains`` have been added together using the binary operator ``operator``. The given @@ -227,46 +232,16 @@ def combine(operator, unit, zero, domains): result = [operator] * (count - 1) + result return result + def AND(domains): """AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """ return combine(AND_OPERATOR, TRUE_DOMAIN, FALSE_DOMAIN, domains) + def OR(domains): """OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """ return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains) -def is_operator(element): - """Test whether an object is a valid domain operator. """ - return isinstance(element, basestring) and element in DOMAIN_OPERATORS - -# TODO change the share wizard to use this function. -def is_leaf(element, internal=False): - """ Test whether an object is a valid domain term. - - :param internal: allow or not the 'inselect' internal operator in the term. - This normally should be always left to False. - """ - INTERNAL_OPS = TERM_OPERATORS + ('inselect',) - return (isinstance(element, tuple) or isinstance(element, list)) \ - and len(element) == 3 \ - and (((not internal) and element[1] in TERM_OPERATORS + ('<>',)) \ - or (internal and element[1] in INTERNAL_OPS + ('<>',))) - -def normalize_leaf(left, operator, right): - """ Change a term's operator to some canonical form, simplifying later - processing. - """ - original = operator - operator = operator.lower() - if operator == '<>': - operator = '!=' - if isinstance(right, bool) and operator in ('in', 'not in'): - _logger.warning("The domain term '%s' should use the '=' or '!=' operator." % ((left, original, right),)) - operator = '=' if operator == 'in' else '!=' - if isinstance(right, (list, tuple)) and operator in ('=', '!='): - _logger.warning("The domain term '%s' should use the 'in' or 'not in' operator." % ((left, original, right),)) - operator = 'in' if operator == '=' else 'not in' - return left, operator, right def distribute_not(domain): """ Distribute any '!' domain operators found inside a normalized domain. @@ -308,6 +283,7 @@ def distribute_not(domain): operator = mapping[operator] return [(left, operator, right)] return [NOT_OPERATOR, (left, operator, right)] + def distribute_negate(domain): """Negate the domain ``subtree`` rooted at domain[0], leaving the rest of the domain intact, and return @@ -331,34 +307,268 @@ def distribute_not(domain): done, todo = distribute_negate(domain[1:]) return done + distribute_not(todo) + +# -------------------------------------------------- +# Generic leaf manipulation +# -------------------------------------------------- + +def normalize_leaf(element): + """ Change a term's operator to some canonical form, simplifying later + processing. """ + if not is_leaf(element): + return element + left, operator, right = element + original = operator + operator = operator.lower() + if operator == '<>': + operator = '!=' + if isinstance(right, bool) and operator in ('in', 'not in'): + _logger.warning("The domain term '%s' should use the '=' or '!=' operator." % ((left, original, right),)) + operator = '=' if operator == 'in' else '!=' + if isinstance(right, (list, tuple)) and operator in ('=', '!='): + _logger.warning("The domain term '%s' should use the 'in' or 'not in' operator." % ((left, original, right),)) + operator = 'in' if operator == '=' else 'not in' + return (left, operator, right) + + +def is_operator(element): + """Test whether an object is a valid domain operator. """ + return isinstance(element, basestring) and element in DOMAIN_OPERATORS + + +# TODO change the share wizard to use this function. +def is_leaf(element, internal=False): + """ Test whether an object is a valid domain term: + - is a list or tuple + - with 3 elements + - second element if a valid op + + :param tuple element: a leaf in form (left, operator, right) + :param boolean internal: allow or not the 'inselect' internal operator + in the term. This should be always left to False. + """ + INTERNAL_OPS = TERM_OPERATORS + ('<>',) + if internal: + INTERNAL_OPS += ('inselect',) + return (isinstance(element, tuple) or isinstance(element, list)) \ + and len(element) == 3 \ + and element[1] in INTERNAL_OPS + + +# -------------------------------------------------- +# SQL utils +# -------------------------------------------------- + def select_from_where(cr, select_field, from_table, where_field, where_ids, where_operator): # todo: merge into parent query as sub-query res = [] if where_ids: - if where_operator in ['<','>','>=','<=']: + if where_operator in ['<', '>', '>=', '<=']: cr.execute('SELECT "%s" FROM "%s" WHERE "%s" %s %%s' % \ (select_field, from_table, where_field, where_operator), - (where_ids[0],)) # TODO shouldn't this be min/max(where_ids) ? + (where_ids[0],)) # TODO shouldn't this be min/max(where_ids) ? res = [r[0] for r in cr.fetchall()] - else: # TODO where_operator is supposed to be 'in'? It is called with child_of... + else: # TODO where_operator is supposed to be 'in'? It is called with child_of... for i in range(0, len(where_ids), cr.IN_MAX): - subids = where_ids[i:i+cr.IN_MAX] + subids = where_ids[i:i + cr.IN_MAX] cr.execute('SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % \ (select_field, from_table, where_field), (tuple(subids),)) res.extend([r[0] for r in cr.fetchall()]) return res + def select_distinct_from_where_not_null(cr, select_field, from_table): - cr.execute('SELECT distinct("%s") FROM "%s" where "%s" is not null' % \ - (select_field, from_table, select_field)) + cr.execute('SELECT distinct("%s") FROM "%s" where "%s" is not null' % (select_field, from_table, select_field)) return [r[0] for r in cr.fetchall()] + +# -------------------------------------------------- +# ExtendedLeaf class for managing leafs and contexts +# ------------------------------------------------- + +class ExtendedLeaf(object): + + def __init__(self, leaf, table, context_stack=None): + """ Initialize the ExtendedLeaf + + :attr [string, tuple] leaf: operator or tuple-formatted domain + expression + :attr object table: table object + :attr list _tables: list of chained table objects, updated when + adding joins + :attr tuple elements: manipulation-friendly leaf + :attr object field: field obj, taken from table, not necessarily + found (inherits, 'id') + :attr list field_path: exploded left of elements + (partner_id.name -> ['partner_id', 'name']) + :attr object relational_table: distant table for relational fields + """ + assert table, 'Invalid leaf creation without table' + self.context_stack = context_stack or [] + # validate the leaf + self.leaf = leaf + # normalize the leaf's operator + self.normalize_leaf() + # set working variables; handle the context stack and previous tables + self.table = table + self._tables = [] + for item in self.context_stack: + self._tables.append(item[0]) + self._tables.append(table) + # if leaf.is_operator: + # self.elements = self.leaf, None, None + # elif leaf.is_true_leaf() or leaf.is_false_leaf(): + # # because we consider left as a string + # self.elements = ('%s' % leaf.leaf[0], leaf.leaf[1], leaf.leaf[2]) + # else: + # self.elements = leaf.leaf + # self.field_path = self.elements[0].split('.', 1) + # self.field = self.table._columns.get(self.field_path[0]) + # if self.field and self.field._obj: + # relational_table = working_table.pool.get(field._obj) + # else: + # relational_table = None + # check validity + self.check_leaf() + + def __str__(self): + return '' % (str(self.leaf), self.table._table, ','.join(self._get_context_debug())) + + def create_substitution_leaf(self, new_leaf, new_table=None): + if new_table is None: + new_table = self.table + return ExtendedLeaf(new_leaf, new_table, self.context_stack) + + def create_sibling_leaf(self, new_leaf): + pass + + # -------------------------------------------------- + # Join / Context manipulation + # running examples: + # - res_users.name, like, foo: name is on res_partner, not on res_users + # - res_partner.bank_ids.name, like, foo: bank_ids is a one2many with _auto_join + # - res_partner.state_id.name, like, foo: state_id is a many2one with _auto_join + # A join: + # - link between src_table and dst_table, using src_field and dst_field + # i.e.: inherits: res_users.partner_id = res_partner.id + # i.e.: one2many: res_partner.id = res_partner_bank.partner_id + # i.e.: many2one: res_partner.state_id = res_country_state.id + # - done in the context of a field + # i.e.: inherits: 'partner_id' + # i.e.: one2many: 'bank_ids' + # i.e.: many2one: 'state_id' + # - table names use aliases: initial table followed by the context field + # names, joined using a '__' + # i.e.: inherits: res_partner as res_users__partner_id + # i.e.: one2many: res_partner_bank as res_partner__bank_ids + # i.e.: many2one: res_country_state as res_partner__state_id + # - join condition use aliases + # i.e.: inherits: res_users.partner_id = res_users__partner_id.id + # i.e.: one2many: res_partner.id = res_partner__bank_ids.partner_id + # i.e.: many2one: res_partner.state_id = res_partner__state_id.id + # Variables explanation: + # - src_table: working table before the join + # -> res_users, res_partner, res_partner + # - dst_table: working table after the join + # -> res_partner, res_partner_bank, res_country_state + # - src_table_link_name: field name used to link the src table, not + # necessarily a field (because 'id' is not a field instance) + # i.e.: inherits: 'partner_id', found in the inherits of the current table + # i.e.: one2many: 'id', not a field + # i.e.: many2one: 'state_id', the current field name + # - dst_table_link_name: field name used to link the dst table, not + # necessarily a field (because 'id' is not a field instance) + # i.e.: inherits: 'id', not a field + # i.e.: one2many: 'partner_id', _fields_id of the current field + # i.e.: many2one: 'id', not a field + # - context_field_name: field name used as a context to make the alias + # i.e.: inherits: 'partner_id': found in the inherits of the current table + # i.e.: one2many: 'bank_ids': current field name + # i.e.: many2one: 'state_id': current field name + # -------------------------------------------------- + + def _generate_alias(self): + alias = self._tables[0]._table + for context in self.context_stack: + alias += '__' + context[4] + return alias + + def add_join_context(self, context_field_name, src_table_link_name, dst_table, dst_table_link_name): + """ See above comments for more details. A join context is a tuple structure + holding the following elements: (src_table (self.table), src_table_link_name, + dst_table, dst_table_link_name, context_field_name) + After adding the join, the table of the current leaf is updated. + """ + self.context_stack.append((self.table, src_table_link_name, dst_table, dst_table_link_name, context_field_name)) + self._tables.append(dst_table) + self.table = dst_table + + def get_join_conditions(self): + names = [] + alias = self._tables[0]._table + for context in self.context_stack: + previous_alias = alias + alias += '__' + context[4] + names.append('%s."%s"=%s."%s"' % (previous_alias, context[1], alias, context[3])) + return names + + def get_tables(self): + tables = set() + alias = self._tables[0]._table + for context in self.context_stack: + alias += '__' + context[4] + table_full_alias = '"%s" as %s' % (context[2]._table, alias) + tables.add(table_full_alias) + return tables + + def _get_context_debug(self): + names = ['%s."%s"=%s."%s" (%s)' % (table[0]._table, table[1], table[2]._table, table[3], table[4]) for table in self.context_stack] + return names + + # -------------------------------------------------- + # Leaf manipulation + # -------------------------------------------------- + + def check_leaf(self): + """ Leaf validity rules: + - a valid leaf is an operator or a leaf + - a valid leaf has a field objects unless + - it is not a tuple + - it is an inherited field + - left is id, operator is 'child_of' + - left is in MAGIC_COLUMNS + """ + if not is_operator(self.leaf) and not is_leaf(self.leaf, True): + raise ValueError("Invalid leaf %s" % str(self.leaf)) + + # if self.is_leaf() and not (self.is_true_leaf() or self.is_false_leaf()) \ + # and not self.field and not self.field_path[0] in self.table._inherit_fields \ + # and not (self.leaf[0] == 'id' and self.leaf[1] == 'child_of') \ + # and not (self.field_path[0] in MAGIC_COLUMNS): + # raise ValueError("Invalid field %r in leaf %r" % (self.leaf[0], self.leaf)) + + def is_operator(self): + return is_operator(self.leaf) + + def is_true_leaf(self): + return self.leaf == TRUE_LEAF + + def is_false_leaf(self): + return self.leaf == FALSE_LEAF + + def is_leaf(self, internal=False): + return is_leaf(self.leaf, internal=internal) + + def normalize_leaf(self): + self.leaf = normalize_leaf(self.leaf) + return True + + class expression(object): - """ - parse a domain expression - use a real polish notation - leafs are still in a ('foo', '=', 'bar') format - For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html + """ Parse a domain expression + Use a real polish notation + Leafs are still in a ('foo', '=', 'bar') format + For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html """ def __init__(self, cr, uid, exp, table, context): @@ -368,329 +578,301 @@ class expression(object): :param exp: expression (using domain ('foo', '=', 'bar' format)) :param table: root table object - :attr dict leaf_to_table: used to store the table to use for the - sql generation, according to the domain leaf. - structure: { [leaf index]: table object } - :attr set table_aliases: set of aliases. Previously this attribute - was a set of table objects; now that joins generation is included - into the expression parsing, it holds aliases, and a mapping - exist linking aliases to tables. - :attr dict table_aliases_mapping: mapping alias -> table object - :attr list joins: list of join conditions, such as (res_country_state."id" = res_partner."state_id") - :attr root_table: root table, set by parse() + :attr list result: list that will hold the result of the parsing + as a list of ExtendedLeaf + :attr list joins: list of join conditions, such as + (res_country_state."id" = res_partner."state_id") + :attr root_table: base table for the query + :attr list expression: the domain expression, that will be normalized + and prepared """ self.has_unaccent = openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent - self.leaf_to_table = {} - self.table_aliases = set() - self.table_aliases_mapping = {} + self.result = [] self.joins = [] - self.root_table = None - # assign self.exp with the normalized, parsed domain. - self.parse(cr, uid, distribute_not(normalize(exp)), table, context) + self.root_table = table - # TDE note: this seems not to be used anymore, commenting - # # TODO used only for osv_memory - # @property - # def exp(self): - # return self.exp[:] + # normalize and prepare the expression for parsing + self.expression = distribute_not(normalize_domain(exp)) - def _has_table_alias(self, alias): - return alias in self.table_aliases + # parse the domain expression + self.parse(cr, uid, context=context) - def _get_table_from_alias(self, alias): - return self.table_aliases_mapping.get(alias) + # ---------------------------------------- + # Tools for domain manipulation + # ---------------------------------------- - def _get_full_alias(self, alias): - if not self._get_table_from_alias(alias): - return False - table_name = self._get_table_from_alias(alias)._table - if table_name == alias: - return '"%s"' % alias + def to_ids(self, cr, uid, value, relational_table, context=None, limit=None): + """ Normalize a single id or name, or a list of those, into a list of ids + :param {int,long,basestring,list,tuple} value: + if int, long -> return [value] + if basestring, convert it into a list of basestrings, then + if list of basestring -> + perform a name_search on relational_table for each name + return the list of related ids + """ + names = [] + if isinstance(value, basestring): + names = [value] + elif value and isinstance(value, (tuple, list)) and all(isinstance(item, basestring) for item in value): + names = value + elif isinstance(value, (int, long)): + return [value] + if names: + name_get_list = [name_get[0] for name in names for name_get in relational_table.name_search(cr, uid, name, [], 'ilike', context=context, limit=limit)] + return list(set(name_get_list)) + return list(value) + + def child_of_domain(self, cr, uid, left, ids, left_model, parent=None, prefix='', context=None): + """ Return a domain implementing the child_of operator for [(left,child_of,ids)], + either as a range using the parent_left/right tree lookup fields + (when available), or as an expanded [(left,in,child_ids)] """ + if left_model._parent_store and (not left_model.pool._init): + # TODO: Improve where joins are implemented for many with '.', replace by: + # doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)] + doms = [] + for o in left_model.browse(cr, uid, ids, context=context): + if doms: + doms.insert(0, OR_OPERATOR) + doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)] + if prefix: + return [(left, 'in', left_model.search(cr, uid, doms, context=context))] + return doms else: - return '"%s" as %s' % (table_name, alias) + def recursive_children(ids, model, parent_field): + if not ids: + return [] + ids2 = model.search(cr, uid, [(parent_field, 'in', ids)], context=context) + return ids + recursive_children(ids2, model, parent_field) + return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))] - def _add_table_alias(self, alias, table): - if not self._has_table_alias(alias): - self.table_aliases.add(alias) - self.table_aliases_mapping[alias] = table - else: - raise ValueError("Already existing alias %s for table %s, trying to set it for table %s" % (alias, self._get_table_from_alias(alias)._table, table._table)) + # ---------------------------------------- + # Internal structure + # ---------------------------------------- + + def _format_table_name(self, table_name): + return '"%s"' % (table_name) def get_tables(self): """ Returns the list of tables for SQL queries, like select from ... """ - return [self._get_full_alias(item) for item in self.table_aliases] + tables = [] + for leaf in self.result: + for table in leaf.get_tables(): + if table not in tables: + tables.append(table) + table_name = '"%s"' % self.root_table._table + if table_name not in tables: + tables.append(table_name) + return tables - def parse(self, cr, uid, exp, table, context): + # ---------------------------------------- + # Parsing + # ---------------------------------------- + + def parse(self, cr, uid, context): """ Transform the leaves of the expression - For each element in the expression - 1. validity check: operator or True / False or a valid leaf - 2. TDE FIXME: TO COMPLETE + The principle is to pop elements from the left of a leaf stack. Each + leaf is processed. The processing is a if/elif list of various cases + that appear in the leafs (many2one, function fields, ...). Two results + can appear at the end of a leaf processing: + - the leaf is modified or new leafs introduced in the domain: they + are added at the left of the stack, to be processed next + - the leaf is added to the result - Some var explanation for those who have 2 bytes of brain cache memory like me and that cannot remember 32^16 similar variable names. - :var obj working_table: table object, table containing the field (the name provided in the left operand) + Some var explanation: + :var obj working_table: table object, table containing the field + (the name provided in the left operand) :var list field_path: left operand seen as a path (foo.bar -> [foo, bar]) - :var string field_table: working_table._table :var obj relational_table: relational table of a field (field._obj) ex: res_partner.bank_ids -> res_partner_bank - :var obj field_obj: deleted var, now renamed to relational_table - - :param exp: expression (domain) - :param table: table object """ - self.exp = exp - self.root_table = table - self._add_table_alias(table._table, table) + result = [] + stack = [ExtendedLeaf(leaf, self.root_table) for leaf in self.expression] - def child_of_domain(left, ids, left_model, parent=None, prefix=''): - """Returns a domain implementing the child_of operator for [(left,child_of,ids)], - either as a range using the parent_left/right tree lookup fields (when available), - or as an expanded [(left,in,child_ids)]""" - if left_model._parent_store and (not left_model.pool._init): - # TODO: Improve where joins are implemented for many with '.', replace by: - # doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)] - doms = [] - for o in left_model.browse(cr, uid, ids, context=context): - if doms: - doms.insert(0, OR_OPERATOR) - doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)] - if prefix: - return [(left, 'in', left_model.search(cr, uid, doms, context=context))] - return doms + while stack: + # Get the next leaf to process + leaf = stack.pop(0) + leafs_to_stack = [] + results_to_stack = [] + + # Get working variables + working_table = leaf.table + if leaf.is_operator(): + left, operator, right = leaf.leaf, None, None + elif leaf.is_true_leaf() or leaf.is_false_leaf(): + # because we consider left as a string + left, operator, right = ('%s' % leaf.leaf[0], leaf.leaf[1], leaf.leaf[2]) else: - def recursive_children(ids, model, parent_field): - if not ids: - return [] - ids2 = model.search(cr, uid, [(parent_field, 'in', ids)], context=context) - return ids + recursive_children(ids2, model, parent_field) - return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))] - - def to_ids(value, relational_table): - """Normalize a single id or name, or a list of those, into a list of ids""" - names = [] - if isinstance(value, basestring): - names = [value] - if value and isinstance(value, (tuple, list)) and isinstance(value[0], basestring): - names = value - if names: - return flatten([[x[0] for x in relational_table.name_search(cr, uid, n, [], 'ilike', context=context, limit=None)] \ - for n in names]) - elif isinstance(value, (int, long)): - return [value] - return list(value) - - i = -1 - while i + 1 < len(self.exp): - i += 1 - - # 0 Preparation - # - Check validity of current element of expression (operator OR True/False leaf OR leaf) - # - Normalize the leaf's operator - # - Set working variables - - # check validity - e = self.exp[i] - if is_operator(e) or e == TRUE_LEAF or e == FALSE_LEAF: - continue - if not is_leaf(e): - raise ValueError("Invalid term %r in domain expression %r" % (e, exp)) - - # normalize the leaf's operator - e = normalize_leaf(*e) - self.exp[i] = e - left, operator, right = e - - # working variables - working_table = table # The table containing the field (the name provided in the left operand) + left, operator, right = leaf.leaf + # field_path = leaf.field_path + # field = leaf.field field_path = left.split('.', 1) - - # 1 Extract field - # - Try to directly extract the field - # - Handle inherits fields: replace by a join, find the final new - # working table and extract the field - field = working_table._columns.get(field_path[0]) - if field_path[0] in table._inherit_fields: - while not field: - next_table = working_table.pool.get(working_table._inherit_fields[field_path[0]][0]) - if not self._has_table_alias(next_table._table): - self.joins.append('%s."%s"=%s."%s"' % (next_table._table, 'id', working_table._table, working_table._inherits[next_table._name])) - self._add_table_alias(next_table._table, next_table) - working_table = next_table - field = working_table._columns.get(field_path[0]) - self.leaf_to_table[i] = working_table - # 2 Field not found - # - ('id', 'child_of', ids): replace the leaf by a computed domain - # after searching and continue the processing OR - # - field in magic columns (ex: id): continue the processing OR - # - raise an error + if field and field._obj: + relational_table = working_table.pool.get(field._obj) + else: + relational_table = None - if not field: - if left == 'id' and operator == 'child_of': - ids2 = to_ids(right, table) - dom = child_of_domain(left, ids2, working_table) - self.exp = self.exp[:i] + dom + self.exp[i + 1:] - else: - # field could not be found in model columns, it's probably invalid, unless - # it's one of the _log_access special fields - # TODO: make these fields explicitly available in self.columns instead! - if field_path[0] not in MAGIC_COLUMNS: - raise ValueError("Invalid field %r in domain expression %r" % (left, exp)) - continue + # ---------------------------------------- + # SIMPLE CASE + # 1. leaf is an operator + # 2. leaf is a true/false leaf + # -> add directly to result + # ---------------------------------------- - # 3 Field found - # - update working variables - # - get relational table that exists for relational fields - # - prepare the alias for tables, that can be the table name if only one-level - # - if domain is a path (ex: ('partner_id.name', '=', 'foo')): - # TDE TO ADD: once fully implemented, explain auto_join - # replace all the expression by a normalized equivalent domain - # - find the related ids: partner_id.name='foo' -> res_partner.search(('name', '=', 'foo'))) - # - many2one: leaf becomes directly ('partner_id', 'in', 'partner_ids') - # - one2many: - # - search on current table where partner_id is in partner_ids - # - leaf becomes ('id', 'in', ids) - # - get out of current leaf is field is not a property field - # - if domain is not a path: handle some leaf replacement / tweaking - # - handle function fields - # - handle one2many, many2many and many2one fields - # - other fields: handle datetime and translatable fields + if leaf.is_operator(): + results_to_stack.append(leaf) + elif leaf.is_true_leaf() or leaf.is_false_leaf(): + results_to_stack.append(leaf) - relational_table = table.pool.get(field._obj) - alias = working_table._table + # ---------------------------------------- + # FIELD NOT FOUND + # -> from inherits'd fields -> work on the related table, and add + # a join condition + # -> ('id', 'child_of', '..') -> use a 'to_ids' + # -> but is one on the _log_access special fields, add directly to + # result + # TODO: make these fields explicitly available in self.columns instead! + # -> else: crash + # ---------------------------------------- - while len(field_path) > 1 and field._auto_join: - assert field._type in ['many2one', 'one2many'], \ + elif not field and field_path[0] in working_table._inherit_fields: + # comments about inherits'd fields + # { 'field_name': ('parent_model', 'm2o_field_to_reach_parent', + # field_column_obj, origina_parent_model), ... } + next_table = working_table.pool.get(working_table._inherit_fields[field_path[0]][0]) + leaf.add_join_context(working_table._inherits[next_table._name], working_table._inherits[next_table._name], next_table, 'id') + leafs_to_stack.append(leaf) + + elif not field and left == 'id' and operator == 'child_of': + ids2 = self.to_ids(cr, uid, right, working_table, context) + dom = self.child_of_domain(cr, uid, left, ids2, working_table) + leafs_to_stack += [leaf.create_substitution_leaf(dom_leaf, working_table) for dom_leaf in dom] + + elif not field and field_path[0] in MAGIC_COLUMNS: + results_to_stack.append(leaf) + + elif not field: + raise ValueError("Invalid field %r in leaf %r" % (left, leaf)) + + # ---------------------------------------- + # PATH SPOTTED + # -> XX + # -> note: hack about fields.property should not be necessary anymore + # as after transforming the field, it will go through this loop once again + # ---------------------------------------- + + elif len(field_path) > 1 and field._type == 'many2one' and field._auto_join: + # res_partner.parent_id = res_partner.id + leaf.add_join_context(field_path[0], field_path[0], relational_table, 'id') + leafs_to_stack.append(leaf.create_substitution_leaf((field_path[1], operator, right), relational_table)) + + elif len(field_path) > 1 and field._type == 'one2many' and field._auto_join: + # res_partner.id = res_partner.parent_id + leaf.add_join_context(field_path[0], 'id', relational_table, field._fields_id) + leafs_to_stack.append(leaf.create_substitution_leaf((field_path[1], operator, right), relational_table)) + + elif len(field_path) > 1 and field._auto_join: + assert False, \ '_auto_join attribute on something else than a many2one or one2many is currently not supported' - previous_alias = alias - alias = alias + '__' + field_path[0] - if not self._get_table_from_alias(alias): - self._add_table_alias(alias, relational_table) - if field._type == 'many2one': - self.joins.append('%s."%s"=%s."%s"' % (alias, 'id', previous_alias, field_path[0])) - elif field._type == 'one2many': - self.joins.append('%s."%s"=%s."%s"' % (alias, field._fields_id, previous_alias, 'id')) - self.exp[i] = (alias + '.' + field_path[1], self.exp[i][1], self.exp[i][2]) + elif len(field_path) > 1 and field._type == 'many2one': + right_ids = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) + leaf.leaf = (field_path[0], 'in', right_ids) + leafs_to_stack.append(leaf) - # udpate working variables - field_path = field_path[1].split('.', 1) - working_table = relational_table - field = working_table._columns.get(field_path[0]) + # Making search easier when there is a left operand as field.o2m or field.m2m + elif len(field_path) > 1 and field._type in ['many2many', 'one2many']: + right_ids = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) + table_ids = working_table.search(cr, uid, [(field_path[0], 'in', right_ids)], context=dict(context, active_test=False)) + leaf.leaf = ('id', 'in', table_ids) + leafs_to_stack.append(leaf) - if not field: - if left == 'id' and operator == 'child_of': - ids2 = to_ids(right, table) - dom = child_of_domain(left, ids2, working_table) - self.exp = self.exp[:i] + dom + self.exp[i + 1:] - else: - # field could not be found in model columns, it's probably invalid, unless - # it's one of the _log_access special fields - # TODO: make these fields explicitly available in self.columns instead! - if field_path[0] not in MAGIC_COLUMNS: - raise ValueError("Invalid field %r in domain expression %r" % (left, exp)) - break + # ---------------------------------------- + # FUNCTION FIELD + # -> not stored, get the result of fnct_search + # ---------------------------------------- - # moved on top - relational_table = table.pool.get(field._obj) - - if not field: - continue - - if len(field_path) > 1: - if field._type == 'many2one': - right = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) - self.exp[i] = (alias + '.' + field_path[0], 'in', right) - # Making search easier when there is a left operand as field.o2m or field.m2m - if field._type in ['many2many', 'one2many']: - right = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) - right1 = table.search(cr, uid, [(field_path[0], 'in', right)], context=dict(context, active_test=False)) - self.exp[i] = (alias + '.id', 'in', right1) - - if not isinstance(field, fields.property): - continue - - if field._properties and not field.store: + elif field._properties and not field.store and not field._fnct_search: # this is a function field that is not stored - if not field._fnct_search: - # the function field doesn't provide a search function and doesn't store - # values in the database, so we must ignore it : we generate a dummy leaf - self.exp[i] = TRUE_LEAF - _logger.error( - "The field '%s' (%s) can not be searched: non-stored " - "function field without fnct_search", - field.string, left) - # avoid compiling stack trace if not needed - if _logger.isEnabledFor(logging.DEBUG): - _logger.debug(''.join(traceback.format_stack())) + # the function field doesn't provide a search function and doesn't store + # values in the database, so we must ignore it : we generate a dummy leaf + leaf.leaf = TRUE_LEAF + _logger.error( + "The field '%s' (%s) can not be searched: non-stored " + "function field without fnct_search", + field.string, left) + # avoid compiling stack trace if not needed + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug(''.join(traceback.format_stack())) + leafs_to_stack.append(leaf) + + elif field._properties and not field.store: + # this is a function field that is not stored + fct_domain = field.search(cr, uid, working_table, left, [leaf.leaf], context=context) + if not fct_domain: + leaf.leaf = TRUE_LEAF + leafs_to_stack.append(leaf) else: - assert working_table == table, \ - 'function fields can not be contained in paths using _auto_join' - subexp = field.search(cr, uid, table, left, [self.exp[i]], context=context) - if not subexp: - self.exp[i] = TRUE_LEAF - else: - # we assume that the expression is valid - # we create a dummy leaf for forcing the parsing of the resulting expression - self.exp[i] = AND_OPERATOR - self.exp.insert(i + 1, TRUE_LEAF) - for j, se in enumerate(subexp): - self.exp.insert(i + 2 + j, se) - # else, the value of the field is store in the database, so we search on it + # we assume that the expression is valid + # we create a dummy leaf for forcing the parsing of the resulting expression + leafs_to_stack.append(leaf.create_substitution_leaf(AND_OPERATOR, working_table)) + leafs_to_stack.append(leaf.create_substitution_leaf(TRUE_LEAF, working_table)) + for domain_element in fct_domain: + leafs_to_stack.append(leaf.create_substitution_leaf(domain_element, working_table)) + + # Applying recursivity on field(one2many) + elif field._type == 'one2many' and operator == 'child_of': + ids2 = self.to_ids(cr, uid, right, relational_table, context) + if field._obj != working_table._name: + dom = self.child_of_domain(cr, uid, left, ids2, relational_table, prefix=field._obj) + else: + dom = self.child_of_domain(cr, uid, 'id', ids2, working_table, parent=left) + leafs_to_stack += [leaf.create_substitution_leaf(dom_leaf, working_table) for dom_leaf in dom] elif field._type == 'one2many': - # Applying recursivity on field(one2many) - if operator == 'child_of': - ids2 = to_ids(right, relational_table) - if field._obj != working_table._name: - dom = child_of_domain(left, ids2, relational_table, prefix=field._obj) + call_null = True + + if right is not False: + if isinstance(right, basestring): + ids2 = [x[0] for x in relational_table.name_search(cr, uid, right, [], operator, context=context, limit=None)] + if ids2: + operator = 'in' else: - dom = child_of_domain('id', ids2, working_table, parent=left) - self.exp = self.exp[:i] + dom + self.exp[i + 1:] - - else: - call_null = True - - if right is not False: - if isinstance(right, basestring): - ids2 = [x[0] for x in relational_table.name_search(cr, uid, right, [], operator, context=context, limit=None)] - if ids2: - operator = 'in' + if not isinstance(right, list): + ids2 = [right] else: - if not isinstance(right, list): - ids2 = [right] - else: - ids2 = right - if not ids2: - if operator in ['like', 'ilike', 'in', '=']: - #no result found with given search criteria - call_null = False - self.exp[i] = FALSE_LEAF - else: - ids2 = select_from_where(cr, field._fields_id, relational_table._table, 'id', ids2, operator) - if ids2: - call_null = False - o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - self.exp[i] = ('id', o2m_op, ids2) + ids2 = right + if not ids2: + if operator in ['like', 'ilike', 'in', '=']: + #no result found with given search criteria + call_null = False + leafs_to_stack.append(leaf.create_substitution_leaf(FALSE_LEAF, working_table)) + else: + ids2 = select_from_where(cr, field._fields_id, relational_table._table, 'id', ids2, operator) + if ids2: + call_null = False + o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' + leafs_to_stack.append(leaf.create_substitution_leaf(('id', o2m_op, ids2), working_table)) - if call_null: - o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - self.exp[i] = ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)) + if call_null: + o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' + leafs_to_stack.append(leaf.create_substitution_leaf(('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)), working_table)) elif field._type == 'many2many': rel_table, rel_id1, rel_id2 = field._sql_names(working_table) #FIXME if operator == 'child_of': def _rec_convert(ids): - if relational_table == table: + if relational_table == working_table: return ids return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator) - ids2 = to_ids(right, relational_table) - dom = child_of_domain('id', ids2, relational_table) + ids2 = self.to_ids(cr, uid, right, relational_table, context) + dom = self.child_of_domain(cr, uid, 'id', ids2, relational_table) ids2 = relational_table.search(cr, uid, dom, context=context) - self.exp[i] = ('id', 'in', _rec_convert(ids2)) + leafs_to_stack.append(leaf.create_substitution_leaf(('id', 'in', _rec_convert(ids2)), working_table)) else: call_null_m2m = True if right is not False: @@ -707,26 +889,26 @@ class expression(object): if operator in ['like', 'ilike', 'in', '=']: #no result found with given search criteria call_null_m2m = False - self.exp[i] = FALSE_LEAF + leafs_to_stack.append(leaf.create_substitution_leaf(FALSE_LEAF, working_table)) else: operator = 'in' # operator changed because ids are directly related to main object else: call_null_m2m = False m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - self.exp[i] = ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]) + leafs_to_stack.append(leaf.create_substitution_leaf(('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), working_table)) if call_null_m2m: m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - self.exp[i] = ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)) + leafs_to_stack.append(leaf.create_substitution_leaf(('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_table)) elif field._type == 'many2one': if operator == 'child_of': - ids2 = to_ids(right, relational_table) + ids2 = self.to_ids(cr, uid, right, relational_table, context) if field._obj != working_table._name: - dom = child_of_domain(left, ids2, relational_table, prefix=field._obj) + dom = self.child_of_domain(cr, uid, left, ids2, relational_table, prefix=field._obj) else: - dom = child_of_domain('id', ids2, working_table, parent=left) - self.exp = self.exp[:i] + dom + self.exp[i + 1:] + dom = self.child_of_domain(cr, uid, 'id', ids2, working_table, parent=left) + leafs_to_stack += [leaf.create_substitution_leaf(dom_leaf, working_table) for dom_leaf in dom] else: def _get_expression(relational_table, cr, uid, left, right, operator, context=None): if context is None: @@ -750,26 +932,24 @@ class expression(object): # resolve string-based m2o criterion into IDs if isinstance(right, basestring) or \ right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right): - self.exp[i] = _get_expression(relational_table, cr, uid, left, right, operator, context=context) + leafs_to_stack.append(leaf.create_substitution_leaf(_get_expression(relational_table, cr, uid, left, right, operator, context=context), working_table)) else: # right == [] or right == False and all other cases are handled by __leaf_to_sql() - pass + results_to_stack.append(leaf) else: # other field type # add the time part to datetime field when it's not there: - if field._type == 'datetime' and self.exp[i][2] and len(self.exp[i][2]) == 10: - - self.exp[i] = list(self.exp[i]) + if field._type == 'datetime' and right and len(right) == 10: if operator in ('>', '>='): - self.exp[i][2] += ' 00:00:00' + right += ' 00:00:00' elif operator in ('<', '<='): - self.exp[i][2] += ' 23:59:59' + right += ' 23:59:59' - self.exp[i] = tuple(self.exp[i]) + leafs_to_stack.append(leaf.create_substitution_leaf((left, operator, right), working_table)) - if field.translate: + elif field.translate: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator) if need_wildcard: @@ -802,30 +982,49 @@ class expression(object): right, right, ] + leafs_to_stack.append(leaf.create_substitution_leaf(('id', 'inselect', (subselect, params)), working_table)) - self.exp[i] = ('id', 'inselect', (subselect, params)) + else: + results_to_stack.append(leaf) - def __leaf_to_sql(self, leaf, table): + # ---------------------------------------- + # END OF PROCESS OF CURRENT LEAF + # -> results_to_stack elements are added in result + # -> leafs_to_stack elements are inserted back in the processed + # stack to be immediately processed + # ---------------------------------------- + + leafs_to_stack.reverse() + for leaf in results_to_stack: + result.append(leaf) + for leaf in leafs_to_stack: + stack.insert(0, leaf) + + # ---------------------------------------- + # END OF PARSING FULL DOMAIN + # ---------------------------------------- + + self.result = result + + # Generate joins + joins = set() + for leaf in self.result: + joins |= set(leaf.get_join_conditions()) + self.joins = list(joins) + + def __leaf_to_sql(self, eleaf): + table = eleaf.table + leaf = eleaf.leaf left, operator, right = leaf # final sanity checks - should never fail assert operator in (TERM_OPERATORS + ('inselect',)), \ "Invalid operator %r in domain term %r" % (operator, leaf) assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in table._all_columns \ - or left in MAGIC_COLUMNS \ - or ('.' in left and self._has_table_alias(left.split('.')[0])), \ + or left in MAGIC_COLUMNS, \ "Invalid field %r in domain term %r" % (left, leaf) - if not leaf in (TRUE_LEAF, FALSE_LEAF) and '.' in left: - # leaf still contains '.' -> should be aliases (alias.field) - # update table with alias, and left with field - leaf_path = left.split('.') - assert len(leaf_path) == 2, "Invalid leaf with alias %r in leaf %r" % (left, leaf) - table_alias = leaf_path[0] - table = self._get_table_from_alias(table_alias) - left = leaf_path[1] - else: - table_alias = table._table + table_alias = '"%s"' % (eleaf._generate_alias()) if leaf == TRUE_LEAF: query = 'TRUE' @@ -946,25 +1145,26 @@ class expression(object): stack = [] params = [] # Process the domain from right to left, using a stack, to generate a SQL expression. - for i, e in reverse_enumerate(self.exp): - if is_leaf(e, internal=True): - table = self.leaf_to_table.get(i, self.root_table) - q, p = self.__leaf_to_sql(e, table) + self.result.reverse() + for leaf in self.result: + if leaf.is_leaf(internal=True): + q, p = self.__leaf_to_sql(leaf) params.insert(0, p) stack.append(q) - elif e == NOT_OPERATOR: + elif leaf.leaf == NOT_OPERATOR: stack.append('(NOT (%s))' % (stack.pop(),)) else: ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '} q1 = stack.pop() q2 = stack.pop() - stack.append('(%s %s %s)' % (q1, ops[e], q2,)) + stack.append('(%s %s %s)' % (q1, ops[leaf.leaf], q2,)) assert len(stack) == 1 query = stack[0] joins = ' AND '.join(self.joins) if joins: query = '(%s) AND %s' % (joins, query) - return (query, flatten(params)) + + return (query, tools.flatten(params)) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 7c5b0622db7..6c37b4fed8f 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2721,11 +2721,10 @@ class BaseModel(object): parent_table_name = parent_model._table quoted_parent_table_name = '"%s"' % parent_table_name if quoted_parent_table_name not in query.tables: + print '--> _inheratis_join_add adding %s' % (quoted_parent_table_name) query.tables.append(quoted_parent_table_name) query.where_clause.append('(%s.%s = %s.id)' % (current_table._table, inherits_field, parent_table_name)) - - def _inherits_join_calc(self, field, query): """ Adds missing table select and join clause(s) to ``query`` for reaching @@ -4658,8 +4657,10 @@ class BaseModel(object): query.where_clause += added_clause query.where_clause_params += added_params for table in added_tables: - if table not in query.tables: - query.tables.append(table) + quoted_table_name = '%s' % (table) + if quoted_table_name not in query.tables: + print '--> apply_rule adding %s' % quoted_table_name + query.tables.append(quoted_table_name) return True return False diff --git a/openerp/osv/query.py b/openerp/osv/query.py index 252b301d531..d32ea73b29d 100644 --- a/openerp/osv/query.py +++ b/openerp/osv/query.py @@ -102,7 +102,7 @@ class Query(object): tables_to_process = list(self.tables) def add_joins_for_table(table, query_from): - for (dest_table, lhs_col, col, join) in self.joins.get(table,[]): + for (dest_table, lhs_col, col, join) in self.joins.get(table, []): tables_to_process.remove(dest_table) query_from += ' %s %s ON (%s."%s" = %s."%s")' % \ (join, dest_table, table, lhs_col, dest_table, col) @@ -114,7 +114,7 @@ class Query(object): if table in self.joins: query_from = add_joins_for_table(table, query_from) query_from += ',' - query_from = query_from[:-1] # drop last comma + query_from = query_from[:-1] # drop last comma return (query_from, " AND ".join(self.where_clause), self.where_clause_params) def __str__(self): diff --git a/openerp/tests/test_expression.py b/openerp/tests/test_expression.py index dd78e3fa88c..1c5678becf1 100644 --- a/openerp/tests/test_expression.py +++ b/openerp/tests/test_expression.py @@ -2,11 +2,12 @@ import unittest2 import openerp + class test_domain_normalization(unittest2.TestCase): def test_normalize_domain(self): expression = openerp.osv.expression - norm_domain = domain = ['&',(1,'=',1),('a','=','b')] - assert norm_domain == expression.normalize(domain), "Normalized domains should be left untouched" - domain = [('x','in',['y','z']),('a.v','=','e'),'|','|',('a','=','b'),'!',('c','>','d'),('e','!=','f'),('g','=','h')] - norm_domain = ['&','&','&'] + domain - assert norm_domain == expression.normalize(domain), "Non-normalized domains should be properly normalized" + norm_domain = domain = ['&', (1, '=', 1), ('a', '=', 'b')] + assert norm_domain == expression.normalize_domain(domain), "Normalized domains should be left untouched" + domain = [('x', 'in', ['y', 'z']), ('a.v', '=', 'e'), '|', '|', ('a', '=', 'b'), '!', ('c', '>', 'd'), ('e', '!=', 'f'), ('g', '=', 'h')] + norm_domain = ['&', '&', '&'] + domain + assert norm_domain == expression.normalize_domain(domain), "Non-normalized domains should be properly normalized" From 52bd6459eff3ce8a6e0db7206b3db8837f39f24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 4 Dec 2012 15:56:08 +0100 Subject: [PATCH 21/39] [REV] ir_ui_menu: needaction on menus back to their previous state (as in current trunk as ok 2012/12/04; another branch exist for speeding them up, this one is for improving the parsing. bzr revid: tde@openerp.com-20121204145608-lah4uk40ok5litf0 --- openerp/addons/base/ir/ir_ui_menu.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index cf0f07f42da..e286466b85c 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -265,18 +265,7 @@ class ir_ui_menu(osv.osv): return res - def _get_needaction_enabled(self, cr, uid, ids, field_names, args, context=None): - """ needaction_enabled: tell whether the menu has a related action - that uses the needaction mechanism. """ - res = dict.fromkeys(ids, False) - for menu in self.browse(cr, uid, ids, context=context): - if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model: - obj = self.pool.get(menu.action.res_model) - if obj and obj._needaction: - res[menu.id] = True - return res - - def get_needaction_data(self, cr, uid, ids, context=None): + def get_needaction_data(self, cr, uid, ids, field_names, args, context=None): """ Return for each menu entry of ids : - if it uses the needaction mechanism (needaction_enabled) - the needaction counter of the related action, taking into account @@ -315,11 +304,15 @@ class ir_ui_menu(osv.osv): 'web_icon_hover': fields.char('Web Icon File (hover)', size=128), 'web_icon_data': fields.function(_get_image_icon, string='Web Icon Image', type='binary', readonly=True, store=True, multi='icon'), 'web_icon_hover_data': fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'), - 'needaction_enabled': fields.function(_get_needaction_enabled, - type='boolean', + 'needaction_enabled': fields.function(get_needaction_data, + type='boolean', multi='get_needaction_data', store=True, string='Target model uses the need action mechanism', help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.'), + 'needaction_counter': fields.function(get_needaction_data, + type='integer', multi='get_needaction_data', + string='Number of actions the user has to perform', + help='If the target model uses the need action mechanism, this field gives the number of actions the current user has to perform.'), 'action': fields.function(_action, fnct_inv=_action_inv, type='reference', string='Action', selection=[ From a8359924c22223da5d74f0a636ed45303ff8269c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 4 Dec 2012 18:07:44 +0100 Subject: [PATCH 22/39] [IMP] expression.parse: added the support of domain on one2many fields when _auto_true is activated. Leaf creation from an existing leaf moved as a method in expression object. Added some comments. Added / updated some tests. bzr revid: tde@openerp.com-20121204170744-rv8e0zlteqoyj60o --- openerp/addons/base/tests/test_expression.py | 51 ++++++-- openerp/osv/expression.py | 117 +++++++++++-------- 2 files changed, 115 insertions(+), 53 deletions(-) diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index 5286e1c5657..2bfd8ade1c9 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -381,7 +381,42 @@ class test_expression(common.TransactionCase): "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect parameter") # ---------------------------------------- - # Test3: result-based tests + # Test3: domain attribute on one2many fields + # ---------------------------------------- + + partner_child_ids_col._auto_join = True + partner_bank_ids_col._auto_join = True + partner_child_ids_col._domain = lambda self: ['!', ('name', '=', self._name)] + partner_bank_ids_col._domain = [('acc_number', 'like', '1')] + # Do: 2 cascaded one2many with _auto_join, test final leaf is an id + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, ['&', (1, '=', 1), ('child_ids.bank_ids.id', 'in', [b_aa, b_ba])]) + for query in self.query_list: + print query + print partner_ids + print '--------------------' + # Test result: at least one of our added data + self.assertTrue(set([p_a]).issubset(set(partner_ids)), + "_auto_join on one2many with domains incorrect result") + self.assertTrue(set([p_ab, p_ba]) not in set(partner_ids), + "_auto_join on one2many with domains incorrect result") + # Test produced queries that domains effectively present + sql_query = self.query_list[0].get_sql() + self.assertIn('"res_partner__child_ids__bank_ids"."acc_number" like %s', sql_query[1], + "_auto_join on one2many with domains incorrect result") + # TDE TODO: check first domain has a correct table name + self.assertIn('"res_partner__child_ids__bank_ids"."acc_number" like %s', sql_query[1], + "_auto_join on one2many with domains incorrect result") + + partner_child_ids_col._domain = lambda self: [('name', '=', '__%s' % self._name)] + self._reinit_mock() + partner_ids = partner_obj.search(cr, uid, ['&', (1, '=', 1), ('child_ids.bank_ids.id', 'in', [b_aa, b_ba])]) + # Test result: no one + self.assertFalse(partner_ids, + "_auto_join on one2many with domains incorrect result") + + # ---------------------------------------- + # Test4: result-based tests # ---------------------------------------- partner_bank_ids_col._auto_join = False @@ -389,6 +424,8 @@ class test_expression(common.TransactionCase): partner_state_id_col._auto_join = False partner_parent_id_col._auto_join = False state_country_id_col._auto_join = False + partner_child_ids_col._domain = [] + partner_bank_ids_col._domain = [] # Do: ('child_ids.state_id.country_id.code', 'like', '..') without _auto_join self._reinit_mock() @@ -419,12 +456,12 @@ class test_expression(common.TransactionCase): self.assertEqual(len(self.query_list), 1, "_auto_join on: ('child_ids.state_id.country_id.code', 'like', '..') number of queries incorrect") - # # Remove mocks and modifications - # partner_bank_ids_col._auto_join = False - # partner_child_ids_col._auto_join = False - # partner_state_id_col._auto_join = False - # state_country_id_col._auto_join = False - # BaseModel._where_calc = self._base_model_where_calc + # Remove mocks and modifications + partner_bank_ids_col._auto_join = False + partner_child_ids_col._auto_join = False + partner_state_id_col._auto_join = False + state_country_id_col._auto_join = False + BaseModel._where_calc = self._base_model_where_calc if __name__ == '__main__': unittest2.main() diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 2474e6665cd..9078cf5c42c 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -415,32 +415,32 @@ class ExtendedLeaf(object): for item in self.context_stack: self._tables.append(item[0]) self._tables.append(table) - # if leaf.is_operator: - # self.elements = self.leaf, None, None - # elif leaf.is_true_leaf() or leaf.is_false_leaf(): - # # because we consider left as a string - # self.elements = ('%s' % leaf.leaf[0], leaf.leaf[1], leaf.leaf[2]) - # else: - # self.elements = leaf.leaf - # self.field_path = self.elements[0].split('.', 1) - # self.field = self.table._columns.get(self.field_path[0]) - # if self.field and self.field._obj: - # relational_table = working_table.pool.get(field._obj) - # else: - # relational_table = None + if self.is_operator(): + self.elements = self.leaf, None, None + elif self.is_true_leaf() or self.is_false_leaf(): + # because we consider left as a string + self.elements = ('%s' % leaf[0], leaf[1], leaf[2]) + else: + self.elements = leaf + self.field_path = self.elements[0].split('.', 1) + self.field = self.table._columns.get(self.field_path[0]) + if self.field and self.field._obj: + self.relational_table = self.table.pool.get(self.field._obj) + else: + self.relational_table = None # check validity self.check_leaf() def __str__(self): return '' % (str(self.leaf), self.table._table, ','.join(self._get_context_debug())) - def create_substitution_leaf(self, new_leaf, new_table=None): - if new_table is None: - new_table = self.table - return ExtendedLeaf(new_leaf, new_table, self.context_stack) + # def create_substitution_leaf(self, new_leaf, new_table=None): + # if new_table is None: + # new_table = self.table + # return ExtendedLeaf(new_leaf, new_table, self.context_stack) - def create_sibling_leaf(self, new_leaf): - pass + # def create_sibling_leaf(self, new_leaf): + # pass # -------------------------------------------------- # Join / Context manipulation @@ -464,7 +464,7 @@ class ExtendedLeaf(object): # i.e.: many2one: res_country_state as res_partner__state_id # - join condition use aliases # i.e.: inherits: res_users.partner_id = res_users__partner_id.id - # i.e.: one2many: res_partner.id = res_partner__bank_ids.partner_id + # i.e.: one2many: res_partner.id = res_partner__bank_ids.parr_id # i.e.: many2one: res_partner.state_id = res_partner__state_id.id # Variables explanation: # - src_table: working table before the join @@ -646,12 +646,9 @@ class expression(object): return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))] # ---------------------------------------- - # Internal structure + # Leafs management # ---------------------------------------- - def _format_table_name(self, table_name): - return '"%s"' % (table_name) - def get_tables(self): """ Returns the list of tables for SQL queries, like select from ... """ tables = [] @@ -664,6 +661,16 @@ class expression(object): tables.append(table_name) return tables + def create_substitution_leaf(self, leaf, new_elements, new_table=None): + if new_table is None: + new_table = leaf.table + new_context_stack = [tuple(context) for context in leaf.context_stack] + new_leaf = ExtendedLeaf(new_elements, new_table, context_stack=new_context_stack) + return new_leaf + + def create_sibling_leaf(self, leaf, new_elements): + pass + # ---------------------------------------- # Parsing # ---------------------------------------- @@ -697,6 +704,11 @@ class expression(object): # Get working variables working_table = leaf.table + # left, operator, right = leaf.elements + # field_path = leaf.field_path + # field = leaf.field + # relational_table = leaf.relational_table + if leaf.is_operator(): left, operator, right = leaf.leaf, None, None elif leaf.is_true_leaf() or leaf.is_false_leaf(): @@ -748,30 +760,43 @@ class expression(object): elif not field and left == 'id' and operator == 'child_of': ids2 = self.to_ids(cr, uid, right, working_table, context) dom = self.child_of_domain(cr, uid, left, ids2, working_table) - leafs_to_stack += [leaf.create_substitution_leaf(dom_leaf, working_table) for dom_leaf in dom] + leafs_to_stack += [self.create_substitution_leaf(leaf, dom_leaf, working_table) for dom_leaf in dom] elif not field and field_path[0] in MAGIC_COLUMNS: results_to_stack.append(leaf) elif not field: - raise ValueError("Invalid field %r in leaf %r" % (left, leaf)) + raise ValueError("Invalid field %r in leaf %r" % (left, str(leaf))) # ---------------------------------------- # PATH SPOTTED - # -> XX + # -> many2one or one2many with _auto_join: + # - add a join, then jump into linked field: field.remaining on + # src_table is replaced by remaining on dst_table, and set for re-evaluation + # - if a domain is defined on the field, add it into evaluation + # on the relational table + # -> many2one, many2many, one2many: replace by an equivalent computed + # domain, given by recursively searching on the remaining of the path # -> note: hack about fields.property should not be necessary anymore # as after transforming the field, it will go through this loop once again # ---------------------------------------- elif len(field_path) > 1 and field._type == 'many2one' and field._auto_join: - # res_partner.parent_id = res_partner.id + # res_partner.state_id = res_partner__state_id.id leaf.add_join_context(field_path[0], field_path[0], relational_table, 'id') - leafs_to_stack.append(leaf.create_substitution_leaf((field_path[1], operator, right), relational_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, (field_path[1], operator, right), relational_table)) elif len(field_path) > 1 and field._type == 'one2many' and field._auto_join: - # res_partner.id = res_partner.parent_id + # res_partner.id = res_partner__bank_ids.partner_id leaf.add_join_context(field_path[0], 'id', relational_table, field._fields_id) - leafs_to_stack.append(leaf.create_substitution_leaf((field_path[1], operator, right), relational_table)) + domain = field._domain(working_table) if callable(field._domain) else field._domain + if domain: + domain = normalize_domain(domain) + leafs_to_stack.append(self.create_substitution_leaf(leaf, AND_OPERATOR, relational_table)) + for elem in domain: + leafs_to_stack.append(self.create_substitution_leaf(leaf, elem, relational_table)) + print '--> appending %s' % str(leafs_to_stack[-1]) + leafs_to_stack.append(self.create_substitution_leaf(leaf, (field_path[1], operator, right), relational_table)) elif len(field_path) > 1 and field._auto_join: assert False, \ @@ -817,10 +842,10 @@ class expression(object): else: # we assume that the expression is valid # we create a dummy leaf for forcing the parsing of the resulting expression - leafs_to_stack.append(leaf.create_substitution_leaf(AND_OPERATOR, working_table)) - leafs_to_stack.append(leaf.create_substitution_leaf(TRUE_LEAF, working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, AND_OPERATOR, working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, TRUE_LEAF, working_table)) for domain_element in fct_domain: - leafs_to_stack.append(leaf.create_substitution_leaf(domain_element, working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, domain_element, working_table)) # Applying recursivity on field(one2many) elif field._type == 'one2many' and operator == 'child_of': @@ -829,7 +854,7 @@ class expression(object): dom = self.child_of_domain(cr, uid, left, ids2, relational_table, prefix=field._obj) else: dom = self.child_of_domain(cr, uid, 'id', ids2, working_table, parent=left) - leafs_to_stack += [leaf.create_substitution_leaf(dom_leaf, working_table) for dom_leaf in dom] + leafs_to_stack += [self.create_substitution_leaf(leaf, dom_leaf, working_table) for dom_leaf in dom] elif field._type == 'one2many': call_null = True @@ -848,17 +873,17 @@ class expression(object): if operator in ['like', 'ilike', 'in', '=']: #no result found with given search criteria call_null = False - leafs_to_stack.append(leaf.create_substitution_leaf(FALSE_LEAF, working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, FALSE_LEAF, working_table)) else: ids2 = select_from_where(cr, field._fields_id, relational_table._table, 'id', ids2, operator) if ids2: call_null = False o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - leafs_to_stack.append(leaf.create_substitution_leaf(('id', o2m_op, ids2), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', o2m_op, ids2), working_table)) if call_null: o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - leafs_to_stack.append(leaf.create_substitution_leaf(('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)), working_table)) elif field._type == 'many2many': rel_table, rel_id1, rel_id2 = field._sql_names(working_table) @@ -872,7 +897,7 @@ class expression(object): ids2 = self.to_ids(cr, uid, right, relational_table, context) dom = self.child_of_domain(cr, uid, 'id', ids2, relational_table) ids2 = relational_table.search(cr, uid, dom, context=context) - leafs_to_stack.append(leaf.create_substitution_leaf(('id', 'in', _rec_convert(ids2)), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), working_table)) else: call_null_m2m = True if right is not False: @@ -889,17 +914,17 @@ class expression(object): if operator in ['like', 'ilike', 'in', '=']: #no result found with given search criteria call_null_m2m = False - leafs_to_stack.append(leaf.create_substitution_leaf(FALSE_LEAF, working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, FALSE_LEAF, working_table)) else: operator = 'in' # operator changed because ids are directly related to main object else: call_null_m2m = False m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - leafs_to_stack.append(leaf.create_substitution_leaf(('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), working_table)) if call_null_m2m: m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - leafs_to_stack.append(leaf.create_substitution_leaf(('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_table)) elif field._type == 'many2one': if operator == 'child_of': @@ -908,7 +933,7 @@ class expression(object): dom = self.child_of_domain(cr, uid, left, ids2, relational_table, prefix=field._obj) else: dom = self.child_of_domain(cr, uid, 'id', ids2, working_table, parent=left) - leafs_to_stack += [leaf.create_substitution_leaf(dom_leaf, working_table) for dom_leaf in dom] + leafs_to_stack += [self.create_substitution_leaf(leaf, dom_leaf, working_table) for dom_leaf in dom] else: def _get_expression(relational_table, cr, uid, left, right, operator, context=None): if context is None: @@ -932,7 +957,7 @@ class expression(object): # resolve string-based m2o criterion into IDs if isinstance(right, basestring) or \ right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right): - leafs_to_stack.append(leaf.create_substitution_leaf(_get_expression(relational_table, cr, uid, left, right, operator, context=context), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, _get_expression(relational_table, cr, uid, left, right, operator, context=context), working_table)) else: # right == [] or right == False and all other cases are handled by __leaf_to_sql() results_to_stack.append(leaf) @@ -947,7 +972,7 @@ class expression(object): elif operator in ('<', '<='): right += ' 23:59:59' - leafs_to_stack.append(leaf.create_substitution_leaf((left, operator, right), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, (left, operator, right), working_table)) elif field.translate: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') @@ -982,7 +1007,7 @@ class expression(object): right, right, ] - leafs_to_stack.append(leaf.create_substitution_leaf(('id', 'inselect', (subselect, params)), working_table)) + leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', 'inselect', (subselect, params)), working_table)) else: results_to_stack.append(leaf) From ae6f7b3a081b778f4e6924f299dc06e902a927d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 5 Dec 2012 15:07:26 +0100 Subject: [PATCH 23/39] [IMP] ir_needaction: search is now without count but with a limit (to 100), and a simplified order by. This allows much faster queries as postgresql does not have to worry about order and things like that. bzr revid: tde@openerp.com-20121205140726-z1o4ueclat7y96pl --- openerp/addons/base/ir/ir_needaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index 57b324de3b9..898e0c3f473 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -61,4 +61,4 @@ class ir_needaction_mixin(osv.AbstractModel): dom = self._needaction_domain_get(cr, uid, context=context) if not dom: return 0 - return self.search(cr, uid, (domain or []) + dom, limit=100, context=context, count=True) + return self.search(cr, uid, (domain or []) + dom, limit=100, order='id DESC', context=context) From 37051bbe917dacd2a5e4f531cb69ecd117459285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 5 Dec 2012 15:32:38 +0100 Subject: [PATCH 24/39] [FIX] needaction: returns a count-like result. bzr revid: tde@openerp.com-20121205143238-xbna8akgqduap2t5 --- openerp/addons/base/ir/ir_needaction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index 898e0c3f473..39432ca6792 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -61,4 +61,5 @@ class ir_needaction_mixin(osv.AbstractModel): dom = self._needaction_domain_get(cr, uid, context=context) if not dom: return 0 - return self.search(cr, uid, (domain or []) + dom, limit=100, order='id DESC', context=context) + res = self.search(cr, uid, (domain or []) + dom, limit=100, order='id DESC', context=context) + return len(res) From ea01dfe9ddfe02dae9a35812b868ad5e84614d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 5 Dec 2012 16:34:10 +0100 Subject: [PATCH 25/39] [CLEAN] fields.py: added auto_join in one2many and many2one __init__, binded on _auto_join. Added some short explanations in fields.py about the attribute. bzr revid: tde@openerp.com-20121205153410-07vo3j1c641xgmda --- openerp/osv/fields.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index b15cebd29f5..b9096b31d57 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -27,6 +27,9 @@ Fields Attributes: * _classic_read: is a classic sql fields * _type : field type + * _auto_join: for one2many and many2one fields, tells whether select + queries will join the relational table instead of replacing the + field condition by an equivalent-one based on a search. * readonly * required * size @@ -428,9 +431,10 @@ class many2one(_column): _symbol_f = lambda x: x or None _symbol_set = (_symbol_c, _symbol_f) - def __init__(self, obj, string='unknown', **args): + def __init__(self, obj, string='unknown', auto_join=False, **args): _column.__init__(self, string=string, **args) self._obj = obj + self._auto_join = auto_join def get(self, cr, obj, ids, name, user=None, context=None, values=None): if context is None: @@ -497,11 +501,12 @@ class one2many(_column): _prefetch = False _type = 'one2many' - def __init__(self, obj, fields_id, string='unknown', limit=None, **args): + def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args): _column.__init__(self, string=string, **args) self._obj = obj self._fields_id = fields_id self._limit = limit + self._auto_join = auto_join #one2many can't be used as condition for defaults assert(self.change_default != True) From 549262626369b000a22e2475ec32f41c67479676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 5 Dec 2012 16:34:49 +0100 Subject: [PATCH 26/39] [DOC] Updated the doc about auto_join: a bit more precise, and updated accordingly to last changes in implementation. bzr revid: tde@openerp.com-20121205153449-68uel0ne0od3jl9i --- doc/06_misc_auto_join.rst | 65 +++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/doc/06_misc_auto_join.rst b/doc/06_misc_auto_join.rst index dab24f1bf09..1dd2d0edb88 100644 --- a/doc/06_misc_auto_join.rst +++ b/doc/06_misc_auto_join.rst @@ -5,11 +5,11 @@ Perfoming joins in select .. versionadded:: 7.0 -Starting with OpenERP 7.0, an ``_auto_join`` attribute is added on *many2one* and +Starting with OpenERP 7.0, an ``auto_join`` attribute is added on *many2one* and *one2many* fields. The purpose is to allow the automatic generation of joins in select queries. This attribute is set to False by default, therefore not changing -the default behavior of those fields. It is not recommended to use this attribute -unless you understand the limitations of the feature. +the default behavior. Please note that we consider this feature as still experimental +and should be used only if you understand its limitations and targets. Without ``_auto_join``, the behavior of expression.parse() is the same as before. Leafs holding a path beginning with many2one or one2many fields perform a search @@ -19,42 +19,53 @@ For example, if you have on res.partner a domain like ``[('bank_ids.name', performed : - 1 on res_partner_bank, with domain ``[('name', '=', 'foo')]``, that returns a - list of (res.partner.bank) bids + list of res.partner.bank ids (bids) - 1 on res_partner, with a domain ``['bank_ids', 'in', bids)]``, that returns a - list of (res.partner) pids + list of res.partner ids (pids) - 1 on res_partner, with a domain ``[('id', 'in', pids)]`` -When the _auto_join attribute is True, it will perform a select on res_partner -as well as on res_partner_bank. +When the ``auto_join`` attribute is True on a relational field, the destination +table will be joined to produce only one query. -- the relational table will be accessed through an alias: ``'"res_partner_bank" - as res_partner__bank_ids`` -- the relational table will have a join condition on the main table: +- the relational table is accessed using an alias: ``'"res_partner_bank" + as res_partner__bank_ids``. The alias is generated using the relational field + name. This allows to have multiple joins with different join conditions on the + same table, depending on the domain. +- there is a join condition between the destination table and the main table: ``res_partner__bank_ids."partner_id"=res_partner."id"`` -- the condition will be written on the relational table: +- the condition is then written on the relational table: ``res_partner__bank_ids."name" = 'foo'`` -This job is performed in expression.parse(). For leafs containing a path, it -checks whether the first item of the path is a *many2one* or *one2many* field -with the ``auto_join`` attribute set. If set, it adds a join query and recursively -analyzes the remaining of the leaf, going back to the normal behavior when -not reaching an ``_auto_join`` field. The sql condition created from the leaf -will be updated to take into account the table aliases. +This manipulation is performed in expression.parse(). It checks leafs that +contain a path, i.e. any domain containing a '.'. It then checks whether the +first item of the path is a *many2one* or *one2many* field with the ``auto_join`` +attribute set. If set, it adds a join query and recursively analyzes the +remaining of the leaf, using the same behavior. If the remaining path also holds +a path with auto_join fields, it will add all tables and add every necessary +join conditions. -Chaining _auto_join allows to reduce the number of queries performed, and to -avoid having too long ``('id', 'in', ids)`` replacement leafs in domains. -However, severe limitations exist on this feature that limits its current use as -of version 7.0. **This feature is therefore considered as experimental, and used +Chaining joins allows to reduce the number of queries performed, and to avoid +having too long equivalent leaf replacement in domains. Indeed, the internal +queries produced by this behavior can be very costly, because they were generally +select queries without limit that could lead to huge ('id', 'in', [...]) +leafs to analyze and execute. + +Some limitations exist on this feature that limits its current use as of version +7.0. **This feature is therefore considered as experimental, and used to speedup some precise bottlenecks in OpenERP**. List of known issues and limitations: -- using _auto_join bypasses the business logic; no name search is performed, only - direct matches between ids using join conditions -- ir.rules are not taken into account when performing the _auto_join. -- support of active_test is not asserted -- support of translation is not asserted -- support of _auto_join leading to function fields +- using ``auto_join`` bypasses the business logic; no name search is performed, + only direct matches between ids using join conditions +- ir.rules are not taken into account when analyzing and adding the join + conditions + +List of already-supported corner cases : + +- one2many fields having a domain attribute. Static domains as well as dynamic + domain are supported +- auto_join leading to functional searchable fields Typical use in OpenERP 7.0: From 02c200844c66331b583e4f4821e9dd638eb7e282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 5 Dec 2012 16:35:35 +0100 Subject: [PATCH 27/39] [CLEAN] Removed some forgotten print statements. bzr revid: tde@openerp.com-20121205153535-lfs6yrokyk0wfm0w --- openerp/osv/orm.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index f37d1a2202d..a08126d12fc 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2732,7 +2732,6 @@ class BaseModel(object): parent_table_name = parent_model._table quoted_parent_table_name = '"%s"' % parent_table_name if quoted_parent_table_name not in query.tables: - print '--> _inheratis_join_add adding %s' % (quoted_parent_table_name) query.tables.append(quoted_parent_table_name) query.where_clause.append('(%s.%s = %s.id)' % (current_table._table, inherits_field, parent_table_name)) @@ -4670,7 +4669,6 @@ class BaseModel(object): for table in added_tables: quoted_table_name = '%s' % (table) if quoted_table_name not in query.tables: - print '--> apply_rule adding %s' % quoted_table_name query.tables.append(quoted_table_name) return True return False From df4665fcd2d52a12bf5a36955334e852d9195612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 6 Dec 2012 10:16:48 +0100 Subject: [PATCH 28/39] [IMP] [CLEAN] [REVIEW] field._properties -> isinstance(field, fields.function). bzr revid: tde@openerp.com-20121206091648-vmy2ghr293reoxn6 --- openerp/osv/expression.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 9078cf5c42c..c4c7f4400ea 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -137,6 +137,7 @@ import logging import traceback import openerp.modules +from openerp.osv import fields from openerp.osv.orm import MAGIC_COLUMNS import openerp.tools as tools @@ -814,12 +815,13 @@ class expression(object): leaf.leaf = ('id', 'in', table_ids) leafs_to_stack.append(leaf) - # ---------------------------------------- + # ------------------------------------------------- # FUNCTION FIELD - # -> not stored, get the result of fnct_search - # ---------------------------------------- + # -> not stored: error if no _fnct_search, otherwise handle the result domain + # -> stored: management done in the remaining of parsing + # ------------------------------------------------- - elif field._properties and not field.store and not field._fnct_search: + elif isinstance(field, fields.function) and not field.store and not field._fnct_search: # this is a function field that is not stored # the function field doesn't provide a search function and doesn't store # values in the database, so we must ignore it : we generate a dummy leaf @@ -833,7 +835,7 @@ class expression(object): _logger.debug(''.join(traceback.format_stack())) leafs_to_stack.append(leaf) - elif field._properties and not field.store: + elif isinstance(field, fields.function) and not field.store: # this is a function field that is not stored fct_domain = field.search(cr, uid, working_table, left, [leaf.leaf], context=context) if not fct_domain: From a0b8a58174a0c5f5d0104f99a8b07abac7ca2109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 6 Dec 2012 10:27:45 +0100 Subject: [PATCH 29/39] [IMP] [REVIEW] _auto_join on many2many now raises a NotImplementedError; added a test case for that. bzr revid: tde@openerp.com-20121206092745-a8t62khs4x37db1g --- openerp/addons/base/tests/test_expression.py | 11 ++++++++++- openerp/osv/expression.py | 3 +-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index 2bfd8ade1c9..a8b69ac8c06 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -126,9 +126,10 @@ class test_expression(common.TransactionCase): # Get test columns partner_state_id_col = partner_obj._columns.get('state_id') # many2one on res.partner to res.country.state partner_parent_id_col = partner_obj._columns.get('parent_id') # many2one on res.partner to res.partner + state_country_id_col = state_obj._columns.get('country_id') # many2one on res.country.state on res.country partner_child_ids_col = partner_obj._columns.get('child_ids') # one2many on res.partner to res.partner partner_bank_ids_col = partner_obj._columns.get('bank_ids') # one2many on res.partner to res.partner.bank - state_country_id_col = state_obj._columns.get('country_id') # many2one on res.country.state on res.country + category_id_col = partner_obj._columns.get('category_id') # many2many on res.partner to res.partner.category # Get the first bank account type to be able to create a res.partner.bank bank_type = bank_obj._bank_type_get(cr, uid)[0] @@ -146,6 +147,14 @@ class test_expression(common.TransactionCase): b_ab = bank_obj.create(cr, uid, {'name': '__bank_test_b', 'state': bank_type[0], 'partner_id': p_ab, 'acc_number': '5678'}) b_ba = bank_obj.create(cr, uid, {'name': '__bank_test_b', 'state': bank_type[0], 'partner_id': p_ba, 'acc_number': '9876'}) + # ---------------------------------------- + # Test1: basics about the attribute + # ---------------------------------------- + + category_id_col._auto_join = True + self.assertRaises(NotImplementedError, partner_obj.search, cr, uid, [('category_id.name', '=', 'foo')]) + category_id_col._auto_join = False + # ---------------------------------------- # Test1: one2many # ---------------------------------------- diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index c4c7f4400ea..383e70dccfa 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -800,8 +800,7 @@ class expression(object): leafs_to_stack.append(self.create_substitution_leaf(leaf, (field_path[1], operator, right), relational_table)) elif len(field_path) > 1 and field._auto_join: - assert False, \ - '_auto_join attribute on something else than a many2one or one2many is currently not supported' + raise NotImplementedError('_auto_join attribute not supported on many2many field %s' % (left)) elif len(field_path) > 1 and field._type == 'many2one': right_ids = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) From bb57752f0a930647ee2205aef3759bac466e132d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 6 Dec 2012 17:21:28 +0100 Subject: [PATCH 30/39] [IMP] [CLEAN] [REVIEW] expression: udpated domain management. Added pop and push methods in parse to hide the internal plumbery. Cleaned some comments, deleted dead/unnecessary code, updated some comments. to_ids and childof_domain back in parse. Added generate_table_alias, method that should be used everywhere if possible when generating table name or aliases for sql conditions and things like that. bzr revid: tde@openerp.com-20121206162128-1qol201os1xztlz5 --- openerp/osv/expression.py | 352 ++++++++++++++++++-------------------- 1 file changed, 171 insertions(+), 181 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 383e70dccfa..8967d9f254a 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -313,6 +313,37 @@ def distribute_not(domain): # Generic leaf manipulation # -------------------------------------------------- +def _quote(to_quote): + if '"' not in to_quote: + return '"%s"' % to_quote + return to_quote + + +def generate_table_alias(src_model, joined_tables=[]): + """ Generate a standard table alias name. An alias is generated as following: + - the base is the source model table name + - then, each joined table is added in the alias using a 'link field name' + that is used to render unique aliases for a given path + Examples: + - src_model=res.users, join_tables=[]: + alias = '"res_users"' + - src_model=res.users, join_tables=[(res.partner, 'parent_id')] + alias = '"res_partner" as "res_users__parent_id"' + + :param model src_model: model source of the alias + :param list join_tables: list of tuples + (dst_model, link_field) + + :return tuple: (table alias, alias statement for from clause with quotes added) + """ + alias = src_model._table + if not joined_tables: + return ('%s' % alias, '%s' % _quote(alias)) + for link in joined_tables: + alias += '__' + link[1] + return ('%s' % alias, '%s as %s' % (_quote(joined_tables[-1][0]._table), _quote(alias))) + + def normalize_leaf(element): """ Change a term's operator to some canonical form, simplifying later processing. """ @@ -416,33 +447,12 @@ class ExtendedLeaf(object): for item in self.context_stack: self._tables.append(item[0]) self._tables.append(table) - if self.is_operator(): - self.elements = self.leaf, None, None - elif self.is_true_leaf() or self.is_false_leaf(): - # because we consider left as a string - self.elements = ('%s' % leaf[0], leaf[1], leaf[2]) - else: - self.elements = leaf - self.field_path = self.elements[0].split('.', 1) - self.field = self.table._columns.get(self.field_path[0]) - if self.field and self.field._obj: - self.relational_table = self.table.pool.get(self.field._obj) - else: - self.relational_table = None # check validity self.check_leaf() def __str__(self): return '' % (str(self.leaf), self.table._table, ','.join(self._get_context_debug())) - # def create_substitution_leaf(self, new_leaf, new_table=None): - # if new_table is None: - # new_table = self.table - # return ExtendedLeaf(new_leaf, new_table, self.context_stack) - - # def create_sibling_leaf(self, new_leaf): - # pass - # -------------------------------------------------- # Join / Context manipulation # running examples: @@ -488,42 +498,48 @@ class ExtendedLeaf(object): # i.e.: many2one: 'state_id': current field name # -------------------------------------------------- - def _generate_alias(self): - alias = self._tables[0]._table - for context in self.context_stack: - alias += '__' + context[4] + def generate_alias(self): + links = [(context[1], context[4]) for context in self.context_stack] + alias, alias_statement = generate_table_alias(self._tables[0], links) return alias - def add_join_context(self, context_field_name, src_table_link_name, dst_table, dst_table_link_name): - """ See above comments for more details. A join context is a tuple structure - holding the following elements: (src_table (self.table), src_table_link_name, - dst_table, dst_table_link_name, context_field_name) + def add_join_context(self, table, lhs_col, table_col, link): + """ See above comments for more details. A join context is a tuple like: + ``(lhs, table, lhs_col, col, link)`` + where + - lhs is the source table (self.table) + - table is the destination table + - lsh_col is the source table column name used for the condition + - table_col is the destination table column name used for the condition + - link is the field name source of the join used as context to + generate the destination table alias + After adding the join, the table of the current leaf is updated. """ - self.context_stack.append((self.table, src_table_link_name, dst_table, dst_table_link_name, context_field_name)) - self._tables.append(dst_table) - self.table = dst_table + self.context_stack.append((self.table, table, lhs_col, table_col, link)) + self._tables.append(table) + self.table = table def get_join_conditions(self): - names = [] + conditions = [] alias = self._tables[0]._table for context in self.context_stack: previous_alias = alias alias += '__' + context[4] - names.append('%s."%s"=%s."%s"' % (previous_alias, context[1], alias, context[3])) - return names + conditions.append('"%s"."%s"="%s"."%s"' % (previous_alias, context[2], alias, context[3])) + return conditions def get_tables(self): tables = set() alias = self._tables[0]._table for context in self.context_stack: alias += '__' + context[4] - table_full_alias = '"%s" as %s' % (context[2]._table, alias) + table_full_alias = '"%s" as "%s"' % (context[1]._table, alias) tables.add(table_full_alias) return tables def _get_context_debug(self): - names = ['%s."%s"=%s."%s" (%s)' % (table[0]._table, table[1], table[2]._table, table[3], table[4]) for table in self.context_stack] + names = ['"%s"."%s"="%s"."%s" (%s)' % (item[0]._table, item[2], item[1]._table, item[3], item[4]) for item in self.context_stack] return names # -------------------------------------------------- @@ -542,12 +558,6 @@ class ExtendedLeaf(object): if not is_operator(self.leaf) and not is_leaf(self.leaf, True): raise ValueError("Invalid leaf %s" % str(self.leaf)) - # if self.is_leaf() and not (self.is_true_leaf() or self.is_false_leaf()) \ - # and not self.field and not self.field_path[0] in self.table._inherit_fields \ - # and not (self.leaf[0] == 'id' and self.leaf[1] == 'child_of') \ - # and not (self.field_path[0] in MAGIC_COLUMNS): - # raise ValueError("Invalid field %r in leaf %r" % (self.leaf[0], self.leaf)) - def is_operator(self): return is_operator(self.leaf) @@ -598,54 +608,6 @@ class expression(object): # parse the domain expression self.parse(cr, uid, context=context) - # ---------------------------------------- - # Tools for domain manipulation - # ---------------------------------------- - - def to_ids(self, cr, uid, value, relational_table, context=None, limit=None): - """ Normalize a single id or name, or a list of those, into a list of ids - :param {int,long,basestring,list,tuple} value: - if int, long -> return [value] - if basestring, convert it into a list of basestrings, then - if list of basestring -> - perform a name_search on relational_table for each name - return the list of related ids - """ - names = [] - if isinstance(value, basestring): - names = [value] - elif value and isinstance(value, (tuple, list)) and all(isinstance(item, basestring) for item in value): - names = value - elif isinstance(value, (int, long)): - return [value] - if names: - name_get_list = [name_get[0] for name in names for name_get in relational_table.name_search(cr, uid, name, [], 'ilike', context=context, limit=limit)] - return list(set(name_get_list)) - return list(value) - - def child_of_domain(self, cr, uid, left, ids, left_model, parent=None, prefix='', context=None): - """ Return a domain implementing the child_of operator for [(left,child_of,ids)], - either as a range using the parent_left/right tree lookup fields - (when available), or as an expanded [(left,in,child_ids)] """ - if left_model._parent_store and (not left_model.pool._init): - # TODO: Improve where joins are implemented for many with '.', replace by: - # doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)] - doms = [] - for o in left_model.browse(cr, uid, ids, context=context): - if doms: - doms.insert(0, OR_OPERATOR) - doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)] - if prefix: - return [(left, 'in', left_model.search(cr, uid, doms, context=context))] - return doms - else: - def recursive_children(ids, model, parent_field): - if not ids: - return [] - ids2 = model.search(cr, uid, [(parent_field, 'in', ids)], context=context) - return ids + recursive_children(ids2, model, parent_field) - return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))] - # ---------------------------------------- # Leafs management # ---------------------------------------- @@ -662,16 +624,6 @@ class expression(object): tables.append(table_name) return tables - def create_substitution_leaf(self, leaf, new_elements, new_table=None): - if new_table is None: - new_table = leaf.table - new_context_stack = [tuple(context) for context in leaf.context_stack] - new_leaf = ExtendedLeaf(new_elements, new_table, context_stack=new_context_stack) - return new_leaf - - def create_sibling_leaf(self, leaf, new_elements): - pass - # ---------------------------------------- # Parsing # ---------------------------------------- @@ -694,22 +646,78 @@ class expression(object): :var obj relational_table: relational table of a field (field._obj) ex: res_partner.bank_ids -> res_partner_bank """ - result = [] - stack = [ExtendedLeaf(leaf, self.root_table) for leaf in self.expression] - while stack: + def to_ids(value, relational_table, context=None, limit=None): + """ Normalize a single id or name, or a list of those, into a list of ids + :param {int,long,basestring,list,tuple} value: + if int, long -> return [value] + if basestring, convert it into a list of basestrings, then + if list of basestring -> + perform a name_search on relational_table for each name + return the list of related ids + """ + names = [] + if isinstance(value, basestring): + names = [value] + elif value and isinstance(value, (tuple, list)) and all(isinstance(item, basestring) for item in value): + names = value + elif isinstance(value, (int, long)): + return [value] + if names: + name_get_list = [name_get[0] for name in names for name_get in relational_table.name_search(cr, uid, name, [], 'ilike', context=context, limit=limit)] + return list(set(name_get_list)) + return list(value) + + def child_of_domain(left, ids, left_model, parent=None, prefix='', context=None): + """ Return a domain implementing the child_of operator for [(left,child_of,ids)], + either as a range using the parent_left/right tree lookup fields + (when available), or as an expanded [(left,in,child_ids)] """ + if left_model._parent_store and (not left_model.pool._init): + # TODO: Improve where joins are implemented for many with '.', replace by: + # doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)] + doms = [] + for o in left_model.browse(cr, uid, ids, context=context): + if doms: + doms.insert(0, OR_OPERATOR) + doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)] + if prefix: + return [(left, 'in', left_model.search(cr, uid, doms, context=context))] + return doms + else: + def recursive_children(ids, model, parent_field): + if not ids: + return [] + ids2 = model.search(cr, uid, [(parent_field, 'in', ids)], context=context) + return ids + recursive_children(ids2, model, parent_field) + return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))] + + def create_substitution_leaf(leaf, new_elements, new_table=None): + if new_table is None: + new_table = leaf.table + new_context_stack = [tuple(context) for context in leaf.context_stack] + new_leaf = ExtendedLeaf(new_elements, new_table, context_stack=new_context_stack) + return new_leaf + + def pop(): + return self.stack.pop() + + def push(leaf): + self.stack.append(leaf) + + def push_result(leaf): + self.result.append(leaf) + + self.result = [] + self.stack = [ExtendedLeaf(leaf, self.root_table) for leaf in self.expression] + # process from right to left; expression is from left to right + self.stack.reverse() + + while self.stack: # Get the next leaf to process - leaf = stack.pop(0) - leafs_to_stack = [] - results_to_stack = [] + leaf = pop() # Get working variables working_table = leaf.table - # left, operator, right = leaf.elements - # field_path = leaf.field_path - # field = leaf.field - # relational_table = leaf.relational_table - if leaf.is_operator(): left, operator, right = leaf.leaf, None, None elif leaf.is_true_leaf() or leaf.is_false_leaf(): @@ -717,11 +725,8 @@ class expression(object): left, operator, right = ('%s' % leaf.leaf[0], leaf.leaf[1], leaf.leaf[2]) else: left, operator, right = leaf.leaf - # field_path = leaf.field_path - # field = leaf.field field_path = left.split('.', 1) field = working_table._columns.get(field_path[0]) - if field and field._obj: relational_table = working_table.pool.get(field._obj) else: @@ -734,10 +739,8 @@ class expression(object): # -> add directly to result # ---------------------------------------- - if leaf.is_operator(): - results_to_stack.append(leaf) - elif leaf.is_true_leaf() or leaf.is_false_leaf(): - results_to_stack.append(leaf) + if leaf.is_operator() or leaf.is_true_leaf() or leaf.is_false_leaf(): + push_result(leaf) # ---------------------------------------- # FIELD NOT FOUND @@ -755,16 +758,18 @@ class expression(object): # { 'field_name': ('parent_model', 'm2o_field_to_reach_parent', # field_column_obj, origina_parent_model), ... } next_table = working_table.pool.get(working_table._inherit_fields[field_path[0]][0]) - leaf.add_join_context(working_table._inherits[next_table._name], working_table._inherits[next_table._name], next_table, 'id') - leafs_to_stack.append(leaf) + leaf.add_join_context(next_table, working_table._inherits[next_table._name], 'id', working_table._inherits[next_table._name]) + push(leaf) elif not field and left == 'id' and operator == 'child_of': - ids2 = self.to_ids(cr, uid, right, working_table, context) - dom = self.child_of_domain(cr, uid, left, ids2, working_table) - leafs_to_stack += [self.create_substitution_leaf(leaf, dom_leaf, working_table) for dom_leaf in dom] + ids2 = to_ids(right, working_table, context) + dom = child_of_domain(left, ids2, working_table) + for dom_leaf in reversed(dom): + new_leaf = create_substitution_leaf(leaf, dom_leaf, working_table) + push(new_leaf) elif not field and field_path[0] in MAGIC_COLUMNS: - results_to_stack.append(leaf) + push_result(leaf) elif not field: raise ValueError("Invalid field %r in leaf %r" % (left, str(leaf))) @@ -784,20 +789,19 @@ class expression(object): elif len(field_path) > 1 and field._type == 'many2one' and field._auto_join: # res_partner.state_id = res_partner__state_id.id - leaf.add_join_context(field_path[0], field_path[0], relational_table, 'id') - leafs_to_stack.append(self.create_substitution_leaf(leaf, (field_path[1], operator, right), relational_table)) + leaf.add_join_context(relational_table, field_path[0], 'id', field_path[0]) + push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_table)) elif len(field_path) > 1 and field._type == 'one2many' and field._auto_join: # res_partner.id = res_partner__bank_ids.partner_id - leaf.add_join_context(field_path[0], 'id', relational_table, field._fields_id) + leaf.add_join_context(relational_table, 'id', field._fields_id, field_path[0]) domain = field._domain(working_table) if callable(field._domain) else field._domain + push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_table)) if domain: domain = normalize_domain(domain) - leafs_to_stack.append(self.create_substitution_leaf(leaf, AND_OPERATOR, relational_table)) - for elem in domain: - leafs_to_stack.append(self.create_substitution_leaf(leaf, elem, relational_table)) - print '--> appending %s' % str(leafs_to_stack[-1]) - leafs_to_stack.append(self.create_substitution_leaf(leaf, (field_path[1], operator, right), relational_table)) + for elem in reversed(domain): + push(create_substitution_leaf(leaf, elem, relational_table)) + push(create_substitution_leaf(leaf, AND_OPERATOR, relational_table)) elif len(field_path) > 1 and field._auto_join: raise NotImplementedError('_auto_join attribute not supported on many2many field %s' % (left)) @@ -805,14 +809,14 @@ class expression(object): elif len(field_path) > 1 and field._type == 'many2one': right_ids = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) leaf.leaf = (field_path[0], 'in', right_ids) - leafs_to_stack.append(leaf) + push(leaf) # Making search easier when there is a left operand as field.o2m or field.m2m elif len(field_path) > 1 and field._type in ['many2many', 'one2many']: right_ids = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) table_ids = working_table.search(cr, uid, [(field_path[0], 'in', right_ids)], context=dict(context, active_test=False)) leaf.leaf = ('id', 'in', table_ids) - leafs_to_stack.append(leaf) + push(leaf) # ------------------------------------------------- # FUNCTION FIELD @@ -832,30 +836,31 @@ class expression(object): # avoid compiling stack trace if not needed if _logger.isEnabledFor(logging.DEBUG): _logger.debug(''.join(traceback.format_stack())) - leafs_to_stack.append(leaf) + push(leaf) elif isinstance(field, fields.function) and not field.store: # this is a function field that is not stored fct_domain = field.search(cr, uid, working_table, left, [leaf.leaf], context=context) if not fct_domain: leaf.leaf = TRUE_LEAF - leafs_to_stack.append(leaf) + push(leaf) else: # we assume that the expression is valid # we create a dummy leaf for forcing the parsing of the resulting expression - leafs_to_stack.append(self.create_substitution_leaf(leaf, AND_OPERATOR, working_table)) - leafs_to_stack.append(self.create_substitution_leaf(leaf, TRUE_LEAF, working_table)) - for domain_element in fct_domain: - leafs_to_stack.append(self.create_substitution_leaf(leaf, domain_element, working_table)) + for domain_element in reversed(fct_domain): + push(create_substitution_leaf(leaf, domain_element, working_table)) + # self.push(create_substitution_leaf(leaf, TRUE_LEAF, working_table)) + # self.push(create_substitution_leaf(leaf, AND_OPERATOR, working_table)) # Applying recursivity on field(one2many) elif field._type == 'one2many' and operator == 'child_of': - ids2 = self.to_ids(cr, uid, right, relational_table, context) + ids2 = to_ids(right, relational_table, context) if field._obj != working_table._name: - dom = self.child_of_domain(cr, uid, left, ids2, relational_table, prefix=field._obj) + dom = child_of_domain(left, ids2, relational_table, prefix=field._obj) else: - dom = self.child_of_domain(cr, uid, 'id', ids2, working_table, parent=left) - leafs_to_stack += [self.create_substitution_leaf(leaf, dom_leaf, working_table) for dom_leaf in dom] + dom = child_of_domain('id', ids2, working_table, parent=left) + for dom_leaf in reversed(dom): + push(create_substitution_leaf(leaf, dom_leaf, working_table)) elif field._type == 'one2many': call_null = True @@ -874,17 +879,17 @@ class expression(object): if operator in ['like', 'ilike', 'in', '=']: #no result found with given search criteria call_null = False - leafs_to_stack.append(self.create_substitution_leaf(leaf, FALSE_LEAF, working_table)) + push(create_substitution_leaf(leaf, FALSE_LEAF, working_table)) else: ids2 = select_from_where(cr, field._fields_id, relational_table._table, 'id', ids2, operator) if ids2: call_null = False o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', o2m_op, ids2), working_table)) + push(create_substitution_leaf(leaf, ('id', o2m_op, ids2), working_table)) if call_null: o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)), working_table)) + push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)), working_table)) elif field._type == 'many2many': rel_table, rel_id1, rel_id2 = field._sql_names(working_table) @@ -895,10 +900,10 @@ class expression(object): return ids return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator) - ids2 = self.to_ids(cr, uid, right, relational_table, context) - dom = self.child_of_domain(cr, uid, 'id', ids2, relational_table) + ids2 = to_ids(right, relational_table, context) + dom = child_of_domain('id', ids2, relational_table) ids2 = relational_table.search(cr, uid, dom, context=context) - leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), working_table)) + push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), working_table)) else: call_null_m2m = True if right is not False: @@ -915,26 +920,27 @@ class expression(object): if operator in ['like', 'ilike', 'in', '=']: #no result found with given search criteria call_null_m2m = False - leafs_to_stack.append(self.create_substitution_leaf(leaf, FALSE_LEAF, working_table)) + push(create_substitution_leaf(leaf, FALSE_LEAF, working_table)) else: operator = 'in' # operator changed because ids are directly related to main object else: call_null_m2m = False m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), working_table)) + push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), working_table)) if call_null_m2m: m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_table)) + push(create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_table)) elif field._type == 'many2one': if operator == 'child_of': - ids2 = self.to_ids(cr, uid, right, relational_table, context) + ids2 = to_ids(right, relational_table, context) if field._obj != working_table._name: - dom = self.child_of_domain(cr, uid, left, ids2, relational_table, prefix=field._obj) + dom = child_of_domain(left, ids2, relational_table, prefix=field._obj) else: - dom = self.child_of_domain(cr, uid, 'id', ids2, working_table, parent=left) - leafs_to_stack += [self.create_substitution_leaf(leaf, dom_leaf, working_table) for dom_leaf in dom] + dom = child_of_domain('id', ids2, working_table, parent=left) + for dom_leaf in reversed(dom): + push(create_substitution_leaf(leaf, dom_leaf, working_table)) else: def _get_expression(relational_table, cr, uid, left, right, operator, context=None): if context is None: @@ -958,10 +964,10 @@ class expression(object): # resolve string-based m2o criterion into IDs if isinstance(right, basestring) or \ right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right): - leafs_to_stack.append(self.create_substitution_leaf(leaf, _get_expression(relational_table, cr, uid, left, right, operator, context=context), working_table)) + push(create_substitution_leaf(leaf, _get_expression(relational_table, cr, uid, left, right, operator, context=context), working_table)) else: # right == [] or right == False and all other cases are handled by __leaf_to_sql() - results_to_stack.append(leaf) + push_result(leaf) else: # other field type @@ -973,7 +979,7 @@ class expression(object): elif operator in ('<', '<='): right += ' 23:59:59' - leafs_to_stack.append(self.create_substitution_leaf(leaf, (left, operator, right), working_table)) + push(create_substitution_leaf(leaf, (left, operator, right), working_table)) elif field.translate: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') @@ -1008,30 +1014,15 @@ class expression(object): right, right, ] - leafs_to_stack.append(self.create_substitution_leaf(leaf, ('id', 'inselect', (subselect, params)), working_table)) + push(create_substitution_leaf(leaf, ('id', 'inselect', (subselect, params)), working_table)) else: - results_to_stack.append(leaf) - - # ---------------------------------------- - # END OF PROCESS OF CURRENT LEAF - # -> results_to_stack elements are added in result - # -> leafs_to_stack elements are inserted back in the processed - # stack to be immediately processed - # ---------------------------------------- - - leafs_to_stack.reverse() - for leaf in results_to_stack: - result.append(leaf) - for leaf in leafs_to_stack: - stack.insert(0, leaf) + push_result(leaf) # ---------------------------------------- # END OF PARSING FULL DOMAIN # ---------------------------------------- - self.result = result - # Generate joins joins = set() for leaf in self.result: @@ -1047,10 +1038,9 @@ class expression(object): assert operator in (TERM_OPERATORS + ('inselect',)), \ "Invalid operator %r in domain term %r" % (operator, leaf) assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in table._all_columns \ - or left in MAGIC_COLUMNS, \ - "Invalid field %r in domain term %r" % (left, leaf) + or left in MAGIC_COLUMNS, "Invalid field %r in domain term %r" % (left, leaf) - table_alias = '"%s"' % (eleaf._generate_alias()) + table_alias = '"%s"' % (eleaf.generate_alias()) if leaf == TRUE_LEAF: query = 'TRUE' From d588388ae4a33ee836e7ebbd0a764e3081d0bba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 6 Dec 2012 17:22:19 +0100 Subject: [PATCH 31/39] [IMP] [CLEAN] expression.parse: updated and cleaned a bit the tests. Removed print statements. bzr revid: tde@openerp.com-20121206162219-n1c48dkjghc4nu63 --- openerp/addons/base/tests/test_expression.py | 98 ++++++-------------- 1 file changed, 30 insertions(+), 68 deletions(-) diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index a8b69ac8c06..37fa24f5b1c 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -147,26 +147,23 @@ class test_expression(common.TransactionCase): b_ab = bank_obj.create(cr, uid, {'name': '__bank_test_b', 'state': bank_type[0], 'partner_id': p_ab, 'acc_number': '5678'}) b_ba = bank_obj.create(cr, uid, {'name': '__bank_test_b', 'state': bank_type[0], 'partner_id': p_ba, 'acc_number': '9876'}) - # ---------------------------------------- + # -------------------------------------------------- # Test1: basics about the attribute - # ---------------------------------------- + # -------------------------------------------------- category_id_col._auto_join = True self.assertRaises(NotImplementedError, partner_obj.search, cr, uid, [('category_id.name', '=', 'foo')]) category_id_col._auto_join = False - # ---------------------------------------- - # Test1: one2many - # ---------------------------------------- + # -------------------------------------------------- + # Test2: one2many + # -------------------------------------------------- name_test = 'test_a' # Do: one2many without _auto_join self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('bank_ids.name', 'like', name_test)]) - for query in self.query_list: - print query - print '----------------------' # Test result self.assertEqual(set(partner_ids), set([p_aa]), "_auto_join off: ('bank_ids.name', 'like', '..'): incorrect result") @@ -191,9 +188,6 @@ class test_expression(common.TransactionCase): # Do: cascaded one2many without _auto_join self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('child_ids.bank_ids.id', 'in', [b_aa, b_ba])]) - for query in self.query_list: - print query - print '----------------------' # Test result self.assertEqual(set(partner_ids), set([p_a, p_b]), "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]): incorrect result") @@ -205,22 +199,20 @@ class test_expression(common.TransactionCase): partner_bank_ids_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('bank_ids.name', 'like', 'test_a')]) - # for query in self.query_list: - # print query # Test result self.assertEqual(set(partner_ids), set([p_aa]), "_auto_join on: ('bank_ids.name', 'like', '..') incorrect result") - # # Test produced queries + # Test produced queries self.assertEqual(len(self.query_list), 1, "_auto_join on: ('bank_ids.name', 'like', '..') should produce 1 query") sql_query = self.query_list[0].get_sql() self.assertIn('"res_partner"', sql_query[0], "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect main table") - self.assertIn('"res_partner_bank" as res_partner__bank_ids', sql_query[0], + self.assertIn('"res_partner_bank" as "res_partner__bank_ids"', sql_query[0], "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect join") self.assertIn('"res_partner__bank_ids"."name" like %s', sql_query[1], "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect where condition") - self.assertIn('res_partner."id"=res_partner__bank_ids."partner_id"', sql_query[1], + self.assertIn('"res_partner"."id"="res_partner__bank_ids"."partner_id"', sql_query[1], "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect join condition") self.assertEqual(set(['%' + name_test + '%']), set(sql_query[2]), "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect parameter") @@ -228,24 +220,17 @@ class test_expression(common.TransactionCase): # Do: one2many with _auto_join, test final leaf is an id self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('bank_ids.id', 'in', [b_aa, b_ab])]) - for query in self.query_list: - print query - print '----------------------' # Test result self.assertEqual(set(partner_ids), set([p_aa, p_ab]), "_auto_join on: ('bank_ids.id', 'in', [..]) incorrect result") - # # Test produced queries + # Test produced queries self.assertEqual(len(self.query_list), 1, "_auto_join on: ('bank_ids.id', 'in', [..]) should produce 1 query") sql_query = self.query_list[0].get_sql() self.assertIn('"res_partner"', sql_query[0], "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect main table") - self.assertIn('"res_partner_bank" as res_partner__bank_ids', sql_query[0], - "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect join") self.assertIn('"res_partner__bank_ids"."id" in (%s,%s)', sql_query[1], "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect where condition") - self.assertIn('res_partner."id"=res_partner__bank_ids."partner_id"', sql_query[1], - "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect join condition") self.assertEqual(set([b_aa, b_ab]), set(sql_query[2]), "_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect parameter") @@ -253,9 +238,6 @@ class test_expression(common.TransactionCase): partner_child_ids_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('child_ids.bank_ids.id', 'in', [b_aa, b_ba])]) - for query in self.query_list: - print query - print '----------------------' # Test result self.assertEqual(set(partner_ids), set([p_a, p_b]), "_auto_join on: ('child_ids.bank_ids.id', 'not in', [..]): incorrect result") @@ -265,31 +247,28 @@ class test_expression(common.TransactionCase): sql_query = self.query_list[0].get_sql() self.assertIn('"res_partner"', sql_query[0], "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) incorrect main table") - self.assertIn('"res_partner" as res_partner__child_ids', sql_query[0], + self.assertIn('"res_partner" as "res_partner__child_ids"', sql_query[0], "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join") - self.assertIn('"res_partner_bank" as res_partner__child_ids__bank_ids', sql_query[0], + self.assertIn('"res_partner_bank" as "res_partner__child_ids__bank_ids"', sql_query[0], "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join") self.assertIn('"res_partner__child_ids__bank_ids"."id" in (%s,%s)', sql_query[1], "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect where condition") - self.assertIn('res_partner."id"=res_partner__child_ids."parent_id"', sql_query[1], + self.assertIn('"res_partner"."id"="res_partner__child_ids"."parent_id"', sql_query[1], "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join condition") - self.assertIn('res_partner__child_ids."id"=res_partner__child_ids__bank_ids."partner_id"', sql_query[1], + self.assertIn('"res_partner__child_ids"."id"="res_partner__child_ids__bank_ids"."partner_id"', sql_query[1], "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join condition") self.assertEqual(set([b_aa, b_ba]), set(sql_query[2]), "_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect parameter") - # ---------------------------------------- - # Test2: many2one - # ---------------------------------------- + # -------------------------------------------------- + # Test3: many2one + # -------------------------------------------------- name_test = 'US' # Do: many2one without _auto_join self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) - for query in self.query_list: - print query - print '----------------------' # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), "_auto_join off: ('state_id.country_id.code', 'like', '..') incorrect result") @@ -301,9 +280,6 @@ class test_expression(common.TransactionCase): partner_state_id_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) - for query in self.query_list: - print query - print '----------------------' # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') incorrect result") @@ -320,11 +296,11 @@ class test_expression(common.TransactionCase): sql_query = self.query_list[1].get_sql() self.assertIn('"res_partner"', sql_query[0], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect main table") - self.assertIn('"res_country_state" as res_partner__state_id', sql_query[0], + self.assertIn('"res_country_state" as "res_partner__state_id"', sql_query[0], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect join") self.assertIn('"res_partner__state_id"."country_id" in (%s)', sql_query[1], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect where condition") - self.assertIn('res_partner."state_id"=res_partner__state_id."id"', sql_query[1], + self.assertIn('"res_partner"."state_id"="res_partner__state_id"."id"', sql_query[1], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect join condition") # Do: many2one with 1 _auto_join on the second many2one @@ -332,9 +308,6 @@ class test_expression(common.TransactionCase): state_country_id_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) - for query in self.query_list: - print query - print '----------------------' # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') incorrect result") @@ -345,11 +318,11 @@ class test_expression(common.TransactionCase): sql_query = self.query_list[0].get_sql() self.assertIn('"res_country_state"', sql_query[0], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect main table") - self.assertIn('"res_country" as res_country_state__country_id', sql_query[0], + self.assertIn('"res_country" as "res_country_state__country_id"', sql_query[0], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect join") self.assertIn('"res_country_state__country_id"."code" like %s', sql_query[1], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect where condition") - self.assertIn('res_country_state."country_id"=res_country_state__country_id."id"', sql_query[1], + self.assertIn('"res_country_state"."country_id"="res_country_state__country_id"."id"', sql_query[1], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect join condition") self.assertEqual(['%' + name_test + '%'], sql_query[2], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect parameter") @@ -365,8 +338,6 @@ class test_expression(common.TransactionCase): state_country_id_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)]) - for query in self.query_list: - print query # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)), "_auto_join on: ('state_id.country_id.code', 'like', '..') incorrect result") @@ -376,22 +347,22 @@ class test_expression(common.TransactionCase): sql_query = self.query_list[0].get_sql() self.assertIn('"res_partner"', sql_query[0], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect main table") - self.assertIn('"res_country_state" as res_partner__state_id', sql_query[0], + self.assertIn('"res_country_state" as "res_partner__state_id"', sql_query[0], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join") - self.assertIn('"res_country" as res_partner__state_id__country_id', sql_query[0], + self.assertIn('"res_country" as "res_partner__state_id__country_id"', sql_query[0], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join") self.assertIn('"res_partner__state_id__country_id"."code" like %s', sql_query[1], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect where condition") - self.assertIn('res_partner."state_id"=res_partner__state_id."id"', sql_query[1], + self.assertIn('"res_partner"."state_id"="res_partner__state_id"."id"', sql_query[1], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join condition") - self.assertIn('res_partner__state_id."country_id"=res_partner__state_id__country_id."id"', sql_query[1], + self.assertIn('"res_partner__state_id"."country_id"="res_partner__state_id__country_id"."id"', sql_query[1], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join condition") self.assertEqual(['%' + name_test + '%'], sql_query[2], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect parameter") - # ---------------------------------------- - # Test3: domain attribute on one2many fields - # ---------------------------------------- + # -------------------------------------------------- + # Test4: domain attribute on one2many fields + # -------------------------------------------------- partner_child_ids_col._auto_join = True partner_bank_ids_col._auto_join = True @@ -400,10 +371,6 @@ class test_expression(common.TransactionCase): # Do: 2 cascaded one2many with _auto_join, test final leaf is an id self._reinit_mock() partner_ids = partner_obj.search(cr, uid, ['&', (1, '=', 1), ('child_ids.bank_ids.id', 'in', [b_aa, b_ba])]) - for query in self.query_list: - print query - print partner_ids - print '--------------------' # Test result: at least one of our added data self.assertTrue(set([p_a]).issubset(set(partner_ids)), "_auto_join on one2many with domains incorrect result") @@ -414,7 +381,7 @@ class test_expression(common.TransactionCase): self.assertIn('"res_partner__child_ids__bank_ids"."acc_number" like %s', sql_query[1], "_auto_join on one2many with domains incorrect result") # TDE TODO: check first domain has a correct table name - self.assertIn('"res_partner__child_ids__bank_ids"."acc_number" like %s', sql_query[1], + self.assertIn('"res_partner__child_ids"."name" = %s', sql_query[1], "_auto_join on one2many with domains incorrect result") partner_child_ids_col._domain = lambda self: [('name', '=', '__%s' % self._name)] @@ -425,7 +392,7 @@ class test_expression(common.TransactionCase): "_auto_join on one2many with domains incorrect result") # ---------------------------------------- - # Test4: result-based tests + # Test5: result-based tests # ---------------------------------------- partner_bank_ids_col._auto_join = False @@ -439,9 +406,6 @@ class test_expression(common.TransactionCase): # Do: ('child_ids.state_id.country_id.code', 'like', '..') without _auto_join self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('child_ids.state_id.country_id.code', 'like', name_test)]) - for query in self.query_list: - print query - print '----------------------' # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b]).issubset(set(partner_ids)), "_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result") @@ -455,9 +419,6 @@ class test_expression(common.TransactionCase): state_country_id_col._auto_join = True self._reinit_mock() partner_ids = partner_obj.search(cr, uid, [('child_ids.state_id.country_id.code', 'like', name_test)]) - for query in self.query_list: - print query - print '----------------------' # Test result: at least our added data + demo data self.assertTrue(set([p_a, p_b]).issubset(set(partner_ids)), "_auto_join on: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result") @@ -469,6 +430,7 @@ class test_expression(common.TransactionCase): partner_bank_ids_col._auto_join = False partner_child_ids_col._auto_join = False partner_state_id_col._auto_join = False + partner_parent_id_col._auto_join = False state_country_id_col._auto_join = False BaseModel._where_calc = self._base_model_where_calc From e36b44e82fd10f87fae2840630ee533478533cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 7 Dec 2012 12:54:24 +0100 Subject: [PATCH 32/39] [IMP] [WIP] order_by: now using aliases in _generate_order_by, _generate_o2m_order_by, _inherits_join_add, ... added some tests. Next comits will clean a bit the code, because currently it is a bit messy. bzr revid: tde@openerp.com-20121207115424-x8gkjcqpi8dz96g2 --- openerp/addons/base/tests/test_expression.py | 1 - openerp/addons/base/tests/test_search.py | 49 ++++-- openerp/osv/expression.py | 22 +-- openerp/osv/orm.py | 78 ++++++--- openerp/osv/query.py | 159 +++++++++++++++++-- openerp/test/test_osv.py | 12 +- 6 files changed, 253 insertions(+), 68 deletions(-) diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index 37fa24f5b1c..c43dda85e9b 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -432,7 +432,6 @@ class test_expression(common.TransactionCase): partner_state_id_col._auto_join = False partner_parent_id_col._auto_join = False state_country_id_col._auto_join = False - BaseModel._where_calc = self._base_model_where_calc if __name__ == '__main__': unittest2.main() diff --git a/openerp/addons/base/tests/test_search.py b/openerp/addons/base/tests/test_search.py index dc554408667..a64bfcb3e94 100644 --- a/openerp/addons/base/tests/test_search.py +++ b/openerp/addons/base/tests/test_search.py @@ -2,16 +2,17 @@ import unittest2 import openerp.tests.common as common -class test_expression(common.TransactionCase): - def test_search_order(self): +class test_search(common.TransactionCase): + + def test_00_search_order(self): registry, cr, uid = self.registry, self.cr, self.uid - # Create 6 partners with a given name, and a given creation order to - # ensure the order of their ID. Some are set as unactive to verify they - # are by default excluded from the searches and to provide a second - # `order` argument. + # Create 6 partners with a given name, and a given creation order to + # ensure the order of their ID. Some are set as unactive to verify they + # are by default excluded from the searches and to provide a second + # `order` argument. partners = registry('res.partner') c = partners.create(cr, uid, {'name': 'test_search_order_C'}) @@ -23,9 +24,9 @@ class test_expression(common.TransactionCase): # The tests. - # The basic searches should exclude records that have active = False. - # The order of the returned ids should be given by the `order` - # parameter of search(). + # The basic searches should exclude records that have active = False. + # The order of the returned ids should be given by the `order` + # parameter of search(). name_asc = partners.search(cr, uid, [('name', 'like', 'test_search_order%')], order="name asc") self.assertEqual([a, ab, b, c], name_asc, "Search with 'NAME ASC' order failed.") @@ -36,9 +37,9 @@ class test_expression(common.TransactionCase): id_desc = partners.search(cr, uid, [('name', 'like', 'test_search_order%')], order="id desc") self.assertEqual([ab, b, a, c], id_desc, "Search with 'ID DESC' order failed.") - # The inactive records shouldn't be excluded as soon as a condition on - # that field is present in the domain. The `order` parameter of - # search() should support any legal coma-separated values. + # The inactive records shouldn't be excluded as soon as a condition on + # that field is present in the domain. The `order` parameter of + # search() should support any legal coma-separated values. active_asc_id_asc = partners.search(cr, uid, [('name', 'like', 'test_search_order%'), '|', ('active', '=', True), ('active', '=', False)], order="active asc, id asc") self.assertEqual([d, e, c, a, b, ab], active_asc_id_asc, "Search with 'ACTIVE ASC, ID ASC' order failed.") @@ -57,4 +58,28 @@ class test_expression(common.TransactionCase): id_desc_active_desc = partners.search(cr, uid, [('name', 'like', 'test_search_order%'), '|', ('active', '=', True), ('active', '=', False)], order="id desc, active desc") self.assertEqual([e, ab, b, a, d, c], id_desc_active_desc, "Search with 'ID DESC, ACTIVE DESC' order failed.") + def test_10_m2order(self): + registry, cr, uid = self.registry, self.cr, self.uid + users_obj = registry('res.users') + # Find Employee group + group_employee_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user') + group_employee_id = group_employee_ref and group_employee_ref[1] or False + + search_user = users_obj.create(cr, uid, {'name': '__search', 'login': '__search', 'groups_id': [(6, 0, [group_employee_id])]}) + a = users_obj.create(cr, uid, {'name': '__test_A', 'login': '__z_test_A'}) + b = users_obj.create(cr, uid, {'name': '__test_B', 'login': '__a_test_B'}) + + # Do: search on res.users, order on a field on res.partner, then res.users + print '\n\n' + user_ids = users_obj.search(cr, search_user, [], order='name asc, login desc') + self.assertTrue(set([search_user, a, b]).issubset(set(user_ids)), 'cacaprout') + + # DO: order on a many2one + print '\n\n' + user_ids = users_obj.search(cr, search_user, [], order='state_id asc, country_id desc, name asc, login desc') + print user_ids[0:25] + # self.assertTrue(set([search_user, a, b]).issubset(set(user_ids)), 'cacaprout') + +if __name__ == '__main__': + unittest2.main() diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 8967d9f254a..526b09ccda2 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -319,16 +319,18 @@ def _quote(to_quote): return to_quote -def generate_table_alias(src_model, joined_tables=[]): +def generate_table_alias(src_table_alias, joined_tables=[]): """ Generate a standard table alias name. An alias is generated as following: - - the base is the source model table name + - the base is the source table name (that can already be an alias) - then, each joined table is added in the alias using a 'link field name' that is used to render unique aliases for a given path + - returns a tuple composed of the alias, and the full table alias to be + added in a from condition Examples: - - src_model=res.users, join_tables=[]: - alias = '"res_users"' - - src_model=res.users, join_tables=[(res.partner, 'parent_id')] - alias = '"res_partner" as "res_users__parent_id"' + - src_table_alias='res_users', join_tables=[]: + alias = ('res_users','"res_users"') + - src_model='res_users', join_tables=[(res.partner, 'parent_id')] + alias = ('res_users__parent_id', '"res_partner" as "res_users__parent_id"') :param model src_model: model source of the alias :param list join_tables: list of tuples @@ -336,12 +338,12 @@ def generate_table_alias(src_model, joined_tables=[]): :return tuple: (table alias, alias statement for from clause with quotes added) """ - alias = src_model._table + alias = src_table_alias if not joined_tables: return ('%s' % alias, '%s' % _quote(alias)) for link in joined_tables: alias += '__' + link[1] - return ('%s' % alias, '%s as %s' % (_quote(joined_tables[-1][0]._table), _quote(alias))) + return ('%s' % alias, '%s as %s' % (_quote(joined_tables[-1][0]), _quote(alias))) def normalize_leaf(element): @@ -499,8 +501,8 @@ class ExtendedLeaf(object): # -------------------------------------------------- def generate_alias(self): - links = [(context[1], context[4]) for context in self.context_stack] - alias, alias_statement = generate_table_alias(self._tables[0], links) + links = [(context[1]._table, context[4]) for context in self.context_stack] + alias, alias_statement = generate_table_alias(self._tables[0]._table, links) return alias def add_join_context(self, table, lhs_col, table_col, link): diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index a08126d12fc..d2c0a0f5682 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -58,7 +58,6 @@ import types import psycopg2 from lxml import etree -import warnings import fields import openerp @@ -2720,20 +2719,23 @@ class BaseModel(object): return result - def _inherits_join_add(self, current_table, parent_model_name, query): + def _inherits_join_add(self, current_model, parent_model_name, query): """ Add missing table SELECT and JOIN clause to ``query`` for reaching the parent table (no duplicates) - :param current_table: current model object + :param current_model: current model object :param parent_model_name: name of the parent model for which the clauses should be added :param query: query object on which the JOIN should be added """ - inherits_field = current_table._inherits[parent_model_name] + inherits_field = current_model._inherits[parent_model_name] parent_model = self.pool.get(parent_model_name) - parent_table_name = parent_model._table - quoted_parent_table_name = '"%s"' % parent_table_name - if quoted_parent_table_name not in query.tables: - query.tables.append(quoted_parent_table_name) - query.where_clause.append('(%s.%s = %s.id)' % (current_table._table, inherits_field, parent_table_name)) + parent_alias = query.add_implicit_join((current_model, parent_model, inherits_field, 'id', inherits_field)) + return parent_alias + # table_alias = expression.generate_table_alias(current_model, [(parent_model, inherits_field)]) + # print '\t... _inherits_join_add trying to add %s in %s' % (table_alias, query.tables) + # # query.add_table(table_alias) + # if table_alias not in query.tables: + # query.tables.append(table_alias) + # query.where_clause.append('("%s".%s = %s.id)' % (current_model._table, inherits_field, table_alias)) def _inherits_join_calc(self, field, query): """ @@ -2744,13 +2746,15 @@ class BaseModel(object): :param query: query object on which the JOIN should be added :return: qualified name of field, to be used in SELECT clause """ + print '\t--> _inherits_join_calc' current_table = self + parent_alias = current_table._table while field in current_table._inherit_fields and not field in current_table._columns: parent_model_name = current_table._inherit_fields[field][0] parent_table = self.pool.get(parent_model_name) - self._inherits_join_add(current_table, parent_model_name, query) + parent_alias = self._inherits_join_add(current_table, parent_model_name, query) current_table = parent_table - return '"%s".%s' % (current_table._table, field) + return '%s."%s"' % (parent_alias, field) def _parent_store_compute(self, cr): if not self._parent_store: @@ -4659,28 +4663,49 @@ class BaseModel(object): :param query: the current query object """ def apply_rule(added_clause, added_params, added_tables, parent_model=None, child_object=None): + """ :param string parent_model: string of the parent model + :param model child_object: model object, base of the rule application + """ if added_clause: + print '--> _apply_ir_rules.apply_rule,', added_clause, added_params, added_tables, parent_model, child_object if parent_model and child_object: + print '\t... calling inherits-Join_add with parent_model %s, child_object %s' % (parent_model, child_object) # as inherited rules are being applied, we need to add the missing JOIN # to reach the parent table (if it was not JOINed yet in the query) - child_object._inherits_join_add(child_object, parent_model, query) + parent_alias = child_object._inherits_join_add(child_object, parent_model, query) + # inherited rules are applied on the external table -> need to get the alias and replace + parent_table = self.pool.get(parent_model)._table + print parent_table, parent_alias + added_clause = [clause.replace('"%s"' % parent_table, '"%s"' % parent_alias) for clause in added_clause] + # not sure of myself here (in the ORM, this statment is quite cool) + new_tables = [] + for table in added_tables: + if table == '"%s"' % (parent_table): + new_table = '"%s" as "%s"' % (parent_table, parent_alias) + else: + new_table = table.replace('"%s"' % parent_table, '"%s"' % parent_alias) + new_tables.append(new_table) + added_tables = new_tables + print '\t... adding tables %s, clause %s (params %s)' % (added_tables, added_clause, added_params) query.where_clause += added_clause query.where_clause_params += added_params for table in added_tables: - quoted_table_name = '%s' % (table) - if quoted_table_name not in query.tables: - query.tables.append(quoted_table_name) + print '\t... adding table %s in %s' % (table, query.tables) + if table not in query.tables: + query.tables.append(table) return True return False # apply main rules on the object rule_obj = self.pool.get('ir.rule') - apply_rule(*rule_obj.domain_get(cr, uid, self._name, mode, context=context)) + rule_where_clause, rule_where_clause_params, rule_tables = rule_obj.domain_get(cr, uid, self._name, mode, context=context) + apply_rule(rule_where_clause, rule_where_clause_params, rule_tables) # apply ir.rules from the parents (through _inherits) for inherited_model in self._inherits: - kwargs = dict(parent_model=inherited_model, child_object=self) #workaround for python2.5 - apply_rule(*rule_obj.domain_get(cr, uid, inherited_model, mode, context=context), **kwargs) + rule_where_clause, rule_where_clause_params, rule_tables = rule_obj.domain_get(cr, uid, inherited_model, mode, context=context) + apply_rule(rule_where_clause, rule_where_clause_params, rule_tables, + parent_model=inherited_model, child_object=self) def _generate_m2o_order_by(self, order_field, query): """ @@ -4690,6 +4715,7 @@ class BaseModel(object): :return: the qualified field name to use in an ORDER BY clause to sort by ``order_field`` """ + print '_generate_m2o_order_by' if order_field not in self._columns and order_field in self._inherit_fields: # also add missing joins for reaching the table containing the m2o field qualified_field = self._inherits_join_calc(order_field, query) @@ -4715,17 +4741,16 @@ class BaseModel(object): # extract the field names, to be able to qualify them and add desc/asc m2o_order_list = [] for order_part in m2o_order.split(","): - m2o_order_list.append(order_part.strip().split(" ",1)[0].strip()) + m2o_order_list.append(order_part.strip().split(" ", 1)[0].strip()) m2o_order = m2o_order_list # 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 - src_table, src_field = qualified_field.replace('"','').split('.', 1) - query.join((src_table, dest_model._table, src_field, 'id'), outer=True) - qualify = lambda field: '"%s"."%s"' % (dest_model._table, field) + src_table, src_field = qualified_field.replace('"', '').split('.', 1) + dst_alias = query.add_join((src_table, dest_model._table, src_field, 'id'), outer=True) + qualify = lambda field: '"%s"."%s"' % (dst_alias, field) return map(qualify, m2o_order) if isinstance(m2o_order, list) else qualify(m2o_order) - def _generate_order_by(self, order_spec, query): """ Attempt to consruct an appropriate ORDER BY clause based on order_spec, which must be @@ -4742,9 +4767,9 @@ class BaseModel(object): subelems.append(' ') order_list.append('"%s"."%s" %s' % (table, subelems[0], subelems[1])) return order_list - order_by_clause = ','.join(_split_order(self._order, self._table)) if order_spec: + print '-->_generate_order_by beginning' order_by_elements = [] self._check_qorder(order_spec) for order_part in order_spec.split(','): @@ -4761,7 +4786,7 @@ class BaseModel(object): elif order_column._type == 'many2one': inner_clause = self._generate_m2o_order_by(order_field, query) else: - continue # ignore non-readable or "non-joinable" fields + continue # ignore non-readable or "non-joinable" fields elif order_field in self._inherit_fields: parent_obj = self.pool.get(self._inherit_fields[order_field][3]) order_column = parent_obj._columns[order_field] @@ -4770,7 +4795,7 @@ class BaseModel(object): elif order_column._type == 'many2one': inner_clause = self._generate_m2o_order_by(order_field, query) else: - continue # ignore non-readable or "non-joinable" fields + continue # ignore non-readable or "non-joinable" fields if inner_clause: if isinstance(inner_clause, list): for clause in inner_clause: @@ -4779,6 +4804,7 @@ class BaseModel(object): order_by_elements.append("%s %s" % (inner_clause, order_direction)) if order_by_elements: order_by_clause = ",".join(order_by_elements) + print '-->_generate_order_by ending' return order_by_clause and (' ORDER BY %s ' % order_by_clause) or '' diff --git a/openerp/osv/query.py b/openerp/osv/query.py index d32ea73b29d..a07def36a31 100644 --- a/openerp/osv/query.py +++ b/openerp/osv/query.py @@ -21,12 +21,21 @@ #.apidoc title: Query object + def _quote(to_quote): if '"' not in to_quote: return '"%s"' % to_quote return to_quote +def _get_alias_from_statement(string): + if len(string.split(' as ')) > 1: + alias = string.split(' as ')[1].replace('"', '') + else: + alias = string.replace('"', '') + return alias + + class Query(object): """ Dumb implementation of a Query object, using 3 string lists so far @@ -44,6 +53,9 @@ class Query(object): # holds the list of tables joined using default JOIN. # the table names are stored double-quoted (backwards compatibility) self.tables = tables or [] + # holds a mapping of table aliases: + # self._table_alias_mapping = {'alias_1': 'table_name'} + self._table_alias_mapping = {} # holds the list of WHERE clause elements, to be joined with # 'AND' when generating the final query @@ -67,7 +79,110 @@ class Query(object): # LEFT JOIN "table_c" ON ("table_a"."table_a_col2" = "table_c"."table_c_col") self.joins = joins or {} - def join(self, connection, outer=False): + def _add_table_alias(self, table_alias): + pass + + def _get_table_aliases(self): + aliases = [] + for table in self.tables: + if len(table.split(' as ')) > 1: + aliases.append(table.split(' as ')[1].replace('"', '')) + else: + aliases.append(table.replace('"', '')) + # print '--', aliases + return aliases + + def _get_alias_mapping(self): + mapping = {} + aliases = self._get_table_aliases() + for alias in aliases: + for table in self.tables: + if '"%s"' % (alias) in table: + mapping.setdefault(alias, table) + return mapping + + def add_new_join(self, connection, implicit=True, outer=False): + """ Join a destination table to the current table. + + :param implicit: False if the join is an explicit join. This allows + to fall back on the previous implementation of ``join`` before + OpenERP 7.0. It therefore adds the JOIN specified in ``connection`` + If True, the join is done implicitely, by adding the table alias + in the from clause and the join condition in the where clause + of the query. + :param connection: a tuple ``(lhs, table, lhs_col, col, link)``. + The join corresponds to the SQL equivalent of:: + + (lhs.lhs_col = table.col) + + Note that all connection elements are strings. Please refer to expression.py for more details about joins. + + :param outer: True if a LEFT OUTER JOIN should be used, if possible + (no promotion to OUTER JOIN is supported in case the JOIN + was already present in the query, as for the moment + implicit INNER JOINs are only connected from NON-NULL + columns so it would not be correct (e.g. for + ``_inherits`` or when a domain criterion explicitly + adds filtering) + """ + from openerp.osv.expression import generate_table_alias + (lhs, table, lhs_col, col, link) = connection + alias, alias_statement = generate_table_alias(lhs._table, [(table._table, link)]) + + if implicit: + print '\t\t... Query: trying to add %s in %s (received %s)' % (alias_statement, self.tables, connection) + if alias_statement not in self.tables: + self.tables.append(alias_statement) + condition = '("%s"."%s" = "%s"."%s")' % (lhs._table, lhs_col, alias, col) + print '\t\t... added %s' % (condition) + self.where_clause.append(condition) + return alias + else: + (lhs, table, lhs_col, col) = connection + lhs = _quote(lhs) + table = _quote(table) + print connection + aliases = [] + for table in self.tables: + if len(table.split(' as ')) > 1: + aliases.append(table.split(' as ')[1]) + else: + aliases.append(table) + print '--', aliases + aliases = [table.split(' as ') for table in self.tables] + assert lhs in self.aliases, "Left-hand-side table %s must already be part of the query tables %s!" % (lhs, str(self.tables)) + if table in self.tables: + # already joined, must ignore (promotion to outer and multiple joins not supported yet) + pass + else: + # add JOIN + self.tables.append(table) + self.joins.setdefault(lhs, []).append((table, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN')) + return self + + def add_implicit_join(self, connection): + """ Adds an implicit join. This means that left-hand table is added to the + Query.tables (adding a table in the from clause), and that a join + condition is added in Query.where_clause. + + Implicit joins use expression.generate_table_alias to generate the + alias the the joined table. + + :param connection: a tuple``(lhs, table, lhs_col, col, link)`` Please + refer to expression.py for more details about joins. + """ + from openerp.osv.expression import generate_table_alias + (lhs, table, lhs_col, col, link) = connection + alias, alias_statement = generate_table_alias(lhs._table, [(table._table, link)]) + print '\t\t... Query: trying to add %s in %s (received %s)' % (alias_statement, self.tables, connection) + if alias_statement not in self.tables: + self.tables.append(alias_statement) + condition = '("%s"."%s" = "%s"."%s")' % (lhs._table, lhs_col, alias, col) + print '\t\t... added %s' % (condition) + self.where_clause.append(condition) + return alias + + def add_join(self, connection, outer=False): """Adds the JOIN specified in ``connection``. :param connection: a tuple ``(lhs, table, lhs_col, col)``. @@ -75,6 +190,8 @@ class Query(object): (lhs.lhs_col = table.col) + Note that all connection elements are strings. + :param outer: True if a LEFT OUTER JOIN should be used, if possible (no promotion to OUTER JOIN is supported in case the JOIN was already present in the query, as for the moment @@ -83,36 +200,52 @@ class Query(object): ``_inherits`` or when a domain criterion explicitly adds filtering) """ + from openerp.osv.expression import generate_table_alias (lhs, table, lhs_col, col) = connection - lhs = _quote(lhs) - table = _quote(table) - assert lhs in self.tables, "Left-hand-side table must already be part of the query!" - if table in self.tables: + # lhs = _quote(lhs) + # table = _quote(table) + print '\t\t... Query.add_join(): adding connection %s' % str(connection) + + aliases = self._get_table_aliases() + + assert lhs in aliases, "Left-hand-side table %s must already be part of the query tables %s!" % (lhs, str(self.tables)) + + rhs, rhs_statement = generate_table_alias(lhs, [(connection[1], connection[2])]) + print rhs, rhs_statement + + if rhs_statement in self.tables: # already joined, must ignore (promotion to outer and multiple joins not supported yet) pass else: # add JOIN - self.tables.append(table) - self.joins.setdefault(lhs, []).append((table, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN')) - return self + self.tables.append(rhs_statement) + self.joins.setdefault(lhs, []).append((rhs, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN')) + return rhs def get_sql(self): """Returns (query_from, query_where, query_params)""" query_from = '' tables_to_process = list(self.tables) + alias_mapping = self._get_alias_mapping() + + # print 'tables_to_process %s' % (tables_to_process) + # print 'self.joins %s' % (self.joins) + # print 'alias_mapping %s' % (alias_mapping) + def add_joins_for_table(table, query_from): for (dest_table, lhs_col, col, join) in self.joins.get(table, []): - tables_to_process.remove(dest_table) - query_from += ' %s %s ON (%s."%s" = %s."%s")' % \ - (join, dest_table, table, lhs_col, dest_table, col) + # print dest_table + tables_to_process.remove(alias_mapping[dest_table]) + query_from += ' %s %s ON ("%s"."%s" = "%s"."%s")' % \ + (join, alias_mapping[dest_table], table, lhs_col, dest_table, col) query_from = add_joins_for_table(dest_table, query_from) return query_from for table in tables_to_process: query_from += table - if table in self.joins: - query_from = add_joins_for_table(table, query_from) + if _get_alias_from_statement(table) in self.joins: + query_from = add_joins_for_table(_get_alias_from_statement(table), query_from) query_from += ',' query_from = query_from[:-1] # drop last comma return (query_from, " AND ".join(self.where_clause), self.where_clause_params) diff --git a/openerp/test/test_osv.py b/openerp/test/test_osv.py index d3ff75c03d5..2db9cec1cbd 100644 --- a/openerp/test/test_osv.py +++ b/openerp/test/test_osv.py @@ -28,8 +28,8 @@ class QueryTestCase(unittest.TestCase): query = Query() query.tables.extend(['"product_product"','"product_template"']) query.where_clause.append("product_product.template_id = product_template.id") - query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join - query.join(("product_product", "res_user", "user_id", "id"), outer=True) # outer join + query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join + query.add_join(("product_product", "res_user", "user_id", "id"), outer=True) # outer join self.assertEquals(query.get_sql()[0].strip(), """"product_product" LEFT JOIN "res_user" ON ("product_product"."user_id" = "res_user"."id"),"product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") """.strip()) self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip()) @@ -38,8 +38,8 @@ class QueryTestCase(unittest.TestCase): query = Query() query.tables.extend(['"product_product"','"product_template"']) query.where_clause.append("product_product.template_id = product_template.id") - query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join - query.join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join + query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join + query.add_join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join self.assertEquals(query.get_sql()[0].strip(), """"product_product","product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") LEFT JOIN "res_user" ON ("product_category"."user_id" = "res_user"."id")""".strip()) self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip()) @@ -48,8 +48,8 @@ class QueryTestCase(unittest.TestCase): query = Query() query.tables.extend(['"product_product"','"product_template"']) query.where_clause.append("product_product.template_id = product_template.id") - query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join - query.join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join + query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join + query.add_join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join query.tables.append('"account.account"') query.where_clause.append("product_category.expense_account_id = account_account.id") # additional implicit join self.assertEquals(query.get_sql()[0].strip(), From 390ff1540fb8eef5228e10a74d9aa2c88daa886a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 7 Dec 2012 13:37:27 +0100 Subject: [PATCH 33/39] [CLEAN] expression.py: cleaned and added comments, var names (table -> model notably). bzr revid: tde@openerp.com-20121207123727-md65da41863y827e --- openerp/osv/expression.py | 331 ++++++++++++++++++++------------------ 1 file changed, 173 insertions(+), 158 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 526b09ccda2..703b7503387 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -325,7 +325,7 @@ def generate_table_alias(src_table_alias, joined_tables=[]): - then, each joined table is added in the alias using a 'link field name' that is used to render unique aliases for a given path - returns a tuple composed of the alias, and the full table alias to be - added in a from condition + added in a from condition with quoting done Examples: - src_table_alias='res_users', join_tables=[]: alias = ('res_users','"res_users"') @@ -336,7 +336,7 @@ def generate_table_alias(src_table_alias, joined_tables=[]): :param list join_tables: list of tuples (dst_model, link_field) - :return tuple: (table alias, alias statement for from clause with quotes added) + :return tuple: (table_alias, alias statement for from clause with quotes added) """ alias = src_table_alias if not joined_tables: @@ -366,11 +366,10 @@ def normalize_leaf(element): def is_operator(element): - """Test whether an object is a valid domain operator. """ + """ Test whether an object is a valid domain operator. """ return isinstance(element, basestring) and element in DOMAIN_OPERATORS -# TODO change the share wizard to use this function. def is_leaf(element, internal=False): """ Test whether an object is a valid domain term: - is a list or tuple @@ -380,6 +379,8 @@ def is_leaf(element, internal=False): :param tuple element: a leaf in form (left, operator, right) :param boolean internal: allow or not the 'inselect' internal operator in the term. This should be always left to False. + + Note: OLD TODO change the share wizard to use this function. """ INTERNAL_OPS = TERM_OPERATORS + ('<>',) if internal: @@ -421,39 +422,10 @@ def select_distinct_from_where_not_null(cr, select_field, from_table): # ------------------------------------------------- class ExtendedLeaf(object): - - def __init__(self, leaf, table, context_stack=None): - """ Initialize the ExtendedLeaf - - :attr [string, tuple] leaf: operator or tuple-formatted domain - expression - :attr object table: table object - :attr list _tables: list of chained table objects, updated when - adding joins - :attr tuple elements: manipulation-friendly leaf - :attr object field: field obj, taken from table, not necessarily - found (inherits, 'id') - :attr list field_path: exploded left of elements - (partner_id.name -> ['partner_id', 'name']) - :attr object relational_table: distant table for relational fields - """ - assert table, 'Invalid leaf creation without table' - self.context_stack = context_stack or [] - # validate the leaf - self.leaf = leaf - # normalize the leaf's operator - self.normalize_leaf() - # set working variables; handle the context stack and previous tables - self.table = table - self._tables = [] - for item in self.context_stack: - self._tables.append(item[0]) - self._tables.append(table) - # check validity - self.check_leaf() - - def __str__(self): - return '' % (str(self.leaf), self.table._table, ','.join(self._get_context_debug())) + """ Class wrapping a domain leaf, and giving some services and management + features on it. In particular it managed join contexts to be able to + construct queries through multiple models. + """ # -------------------------------------------------- # Join / Context manipulation @@ -500,32 +472,61 @@ class ExtendedLeaf(object): # i.e.: many2one: 'state_id': current field name # -------------------------------------------------- + def __init__(self, leaf, model, join_context=None): + """ Initialize the ExtendedLeaf + + :attr [string, tuple] leaf: operator or tuple-formatted domain + expression + :attr obj model: current working model + :attr list _models: list of chained models, updated when + adding joins + :attr list join_context: list of join contexts. This is a list of + tuples like ``(lhs, table, lhs_col, col, link)`` + :param obj lhs: source (left hand) model + :param obj model: destination (right hand) model + :param string lhs_col: source model column for join condition + :param string col: destination model column for join condition + :param link: link column between source and destination model + that is not necessarily (but generally) a real column used + in the condition (i.e. in many2one); this link is used to + compute aliases + """ + assert model, 'Invalid leaf creation without table' + self.join_context = join_context or [] + self.leaf = leaf + # normalize the leaf's operator + self.normalize_leaf() + # set working variables; handle the context stack and previous tables + self.model = model + self._models = [] + for item in self.join_context: + self._models.append(item[0]) + self._models.append(model) + # check validity + self.check_leaf() + + def __str__(self): + return '' % (str(self.leaf), self.model._table, ','.join(self._get_context_debug())) + def generate_alias(self): - links = [(context[1]._table, context[4]) for context in self.context_stack] - alias, alias_statement = generate_table_alias(self._tables[0]._table, links) + links = [(context[1]._table, context[4]) for context in self.join_context] + alias, alias_statement = generate_table_alias(self._models[0]._table, links) return alias - def add_join_context(self, table, lhs_col, table_col, link): + def add_join_context(self, model, lhs_col, table_col, link): """ See above comments for more details. A join context is a tuple like: - ``(lhs, table, lhs_col, col, link)`` - where - - lhs is the source table (self.table) - - table is the destination table - - lsh_col is the source table column name used for the condition - - table_col is the destination table column name used for the condition - - link is the field name source of the join used as context to - generate the destination table alias + ``(lhs, model, lhs_col, col, link)`` - After adding the join, the table of the current leaf is updated. + After adding the join, the model of the current leaf is updated. """ - self.context_stack.append((self.table, table, lhs_col, table_col, link)) - self._tables.append(table) - self.table = table + self.join_context.append((self.model, model, lhs_col, table_col, link)) + self._models.append(model) + self.model = model def get_join_conditions(self): conditions = [] - alias = self._tables[0]._table - for context in self.context_stack: + alias = self._models[0]._table + for context in self.join_context: previous_alias = alias alias += '__' + context[4] conditions.append('"%s"."%s"="%s"."%s"' % (previous_alias, context[2], alias, context[3])) @@ -533,15 +534,15 @@ class ExtendedLeaf(object): def get_tables(self): tables = set() - alias = self._tables[0]._table - for context in self.context_stack: - alias += '__' + context[4] - table_full_alias = '"%s" as "%s"' % (context[1]._table, alias) - tables.add(table_full_alias) + links = [] + for context in self.join_context: + links.append((context[1]._table, context[4])) + alias, alias_statement = generate_table_alias(self._models[0]._table, links) + tables.add(alias_statement) return tables def _get_context_debug(self): - names = ['"%s"."%s"="%s"."%s" (%s)' % (item[0]._table, item[2], item[1]._table, item[3], item[4]) for item in self.context_stack] + names = ['"%s"."%s"="%s"."%s" (%s)' % (item[0]._table, item[2], item[1]._table, item[3], item[4]) for item in self.join_context] return names # -------------------------------------------------- @@ -589,20 +590,19 @@ class expression(object): right after initialization. :param exp: expression (using domain ('foo', '=', 'bar' format)) - :param table: root table object + :param table: root model :attr list result: list that will hold the result of the parsing as a list of ExtendedLeaf :attr list joins: list of join conditions, such as (res_country_state."id" = res_partner."state_id") - :attr root_table: base table for the query + :attr root_model: base model for the query :attr list expression: the domain expression, that will be normalized and prepared """ self.has_unaccent = openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent - self.result = [] self.joins = [] - self.root_table = table + self.root_model = table # normalize and prepare the expression for parsing self.expression = distribute_not(normalize_domain(exp)) @@ -621,7 +621,7 @@ class expression(object): for table in leaf.get_tables(): if table not in tables: tables.append(table) - table_name = '"%s"' % self.root_table._table + table_name = _quote(self.root_model._table) if table_name not in tables: tables.append(table_name) return tables @@ -633,29 +633,30 @@ class expression(object): def parse(self, cr, uid, context): """ Transform the leaves of the expression - The principle is to pop elements from the left of a leaf stack. Each - leaf is processed. The processing is a if/elif list of various cases - that appear in the leafs (many2one, function fields, ...). Two results - can appear at the end of a leaf processing: - - the leaf is modified or new leafs introduced in the domain: they - are added at the left of the stack, to be processed next + The principle is to pop elements from a leaf stack one at a time. + Each leaf is processed. The processing is a if/elif list of various + cases that appear in the leafs (many2one, function fields, ...). + Two things can happen as a processing result: + - the leaf has been modified and/or new leafs have to be introduced + in the expression; they are pushed into the leaf stack, to be + processed right after - the leaf is added to the result - Some var explanation: - :var obj working_table: table object, table containing the field + Some internal var explanation: + :var obj working_model: model object, model containing the field (the name provided in the left operand) :var list field_path: left operand seen as a path (foo.bar -> [foo, bar]) - :var obj relational_table: relational table of a field (field._obj) - ex: res_partner.bank_ids -> res_partner_bank + :var obj relational_model: relational model of a field (field._obj) + ex: res_partner.bank_ids -> res.partner.bank """ - def to_ids(value, relational_table, context=None, limit=None): + def to_ids(value, relational_model, context=None, limit=None): """ Normalize a single id or name, or a list of those, into a list of ids :param {int,long,basestring,list,tuple} value: if int, long -> return [value] if basestring, convert it into a list of basestrings, then if list of basestring -> - perform a name_search on relational_table for each name + perform a name_search on relational_model for each name return the list of related ids """ names = [] @@ -666,7 +667,7 @@ class expression(object): elif isinstance(value, (int, long)): return [value] if names: - name_get_list = [name_get[0] for name in names for name_get in relational_table.name_search(cr, uid, name, [], 'ilike', context=context, limit=limit)] + name_get_list = [name_get[0] for name in names for name_get in relational_model.name_search(cr, uid, name, [], 'ilike', context=context, limit=limit)] return list(set(name_get_list)) return list(value) @@ -693,24 +694,31 @@ class expression(object): return ids + recursive_children(ids2, model, parent_field) return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))] - def create_substitution_leaf(leaf, new_elements, new_table=None): - if new_table is None: - new_table = leaf.table - new_context_stack = [tuple(context) for context in leaf.context_stack] - new_leaf = ExtendedLeaf(new_elements, new_table, context_stack=new_context_stack) + def create_substitution_leaf(leaf, new_elements, new_model=None): + """ From a leaf, create a new leaf (based on the new_elements tuple + and new_model), that will have the same join context. Used to + insert equivalent leafs in the processing stack. """ + if new_model is None: + new_model = leaf.model + new_join_context = [tuple(context) for context in leaf.join_context] + new_leaf = ExtendedLeaf(new_elements, new_model, join_context=new_join_context) return new_leaf def pop(): + """ Pop a leaf to process. """ return self.stack.pop() def push(leaf): + """ Push a leaf to be processed right after. """ self.stack.append(leaf) def push_result(leaf): + """ Push a leaf to the results. This leaf has been fully processed + and validated. """ self.result.append(leaf) self.result = [] - self.stack = [ExtendedLeaf(leaf, self.root_table) for leaf in self.expression] + self.stack = [ExtendedLeaf(leaf, self.root_model) for leaf in self.expression] # process from right to left; expression is from left to right self.stack.reverse() @@ -719,7 +727,7 @@ class expression(object): leaf = pop() # Get working variables - working_table = leaf.table + working_model = leaf.model if leaf.is_operator(): left, operator, right = leaf.leaf, None, None elif leaf.is_true_leaf() or leaf.is_false_leaf(): @@ -728,11 +736,11 @@ class expression(object): else: left, operator, right = leaf.leaf field_path = left.split('.', 1) - field = working_table._columns.get(field_path[0]) + field = working_model._columns.get(field_path[0]) if field and field._obj: - relational_table = working_table.pool.get(field._obj) + relational_model = working_model.pool.get(field._obj) else: - relational_table = None + relational_model = None # ---------------------------------------- # SIMPLE CASE @@ -746,7 +754,7 @@ class expression(object): # ---------------------------------------- # FIELD NOT FOUND - # -> from inherits'd fields -> work on the related table, and add + # -> from inherits'd fields -> work on the related model, and add # a join condition # -> ('id', 'child_of', '..') -> use a 'to_ids' # -> but is one on the _log_access special fields, add directly to @@ -755,19 +763,19 @@ class expression(object): # -> else: crash # ---------------------------------------- - elif not field and field_path[0] in working_table._inherit_fields: + elif not field and field_path[0] in working_model._inherit_fields: # comments about inherits'd fields # { 'field_name': ('parent_model', 'm2o_field_to_reach_parent', # field_column_obj, origina_parent_model), ... } - next_table = working_table.pool.get(working_table._inherit_fields[field_path[0]][0]) - leaf.add_join_context(next_table, working_table._inherits[next_table._name], 'id', working_table._inherits[next_table._name]) + next_model = working_model.pool.get(working_model._inherit_fields[field_path[0]][0]) + leaf.add_join_context(next_model, working_model._inherits[next_model._name], 'id', working_model._inherits[next_model._name]) push(leaf) elif not field and left == 'id' and operator == 'child_of': - ids2 = to_ids(right, working_table, context) - dom = child_of_domain(left, ids2, working_table) + ids2 = to_ids(right, working_model, context) + dom = child_of_domain(left, ids2, working_model) for dom_leaf in reversed(dom): - new_leaf = create_substitution_leaf(leaf, dom_leaf, working_table) + new_leaf = create_substitution_leaf(leaf, dom_leaf, working_model) push(new_leaf) elif not field and field_path[0] in MAGIC_COLUMNS: @@ -791,32 +799,32 @@ class expression(object): elif len(field_path) > 1 and field._type == 'many2one' and field._auto_join: # res_partner.state_id = res_partner__state_id.id - leaf.add_join_context(relational_table, field_path[0], 'id', field_path[0]) - push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_table)) + leaf.add_join_context(relational_model, field_path[0], 'id', field_path[0]) + push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_model)) elif len(field_path) > 1 and field._type == 'one2many' and field._auto_join: # res_partner.id = res_partner__bank_ids.partner_id - leaf.add_join_context(relational_table, 'id', field._fields_id, field_path[0]) - domain = field._domain(working_table) if callable(field._domain) else field._domain - push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_table)) + leaf.add_join_context(relational_model, 'id', field._fields_id, field_path[0]) + domain = field._domain(working_model) if callable(field._domain) else field._domain + push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_model)) if domain: domain = normalize_domain(domain) for elem in reversed(domain): - push(create_substitution_leaf(leaf, elem, relational_table)) - push(create_substitution_leaf(leaf, AND_OPERATOR, relational_table)) + push(create_substitution_leaf(leaf, elem, relational_model)) + push(create_substitution_leaf(leaf, AND_OPERATOR, relational_model)) elif len(field_path) > 1 and field._auto_join: raise NotImplementedError('_auto_join attribute not supported on many2many field %s' % (left)) elif len(field_path) > 1 and field._type == 'many2one': - right_ids = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) + right_ids = relational_model.search(cr, uid, [(field_path[1], operator, right)], context=context) leaf.leaf = (field_path[0], 'in', right_ids) push(leaf) # Making search easier when there is a left operand as field.o2m or field.m2m elif len(field_path) > 1 and field._type in ['many2many', 'one2many']: - right_ids = relational_table.search(cr, uid, [(field_path[1], operator, right)], context=context) - table_ids = working_table.search(cr, uid, [(field_path[0], 'in', right_ids)], context=dict(context, active_test=False)) + right_ids = relational_model.search(cr, uid, [(field_path[1], operator, right)], context=context) + table_ids = working_model.search(cr, uid, [(field_path[0], 'in', right_ids)], context=dict(context, active_test=False)) leaf.leaf = ('id', 'in', table_ids) push(leaf) @@ -842,7 +850,7 @@ class expression(object): elif isinstance(field, fields.function) and not field.store: # this is a function field that is not stored - fct_domain = field.search(cr, uid, working_table, left, [leaf.leaf], context=context) + fct_domain = field.search(cr, uid, working_model, left, [leaf.leaf], context=context) if not fct_domain: leaf.leaf = TRUE_LEAF push(leaf) @@ -850,26 +858,30 @@ class expression(object): # we assume that the expression is valid # we create a dummy leaf for forcing the parsing of the resulting expression for domain_element in reversed(fct_domain): - push(create_substitution_leaf(leaf, domain_element, working_table)) - # self.push(create_substitution_leaf(leaf, TRUE_LEAF, working_table)) - # self.push(create_substitution_leaf(leaf, AND_OPERATOR, working_table)) + push(create_substitution_leaf(leaf, domain_element, working_model)) + # self.push(create_substitution_leaf(leaf, TRUE_LEAF, working_model)) + # self.push(create_substitution_leaf(leaf, AND_OPERATOR, working_model)) + + # ------------------------------------------------- + # RELATIONAL FIELDS + # ------------------------------------------------- # Applying recursivity on field(one2many) elif field._type == 'one2many' and operator == 'child_of': - ids2 = to_ids(right, relational_table, context) - if field._obj != working_table._name: - dom = child_of_domain(left, ids2, relational_table, prefix=field._obj) + ids2 = to_ids(right, relational_model, context) + if field._obj != working_model._name: + dom = child_of_domain(left, ids2, relational_model, prefix=field._obj) else: - dom = child_of_domain('id', ids2, working_table, parent=left) + dom = child_of_domain('id', ids2, working_model, parent=left) for dom_leaf in reversed(dom): - push(create_substitution_leaf(leaf, dom_leaf, working_table)) + push(create_substitution_leaf(leaf, dom_leaf, working_model)) elif field._type == 'one2many': call_null = True if right is not False: if isinstance(right, basestring): - ids2 = [x[0] for x in relational_table.name_search(cr, uid, right, [], operator, context=context, limit=None)] + ids2 = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, context=context, limit=None)] if ids2: operator = 'in' else: @@ -881,36 +893,36 @@ class expression(object): if operator in ['like', 'ilike', 'in', '=']: #no result found with given search criteria call_null = False - push(create_substitution_leaf(leaf, FALSE_LEAF, working_table)) + push(create_substitution_leaf(leaf, FALSE_LEAF, working_model)) else: - ids2 = select_from_where(cr, field._fields_id, relational_table._table, 'id', ids2, operator) + ids2 = select_from_where(cr, field._fields_id, relational_model._table, 'id', ids2, operator) if ids2: call_null = False o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - push(create_substitution_leaf(leaf, ('id', o2m_op, ids2), working_table)) + push(create_substitution_leaf(leaf, ('id', o2m_op, ids2), working_model)) if call_null: o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_table._table)), working_table)) + push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_model._table)), working_model)) elif field._type == 'many2many': - rel_table, rel_id1, rel_id2 = field._sql_names(working_table) + rel_table, rel_id1, rel_id2 = field._sql_names(working_model) #FIXME if operator == 'child_of': def _rec_convert(ids): - if relational_table == working_table: + if relational_model == working_model: return ids return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator) - ids2 = to_ids(right, relational_table, context) - dom = child_of_domain('id', ids2, relational_table) - ids2 = relational_table.search(cr, uid, dom, context=context) - push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), working_table)) + ids2 = to_ids(right, relational_model, context) + dom = child_of_domain('id', ids2, relational_model) + ids2 = relational_model.search(cr, uid, dom, context=context) + push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), working_model)) else: call_null_m2m = True if right is not False: if isinstance(right, basestring): - res_ids = [x[0] for x in relational_table.name_search(cr, uid, right, [], operator, context=context)] + res_ids = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, context=context)] if res_ids: operator = 'in' else: @@ -922,29 +934,29 @@ class expression(object): if operator in ['like', 'ilike', 'in', '=']: #no result found with given search criteria call_null_m2m = False - push(create_substitution_leaf(leaf, FALSE_LEAF, working_table)) + push(create_substitution_leaf(leaf, FALSE_LEAF, working_model)) else: operator = 'in' # operator changed because ids are directly related to main object else: call_null_m2m = False m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), working_table)) + push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), working_model)) if call_null_m2m: m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - push(create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_table)) + push(create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_model)) elif field._type == 'many2one': if operator == 'child_of': - ids2 = to_ids(right, relational_table, context) - if field._obj != working_table._name: - dom = child_of_domain(left, ids2, relational_table, prefix=field._obj) + ids2 = to_ids(right, relational_model, context) + if field._obj != working_model._name: + dom = child_of_domain(left, ids2, relational_model, prefix=field._obj) else: - dom = child_of_domain('id', ids2, working_table, parent=left) + dom = child_of_domain('id', ids2, working_model, parent=left) for dom_leaf in reversed(dom): - push(create_substitution_leaf(leaf, dom_leaf, working_table)) + push(create_substitution_leaf(leaf, dom_leaf, working_model)) else: - def _get_expression(relational_table, cr, uid, left, right, operator, context=None): + def _get_expression(relational_model, cr, uid, left, right, operator, context=None): if context is None: context = {} c = context.copy() @@ -959,29 +971,32 @@ class expression(object): operator = dict_op[operator] elif isinstance(right, list) and operator in ['!=', '=']: # for domain (FIELD,'=',['value1','value2']) operator = dict_op[operator] - res_ids = [x[0] for x in relational_table.name_search(cr, uid, right, [], operator, limit=None, context=c)] + res_ids = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, limit=None, context=c)] if operator in NEGATIVE_TERM_OPERATORS: res_ids.append(False) # TODO this should not be appended if False was in 'right' return (left, 'in', res_ids) # resolve string-based m2o criterion into IDs if isinstance(right, basestring) or \ right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right): - push(create_substitution_leaf(leaf, _get_expression(relational_table, cr, uid, left, right, operator, context=context), working_table)) + push(create_substitution_leaf(leaf, _get_expression(relational_model, cr, uid, left, right, operator, context=context), working_model)) else: # right == [] or right == False and all other cases are handled by __leaf_to_sql() push_result(leaf) - else: - # other field type - # add the time part to datetime field when it's not there: - if field._type == 'datetime' and right and len(right) == 10: + # ------------------------------------------------- + # OTHER FIELDS + # -> datetime fields: manage time part of the datetime + # field when it is not there + # -> manage translatable fields + # ------------------------------------------------- + else: + if field._type == 'datetime' and right and len(right) == 10: if operator in ('>', '>='): right += ' 00:00:00' elif operator in ('<', '<='): right += ' 23:59:59' - - push(create_substitution_leaf(leaf, (left, operator, right), working_table)) + push(create_substitution_leaf(leaf, (left, operator, right), working_model)) elif field.translate: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') @@ -1001,45 +1016,45 @@ class expression(object): subselect += ' AND value ' + sql_operator + ' ' + " (" + instr + ")" \ ') UNION (' \ ' SELECT id' \ - ' FROM "' + working_table._table + '"' \ + ' FROM "' + working_model._table + '"' \ ' WHERE "' + left + '" ' + sql_operator + ' ' + " (" + instr + "))" else: subselect += ' AND value ' + sql_operator + instr + \ ') UNION (' \ ' SELECT id' \ - ' FROM "' + working_table._table + '"' \ + ' FROM "' + working_model._table + '"' \ ' WHERE "' + left + '" ' + sql_operator + instr + ")" - params = [working_table._name + ',' + left, + params = [working_model._name + ',' + left, context.get('lang', False) or 'en_US', 'model', right, right, ] - push(create_substitution_leaf(leaf, ('id', 'inselect', (subselect, params)), working_table)) + push(create_substitution_leaf(leaf, ('id', 'inselect', (subselect, params)), working_model)) else: push_result(leaf) # ---------------------------------------- # END OF PARSING FULL DOMAIN + # -> generate joins # ---------------------------------------- - # Generate joins joins = set() for leaf in self.result: joins |= set(leaf.get_join_conditions()) self.joins = list(joins) def __leaf_to_sql(self, eleaf): - table = eleaf.table + model = eleaf.model leaf = eleaf.leaf left, operator, right = leaf # final sanity checks - should never fail assert operator in (TERM_OPERATORS + ('inselect',)), \ "Invalid operator %r in domain term %r" % (operator, leaf) - assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in table._all_columns \ + assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in model._all_columns \ or left in MAGIC_COLUMNS, "Invalid field %r in domain term %r" % (left, leaf) table_alias = '"%s"' % (eleaf.generate_alias()) @@ -1079,7 +1094,7 @@ class expression(object): if left == 'id': instr = ','.join(['%s'] * len(params)) else: - instr = ','.join([table._columns[left]._symbol_set[0]] * len(params)) + instr = ','.join([model._columns[left]._symbol_set[0]] * len(params)) query = '(%s."%s" %s (%s))' % (table_alias, left, operator, instr) else: # The case for (left, 'in', []) or (left, 'not in', []). @@ -1094,7 +1109,7 @@ class expression(object): else: # Must not happen raise ValueError("Invalid domain term %r" % (leaf,)) - elif right == False and (left in table._columns) and table._columns[left]._type == "boolean" and (operator == '='): + elif right == False and (left in model._columns) and model._columns[left]._type == "boolean" and (operator == '='): query = '(%s."%s" IS NULL or %s."%s" = false )' % (table_alias, left, table_alias, left) params = [] @@ -1102,7 +1117,7 @@ class expression(object): query = '%s."%s" IS NULL ' % (table_alias, left) params = [] - elif right == False and (left in table._columns) and table._columns[left]._type == "boolean" and (operator == '!='): + elif right == False and (left in model._columns) and model._columns[left]._type == "boolean" and (operator == '!='): query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table_alias, left, table_alias, left) params = [] @@ -1117,7 +1132,7 @@ class expression(object): params = [] else: # '=?' behaves like '=' in other cases - query, params = self.__leaf_to_sql((left, '=', right), table) + query, params = self.__leaf_to_sql((left, '=', right), model) elif left == 'id': query = '%s.id %s %%s' % (table_alias, operator) @@ -1127,8 +1142,8 @@ class expression(object): need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator) - if left in table._columns: - format = need_wildcard and '%s' or table._columns[left]._symbol_set[0] + if left in model._columns: + format = need_wildcard and '%s' or model._columns[left]._symbol_set[0] if self.has_unaccent and sql_operator in ('ilike', 'not ilike'): query = '(unaccent(%s."%s") %s unaccent(%s))' % (table_alias, left, sql_operator, format) else: @@ -1149,8 +1164,8 @@ class expression(object): str_utf8 = str(right) params = '%%%s%%' % str_utf8 add_null = not str_utf8 - elif left in table._columns: - params = table._columns[left]._symbol_set[1](right) + elif left in model._columns: + params = model._columns[left]._symbol_set[1](right) if add_null: query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left) From 1ca2c45d418884505462d257b75aebdffbfc3d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 7 Dec 2012 14:49:46 +0100 Subject: [PATCH 34/39] [IMP] [TESTS] Cleaned and updated tests about order_by with inherits and m2o fields. bzr revid: tde@openerp.com-20121207134946-h0p1em7iq80ux0js --- openerp/addons/base/tests/test_search.py | 35 +++++++++++++++++------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/openerp/addons/base/tests/test_search.py b/openerp/addons/base/tests/test_search.py index a64bfcb3e94..98dfefd4933 100644 --- a/openerp/addons/base/tests/test_search.py +++ b/openerp/addons/base/tests/test_search.py @@ -58,7 +58,7 @@ class test_search(common.TransactionCase): id_desc_active_desc = partners.search(cr, uid, [('name', 'like', 'test_search_order%'), '|', ('active', '=', True), ('active', '=', False)], order="id desc, active desc") self.assertEqual([e, ab, b, a, d, c], id_desc_active_desc, "Search with 'ID DESC, ACTIVE DESC' order failed.") - def test_10_m2order(self): + def test_10_unherits_m2order(self): registry, cr, uid = self.registry, self.cr, self.uid users_obj = registry('res.users') @@ -66,20 +66,35 @@ class test_search(common.TransactionCase): group_employee_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user') group_employee_id = group_employee_ref and group_employee_ref[1] or False + # Get country/state data + country_us_id = registry('res.country').search(cr, uid, [('code', 'like', 'US')])[0] + state_ids = registry('res.country.state').search(cr, uid, [('country_id', '=', country_us_id)], limit=2) + country_be_id = registry('res.country').search(cr, uid, [('code', 'like', 'BE')])[0] + + # Create test users search_user = users_obj.create(cr, uid, {'name': '__search', 'login': '__search', 'groups_id': [(6, 0, [group_employee_id])]}) - a = users_obj.create(cr, uid, {'name': '__test_A', 'login': '__z_test_A'}) - b = users_obj.create(cr, uid, {'name': '__test_B', 'login': '__a_test_B'}) + a = users_obj.create(cr, uid, {'name': '__test_A', 'login': '__test_A', 'country_id': country_be_id, 'state_id': country_be_id}) + b = users_obj.create(cr, uid, {'name': '__test_B', 'login': '__a_test_B', 'country_id': country_us_id, 'state_id': state_ids[1]}) + c = users_obj.create(cr, uid, {'name': '__test_B', 'login': '__z_test_B', 'country_id': country_us_id, 'state_id': state_ids[0]}) - # Do: search on res.users, order on a field on res.partner, then res.users - print '\n\n' + # Do: search on res.users, order on a field on res.partner to try inherits'd fields, then res.users user_ids = users_obj.search(cr, search_user, [], order='name asc, login desc') - self.assertTrue(set([search_user, a, b]).issubset(set(user_ids)), 'cacaprout') + expected_ids = [search_user, a, c, b] + test_user_ids = filter(lambda x: x in expected_ids, user_ids) + self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order') - # DO: order on a many2one - print '\n\n' + # Do: order on many2one and inherits'd fields user_ids = users_obj.search(cr, search_user, [], order='state_id asc, country_id desc, name asc, login desc') - print user_ids[0:25] - # self.assertTrue(set([search_user, a, b]).issubset(set(user_ids)), 'cacaprout') + expected_ids = [c, b, a, search_user] + test_user_ids = filter(lambda x: x in expected_ids, user_ids) + self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order') + + # Do: order on many2one and inherits'd fields + user_ids = users_obj.search(cr, search_user, [], order='country_id desc, state_id desc, name asc, login desc') + expected_ids = [search_user, b, c, a] + test_user_ids = filter(lambda x: x in expected_ids, user_ids) + self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order') + if __name__ == '__main__': unittest2.main() From efc6e78d25e71fdb90dc8a0e3fd03827b72f8603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 7 Dec 2012 15:29:31 +0100 Subject: [PATCH 35/39] [IMP] [MOV] Moved test_osv and test_translate from unchecked test directory to tests, to be now effectively run. Updated test_osv to matche the new alias coding style. bzr revid: tde@openerp.com-20121207142931-6ll5yduitdk3ijes --- openerp/test/__init__.py | 25 ------------------- openerp/tests/__init__.py | 7 ++++-- openerp/{test => tests}/test_osv.py | 30 +++++++++++------------ openerp/{test => tests}/test_translate.py | 0 4 files changed, 20 insertions(+), 42 deletions(-) delete mode 100644 openerp/test/__init__.py rename openerp/{test => tests}/test_osv.py (62%) rename openerp/{test => tests}/test_translate.py (100%) diff --git a/openerp/test/__init__.py b/openerp/test/__init__.py deleted file mode 100644 index 7a6c167d85b..00000000000 --- a/openerp/test/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2010 OpenERP S.A. http://www.openerp.com -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from test_osv import * -from test_translate import * - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py index d036ccfc751..92a40ab306c 100644 --- a/openerp/tests/__init__.py +++ b/openerp/tests/__init__.py @@ -8,9 +8,10 @@ Tests can be explicitely added to the `fast_suite` or `checks` lists or not. See the :ref:`test-framework` section in the :ref:`features` list. """ -from . import test_expression, test_mail, test_ir_sequence, test_orm,\ +from . import test_expression, test_mail, test_ir_sequence, test_orm, \ test_fields, test_basecase, \ - test_view_validation, test_uninstall, test_misc, test_db_cursor + test_view_validation, test_uninstall, test_misc, test_db_cursor, \ + test_osv, test_translate from . import test_ir_filters fast_suite = [ @@ -27,6 +28,8 @@ checks = [ test_basecase, test_view_validation, test_misc, + test_osv, + test_translate, ] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/test/test_osv.py b/openerp/tests/test_osv.py similarity index 62% rename from openerp/test/test_osv.py rename to openerp/tests/test_osv.py index 2db9cec1cbd..80be8160b61 100644 --- a/openerp/test/test_osv.py +++ b/openerp/tests/test_osv.py @@ -22,45 +22,45 @@ import unittest from openerp.osv.query import Query + class QueryTestCase(unittest.TestCase): def test_basic_query(self): query = Query() - query.tables.extend(['"product_product"','"product_template"']) + query.tables.extend(['"product_product"', '"product_template"']) query.where_clause.append("product_product.template_id = product_template.id") - query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join - query.add_join(("product_product", "res_user", "user_id", "id"), outer=True) # outer join + query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join + query.add_join(("product_product", "res_user", "user_id", "id"), outer=True) # outer join self.assertEquals(query.get_sql()[0].strip(), - """"product_product" LEFT JOIN "res_user" ON ("product_product"."user_id" = "res_user"."id"),"product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") """.strip()) + """"product_product" LEFT JOIN "res_user" as "product_product__user_id" ON ("product_product"."user_id" = "product_product__user_id"."id"),"product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") """.strip()) self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip()) def test_query_chained_explicit_joins(self): query = Query() - query.tables.extend(['"product_product"','"product_template"']) + query.tables.extend(['"product_product"', '"product_template"']) query.where_clause.append("product_product.template_id = product_template.id") - query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join - query.add_join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join + query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join + query.add_join(("product_template__categ_id", "res_user", "user_id", "id"), outer=True) # CHAINED outer join self.assertEquals(query.get_sql()[0].strip(), - """"product_product","product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") LEFT JOIN "res_user" ON ("product_category"."user_id" = "res_user"."id")""".strip()) + """"product_product","product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" as "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id")""".strip()) self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip()) def test_mixed_query_chained_explicit_implicit_joins(self): query = Query() - query.tables.extend(['"product_product"','"product_template"']) + query.tables.extend(['"product_product"', '"product_template"']) query.where_clause.append("product_product.template_id = product_template.id") - query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join - query.add_join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join + query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join + query.add_join(("product_template__categ_id", "res_user", "user_id", "id"), outer=True) # CHAINED outer join query.tables.append('"account.account"') - query.where_clause.append("product_category.expense_account_id = account_account.id") # additional implicit join + query.where_clause.append("product_category.expense_account_id = account_account.id") # additional implicit join self.assertEquals(query.get_sql()[0].strip(), - """"product_product","product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") LEFT JOIN "res_user" ON ("product_category"."user_id" = "res_user"."id"),"account.account" """.strip()) + """"product_product","product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" as "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id"),"account.account" """.strip()) self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id AND product_category.expense_account_id = account_account.id""".strip()) - def test_raise_missing_lhs(self): query = Query() query.tables.append('"product_product"') - self.assertRaises(AssertionError, query.join, ("product_template", "product_category", "categ_id", "id"), outer=False) + self.assertRaises(AssertionError, query.add_join, ("product_template", "product_category", "categ_id", "id"), outer=False) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/test/test_translate.py b/openerp/tests/test_translate.py similarity index 100% rename from openerp/test/test_translate.py rename to openerp/tests/test_translate.py From 50cf6d4008f61eff4f7ed17e46a21dcfe337099b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 7 Dec 2012 16:42:04 +0100 Subject: [PATCH 36/39] [CLEAN] Query: cleaned a bit the code. All joins now goes through the same method, either implicit or explicit. Will have to be upgraded in future versions, but at least this is a bit centralized. Updated ORM accordingly. Updated tests. Added a get_alias_from_query method in expression that find the table and the alias from a 'full alias' statement. bzr revid: tde@openerp.com-20121207154204-mx036lpj3vdclu77 --- openerp/osv/expression.py | 14 ++++ openerp/osv/orm.py | 19 +----- openerp/osv/query.py | 140 ++++++++------------------------------ openerp/tests/test_osv.py | 14 ++-- 4 files changed, 50 insertions(+), 137 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 703b7503387..8535213b4de 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -346,6 +346,20 @@ def generate_table_alias(src_table_alias, joined_tables=[]): return ('%s' % alias, '%s as %s' % (_quote(joined_tables[-1][0]), _quote(alias))) +def get_alias_from_query(from_query): + """ :param string from_query: is something like : + - '"res_partner"' OR + - '"res_partner" as "res_users__partner_id"'' + :param tuple result: (unquoted table name, unquoted alias) + i.e. (res_partners, res_partner) OR (res_partner, res_users__partner_id) + """ + from_splitted = from_query.split(' as ') + if len(from_splitted) > 1: + return (from_splitted[0].replace('"', ''), from_splitted[1].replace('"', '')) + else: + return (from_splitted[0].replace('"', ''), from_splitted[0].replace('"', '')) + + def normalize_leaf(element): """ Change a term's operator to some canonical form, simplifying later processing. """ diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index d2c0a0f5682..30746bdd7fc 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2728,14 +2728,8 @@ class BaseModel(object): """ inherits_field = current_model._inherits[parent_model_name] parent_model = self.pool.get(parent_model_name) - parent_alias = query.add_implicit_join((current_model, parent_model, inherits_field, 'id', inherits_field)) + parent_alias, parent_alias_statement = query.add_join((current_model._table, parent_model._table, inherits_field, 'id', inherits_field), implicit=True) return parent_alias - # table_alias = expression.generate_table_alias(current_model, [(parent_model, inherits_field)]) - # print '\t... _inherits_join_add trying to add %s in %s' % (table_alias, query.tables) - # # query.add_table(table_alias) - # if table_alias not in query.tables: - # query.tables.append(table_alias) - # query.where_clause.append('("%s".%s = %s.id)' % (current_model._table, inherits_field, table_alias)) def _inherits_join_calc(self, field, query): """ @@ -2746,7 +2740,6 @@ class BaseModel(object): :param query: query object on which the JOIN should be added :return: qualified name of field, to be used in SELECT clause """ - print '\t--> _inherits_join_calc' current_table = self parent_alias = current_table._table while field in current_table._inherit_fields and not field in current_table._columns: @@ -4667,15 +4660,12 @@ class BaseModel(object): :param model child_object: model object, base of the rule application """ if added_clause: - print '--> _apply_ir_rules.apply_rule,', added_clause, added_params, added_tables, parent_model, child_object if parent_model and child_object: - print '\t... calling inherits-Join_add with parent_model %s, child_object %s' % (parent_model, child_object) # as inherited rules are being applied, we need to add the missing JOIN # to reach the parent table (if it was not JOINed yet in the query) parent_alias = child_object._inherits_join_add(child_object, parent_model, query) # inherited rules are applied on the external table -> need to get the alias and replace parent_table = self.pool.get(parent_model)._table - print parent_table, parent_alias added_clause = [clause.replace('"%s"' % parent_table, '"%s"' % parent_alias) for clause in added_clause] # not sure of myself here (in the ORM, this statment is quite cool) new_tables = [] @@ -4686,11 +4676,9 @@ class BaseModel(object): new_table = table.replace('"%s"' % parent_table, '"%s"' % parent_alias) new_tables.append(new_table) added_tables = new_tables - print '\t... adding tables %s, clause %s (params %s)' % (added_tables, added_clause, added_params) query.where_clause += added_clause query.where_clause_params += added_params for table in added_tables: - print '\t... adding table %s in %s' % (table, query.tables) if table not in query.tables: query.tables.append(table) return True @@ -4715,7 +4703,6 @@ class BaseModel(object): :return: the qualified field name to use in an ORDER BY clause to sort by ``order_field`` """ - print '_generate_m2o_order_by' if order_field not in self._columns and order_field in self._inherit_fields: # also add missing joins for reaching the table containing the m2o field qualified_field = self._inherits_join_calc(order_field, query) @@ -4747,7 +4734,7 @@ class BaseModel(object): # 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 src_table, src_field = qualified_field.replace('"', '').split('.', 1) - dst_alias = query.add_join((src_table, dest_model._table, src_field, 'id'), outer=True) + dst_alias, dst_alias_statement = query.add_join((src_table, dest_model._table, src_field, 'id', src_field), implicit=False, outer=True) qualify = lambda field: '"%s"."%s"' % (dst_alias, field) return map(qualify, m2o_order) if isinstance(m2o_order, list) else qualify(m2o_order) @@ -4769,7 +4756,6 @@ class BaseModel(object): return order_list order_by_clause = ','.join(_split_order(self._order, self._table)) if order_spec: - print '-->_generate_order_by beginning' order_by_elements = [] self._check_qorder(order_spec) for order_part in order_spec.split(','): @@ -4804,7 +4790,6 @@ class BaseModel(object): order_by_elements.append("%s %s" % (inner_clause, order_direction)) if order_by_elements: order_by_clause = ",".join(order_by_elements) - print '-->_generate_order_by ending' return order_by_clause and (' ORDER BY %s ' % order_by_clause) or '' diff --git a/openerp/osv/query.py b/openerp/osv/query.py index a07def36a31..7f1bd930c91 100644 --- a/openerp/osv/query.py +++ b/openerp/osv/query.py @@ -53,9 +53,6 @@ class Query(object): # holds the list of tables joined using default JOIN. # the table names are stored double-quoted (backwards compatibility) self.tables = tables or [] - # holds a mapping of table aliases: - # self._table_alias_mapping = {'alias_1': 'table_name'} - self._table_alias_mapping = {} # holds the list of WHERE clause elements, to be joined with # 'AND' when generating the final query @@ -79,29 +76,19 @@ class Query(object): # LEFT JOIN "table_c" ON ("table_a"."table_a_col2" = "table_c"."table_c_col") self.joins = joins or {} - def _add_table_alias(self, table_alias): - pass - def _get_table_aliases(self): - aliases = [] - for table in self.tables: - if len(table.split(' as ')) > 1: - aliases.append(table.split(' as ')[1].replace('"', '')) - else: - aliases.append(table.replace('"', '')) - # print '--', aliases - return aliases + from openerp.osv.expression import get_alias_from_query + return [get_alias_from_query(from_statement)[1] for from_statement in self.tables] def _get_alias_mapping(self): + from openerp.osv.expression import get_alias_from_query mapping = {} - aliases = self._get_table_aliases() - for alias in aliases: - for table in self.tables: - if '"%s"' % (alias) in table: - mapping.setdefault(alias, table) + for table in self.tables: + alias, statement = get_alias_from_query(table) + mapping[statement] = table return mapping - def add_new_join(self, connection, implicit=True, outer=False): + def add_join(self, connection, implicit=True, outer=False): """ Join a destination table to the current table. :param implicit: False if the join is an explicit join. This allows @@ -127,115 +114,41 @@ class Query(object): """ from openerp.osv.expression import generate_table_alias (lhs, table, lhs_col, col, link) = connection - alias, alias_statement = generate_table_alias(lhs._table, [(table._table, link)]) + alias, alias_statement = generate_table_alias(lhs, [(table, link)]) if implicit: - print '\t\t... Query: trying to add %s in %s (received %s)' % (alias_statement, self.tables, connection) if alias_statement not in self.tables: self.tables.append(alias_statement) - condition = '("%s"."%s" = "%s"."%s")' % (lhs._table, lhs_col, alias, col) - print '\t\t... added %s' % (condition) + condition = '("%s"."%s" = "%s"."%s")' % (lhs, lhs_col, alias, col) + # print '\t\t... Query: added %s in %s (received %s)' % (alias_statement, self.tables, connection) self.where_clause.append(condition) - return alias + else: + # print '\t\t... Query: not added %s in %s (received %s)' % (alias_statement, self.tables, connection) + # already joined + pass + return alias, alias_statement else: - (lhs, table, lhs_col, col) = connection - lhs = _quote(lhs) - table = _quote(table) - print connection - aliases = [] - for table in self.tables: - if len(table.split(' as ')) > 1: - aliases.append(table.split(' as ')[1]) - else: - aliases.append(table) - print '--', aliases - aliases = [table.split(' as ') for table in self.tables] - assert lhs in self.aliases, "Left-hand-side table %s must already be part of the query tables %s!" % (lhs, str(self.tables)) - if table in self.tables: + aliases = self._get_table_aliases() + assert lhs in aliases, "Left-hand-side table %s must already be part of the query tables %s!" % (lhs, str(self.tables)) + if alias_statement in self.tables: # already joined, must ignore (promotion to outer and multiple joins not supported yet) + # print '\t\t... Query: not added %s in %s (received %s)' % (alias_statement, self.tables, connection) pass else: # add JOIN - self.tables.append(table) - self.joins.setdefault(lhs, []).append((table, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN')) - return self - - def add_implicit_join(self, connection): - """ Adds an implicit join. This means that left-hand table is added to the - Query.tables (adding a table in the from clause), and that a join - condition is added in Query.where_clause. - - Implicit joins use expression.generate_table_alias to generate the - alias the the joined table. - - :param connection: a tuple``(lhs, table, lhs_col, col, link)`` Please - refer to expression.py for more details about joins. - """ - from openerp.osv.expression import generate_table_alias - (lhs, table, lhs_col, col, link) = connection - alias, alias_statement = generate_table_alias(lhs._table, [(table._table, link)]) - print '\t\t... Query: trying to add %s in %s (received %s)' % (alias_statement, self.tables, connection) - if alias_statement not in self.tables: - self.tables.append(alias_statement) - condition = '("%s"."%s" = "%s"."%s")' % (lhs._table, lhs_col, alias, col) - print '\t\t... added %s' % (condition) - self.where_clause.append(condition) - return alias - - def add_join(self, connection, outer=False): - """Adds the JOIN specified in ``connection``. - - :param connection: a tuple ``(lhs, table, lhs_col, col)``. - The join corresponds to the SQL equivalent of:: - - (lhs.lhs_col = table.col) - - Note that all connection elements are strings. - - :param outer: True if a LEFT OUTER JOIN should be used, if possible - (no promotion to OUTER JOIN is supported in case the JOIN - was already present in the query, as for the moment - implicit INNER JOINs are only connected from NON-NULL - columns so it would not be correct (e.g. for - ``_inherits`` or when a domain criterion explicitly - adds filtering) - """ - from openerp.osv.expression import generate_table_alias - (lhs, table, lhs_col, col) = connection - # lhs = _quote(lhs) - # table = _quote(table) - print '\t\t... Query.add_join(): adding connection %s' % str(connection) - - aliases = self._get_table_aliases() - - assert lhs in aliases, "Left-hand-side table %s must already be part of the query tables %s!" % (lhs, str(self.tables)) - - rhs, rhs_statement = generate_table_alias(lhs, [(connection[1], connection[2])]) - print rhs, rhs_statement - - if rhs_statement in self.tables: - # already joined, must ignore (promotion to outer and multiple joins not supported yet) - pass - else: - # add JOIN - self.tables.append(rhs_statement) - self.joins.setdefault(lhs, []).append((rhs, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN')) - return rhs + self.tables.append(alias_statement) + self.joins.setdefault(lhs, []).append((alias, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN')) + return alias, alias_statement def get_sql(self): - """Returns (query_from, query_where, query_params)""" + """ Returns (query_from, query_where, query_params). """ + from openerp.osv.expression import get_alias_from_query query_from = '' tables_to_process = list(self.tables) - alias_mapping = self._get_alias_mapping() - # print 'tables_to_process %s' % (tables_to_process) - # print 'self.joins %s' % (self.joins) - # print 'alias_mapping %s' % (alias_mapping) - def add_joins_for_table(table, query_from): for (dest_table, lhs_col, col, join) in self.joins.get(table, []): - # print dest_table tables_to_process.remove(alias_mapping[dest_table]) query_from += ' %s %s ON ("%s"."%s" = "%s"."%s")' % \ (join, alias_mapping[dest_table], table, lhs_col, dest_table, col) @@ -244,8 +157,9 @@ class Query(object): for table in tables_to_process: query_from += table - if _get_alias_from_statement(table) in self.joins: - query_from = add_joins_for_table(_get_alias_from_statement(table), query_from) + table_alias = get_alias_from_query(table)[1] + if table_alias in self.joins: + query_from = add_joins_for_table(table_alias, query_from) query_from += ',' query_from = query_from[:-1] # drop last comma return (query_from, " AND ".join(self.where_clause), self.where_clause_params) diff --git a/openerp/tests/test_osv.py b/openerp/tests/test_osv.py index 80be8160b61..e36495fc7c6 100644 --- a/openerp/tests/test_osv.py +++ b/openerp/tests/test_osv.py @@ -29,8 +29,8 @@ class QueryTestCase(unittest.TestCase): query = Query() query.tables.extend(['"product_product"', '"product_template"']) query.where_clause.append("product_product.template_id = product_template.id") - query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join - query.add_join(("product_product", "res_user", "user_id", "id"), outer=True) # outer join + query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join + query.add_join(("product_product", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # outer join self.assertEquals(query.get_sql()[0].strip(), """"product_product" LEFT JOIN "res_user" as "product_product__user_id" ON ("product_product"."user_id" = "product_product__user_id"."id"),"product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") """.strip()) self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip()) @@ -39,8 +39,8 @@ class QueryTestCase(unittest.TestCase): query = Query() query.tables.extend(['"product_product"', '"product_template"']) query.where_clause.append("product_product.template_id = product_template.id") - query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join - query.add_join(("product_template__categ_id", "res_user", "user_id", "id"), outer=True) # CHAINED outer join + query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join + query.add_join(("product_template__categ_id", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # CHAINED outer join self.assertEquals(query.get_sql()[0].strip(), """"product_product","product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" as "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id")""".strip()) self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip()) @@ -49,8 +49,8 @@ class QueryTestCase(unittest.TestCase): query = Query() query.tables.extend(['"product_product"', '"product_template"']) query.where_clause.append("product_product.template_id = product_template.id") - query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join - query.add_join(("product_template__categ_id", "res_user", "user_id", "id"), outer=True) # CHAINED outer join + query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join + query.add_join(("product_template__categ_id", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # CHAINED outer join query.tables.append('"account.account"') query.where_clause.append("product_category.expense_account_id = account_account.id") # additional implicit join self.assertEquals(query.get_sql()[0].strip(), @@ -60,7 +60,7 @@ class QueryTestCase(unittest.TestCase): def test_raise_missing_lhs(self): query = Query() query.tables.append('"product_product"') - self.assertRaises(AssertionError, query.add_join, ("product_template", "product_category", "categ_id", "id"), outer=False) + self.assertRaises(AssertionError, query.add_join, ("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From a786f8406b2a75f0ff5127908722b354361eada6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 7 Dec 2012 17:05:39 +0100 Subject: [PATCH 37/39] [REV] ir_ui_menu: coding-style changes reverted, because not necessary. Only a store=True on needaction_enabled is sufficient. bzr revid: tde@openerp.com-20121207160539-6pqj932lvznxofd4 --- openerp/addons/base/ir/ir_ui_menu.py | 30 +++++++++------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index e286466b85c..cd0512fff74 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -249,7 +249,7 @@ class ir_ui_menu(osv.osv): icon_image = False if icon_path: try: - icon_file = tools.file_open(icon_path, 'rb') + icon_file = tools.file_open(icon_path,'rb') icon_image = base64.encodestring(icon_file.read()) finally: icon_file.close() @@ -265,22 +265,17 @@ class ir_ui_menu(osv.osv): return res - def get_needaction_data(self, cr, uid, ids, field_names, args, context=None): - """ Return for each menu entry of ids : - - if it uses the needaction mechanism (needaction_enabled) - - the needaction counter of the related action, taking into account - the action domain - """ + def _get_needaction(self, cr, uid, ids, field_names, args, context=None): res = {} for menu in self.browse(cr, uid, ids, context=context): res[menu.id] = { 'needaction_enabled': False, 'needaction_counter': False, } - if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model: + if menu.action and menu.action.type in ('ir.actions.act_window','ir.actions.client') and menu.action.res_model: obj = self.pool.get(menu.action.res_model) if obj and obj._needaction: - if menu.action.type == 'ir.actions.act_window': + if menu.action.type=='ir.actions.act_window': dom = menu.action.domain and eval(menu.action.domain, {'uid': uid}) or [] else: dom = eval(menu.action.params_store or '{}', {'uid': uid}).get('domain') @@ -291,7 +286,7 @@ class ir_ui_menu(osv.osv): _columns = { 'name': fields.char('Menu', size=64, required=True, translate=True), 'sequence': fields.integer('Sequence'), - 'child_id': fields.one2many('ir.ui.menu', 'parent_id', 'Child IDs'), + 'child_id' : fields.one2many('ir.ui.menu', 'parent_id','Child IDs'), 'parent_id': fields.many2one('ir.ui.menu', 'Parent Menu', select=True), 'groups_id': fields.many2many('res.groups', 'ir_ui_menu_group_rel', 'menu_id', 'gid', 'Groups', help="If you have groups, the visibility of this menu will be based on these groups. "\ @@ -301,18 +296,11 @@ class ir_ui_menu(osv.osv): 'icon': fields.selection(tools.icons, 'Icon', size=64), 'icon_pict': fields.function(_get_icon_pict, type='char', size=32), 'web_icon': fields.char('Web Icon File', size=128), - 'web_icon_hover': fields.char('Web Icon File (hover)', size=128), + 'web_icon_hover':fields.char('Web Icon File (hover)', size=128), 'web_icon_data': fields.function(_get_image_icon, string='Web Icon Image', type='binary', readonly=True, store=True, multi='icon'), - 'web_icon_hover_data': fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'), - 'needaction_enabled': fields.function(get_needaction_data, - type='boolean', multi='get_needaction_data', - store=True, - string='Target model uses the need action mechanism', - help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.'), - 'needaction_counter': fields.function(get_needaction_data, - type='integer', multi='get_needaction_data', - string='Number of actions the user has to perform', - help='If the target model uses the need action mechanism, this field gives the number of actions the current user has to perform.'), + 'web_icon_hover_data':fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'), + 'needaction_enabled': fields.function(_get_needaction, string='Target model uses the need action mechanism', store=True, type='boolean', help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.', multi='_get_needaction'), + 'needaction_counter': fields.function(_get_needaction, string='Number of actions the user has to perform', type='integer', help='If the target model uses the need action mechanism, this field gives the number of actions the current user has to perform.', multi='_get_needaction'), 'action': fields.function(_action, fnct_inv=_action_inv, type='reference', string='Action', selection=[ From 9381a827158e085b035b8f497467f09ff5f6a5e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 7 Dec 2012 17:09:33 +0100 Subject: [PATCH 38/39] [IMP] Added an assert on the alias length. bzr revid: tde@openerp.com-20121207160933-1ivbkkv6tgg5a7x3 --- openerp/osv/expression.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 8535213b4de..e14e013d536 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -343,6 +343,7 @@ def generate_table_alias(src_table_alias, joined_tables=[]): return ('%s' % alias, '%s' % _quote(alias)) for link in joined_tables: alias += '__' + link[1] + assert len(alias) < 64, 'Table alias name %s is longer than the 64 characters size accepted by default in postgresql.' % (alias) return ('%s' % alias, '%s as %s' % (_quote(joined_tables[-1][0]), _quote(alias))) From bb598cd1052709c4bee4a31405201c4609ff6377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 7 Dec 2012 17:20:19 +0100 Subject: [PATCH 39/39] [CLEAN] orm: added quote around a forgottent table name; cleaned a bit some code and added comments, removed dead code. bzr revid: tde@openerp.com-20121207162019-ec1x7a38vw2rrqze --- openerp/osv/orm.py | 11 ++++++----- openerp/osv/query.py | 8 -------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 54e349b5234..0a28052e51d 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2741,7 +2741,7 @@ class BaseModel(object): :return: qualified name of field, to be used in SELECT clause """ current_table = self - parent_alias = current_table._table + parent_alias = '"%s"' % current_table._table while field in current_table._inherit_fields and not field in current_table._columns: parent_model_name = current_table._inherit_fields[field][0] parent_table = self.pool.get(parent_model_name) @@ -4667,14 +4667,15 @@ class BaseModel(object): # inherited rules are applied on the external table -> need to get the alias and replace parent_table = self.pool.get(parent_model)._table added_clause = [clause.replace('"%s"' % parent_table, '"%s"' % parent_alias) for clause in added_clause] - # not sure of myself here (in the ORM, this statment is quite cool) + # change references to parent_table to parent_alias, because we now use the alias to refer to the table new_tables = [] for table in added_tables: + # table is just a table name -> switch to the full alias if table == '"%s"' % (parent_table): - new_table = '"%s" as "%s"' % (parent_table, parent_alias) + new_tables.append('"%s" as "%s"' % (parent_table, parent_alias)) + # table is already a full statement -> replace reference to the table to its alias, is correct with the way aliases are generated else: - new_table = table.replace('"%s"' % parent_table, '"%s"' % parent_alias) - new_tables.append(new_table) + new_tables.append(table.replace('"%s"' % parent_table, '"%s"' % parent_alias)) added_tables = new_tables query.where_clause += added_clause query.where_clause_params += added_params diff --git a/openerp/osv/query.py b/openerp/osv/query.py index 7f1bd930c91..98a90dcd0bd 100644 --- a/openerp/osv/query.py +++ b/openerp/osv/query.py @@ -28,14 +28,6 @@ def _quote(to_quote): return to_quote -def _get_alias_from_statement(string): - if len(string.split(' as ')) > 1: - alias = string.split(' as ')[1].replace('"', '') - else: - alias = string.replace('"', '') - return alias - - class Query(object): """ Dumb implementation of a Query object, using 3 string lists so far