openerp-dev-web team mailing list archive
-
openerp-dev-web team
-
Mailing list archive
-
Message #00122
[Merge] lp:~openerp-dev/openobject-server/trunk-query-object into lp:openobject-server
Olivier Dony (OpenERP) has proposed merging lp:~openerp-dev/openobject-server/trunk-query-object into lp:openobject-server.
Requested reviews:
Stephane Wirtel (OpenERP) (stephane-openerp): have i missed something in the quick refactoring?
Xavier (Open ERP) (xmo)
Antony Lesuisse (al-openerp): have i missed something in the quick refactoring?
Initial basic implementation of Query object for the ORM:
- added basic Query object to support some more complex ORM use cases (LEFT OUTER JOINS at the moment)
- added trivial unit tests for query object + run_tests.py executable to launch them
- fixed the issue with server-side sorting of m2o columns
Warning: this is very basic and was only done as a low-risk fix to introduce support for OUTER joins, as they are needed in some cases (server-side sorting of m2o columns being one)
This will need to be improved and extended a lot to be really useful -> targeted for after v6.0...
--
https://code.launchpad.net/~openerp-dev/openobject-server/trunk-query-object/+merge/37126
Your team OpenERP R&D Team is subscribed to branch lp:~openerp-dev/openobject-server/trunk-query-object.
=== modified file 'bin/addons/base/ir/ir_rule.py'
--- bin/addons/base/ir/ir_rule.py 2010-09-09 11:02:11 +0000
+++ bin/addons/base/ir/ir_rule.py 2010-09-30 13:37:41 +0000
@@ -137,7 +137,8 @@
def domain_get(self, cr, uid, model_name, mode='read', context={}):
dom = self._compute_domain(cr, uid, model_name, mode=mode)
if dom:
- return self.pool.get(model_name)._where_calc(cr, uid, dom, active_test=False)
+ query = self.pool.get(model_name)._where_calc(cr, uid, dom, active_test=False)
+ return query.where_clause, query.where_clause_params, query.tables
return [], [], ['"'+self.pool.get(model_name)._table+'"']
def unlink(self, cr, uid, ids, context=None):
=== modified file 'bin/osv/orm.py'
--- bin/osv/orm.py 2010-09-29 12:07:21 +0000
+++ bin/osv/orm.py 2010-09-30 13:37:41 +0000
@@ -54,6 +54,7 @@
from tools.translate import _
import fields
+from query import Query
import tools
from tools.safe_eval import safe_eval as eval
@@ -2203,35 +2204,15 @@
if not fields:
fields = self._columns.keys()
- # compute the where, order by, limit and offset clauses
- (where_clause, where_clause_params, tables) = self._where_calc(cr, uid, domain, context=context)
-
- # apply direct ir.rules from current model
- self._apply_ir_rules(cr, uid, where_clause, where_clause_params, tables, 'read', context=context)
-
- # then apply the ir.rules from the parents (through _inherits), adding the appropriate JOINs if needed
- for inherited_model in self._inherits:
- previous_tables = list(tables)
- if self._apply_ir_rules(cr, uid, where_clause, where_clause_params, tables, 'read', model_name=inherited_model, context=context):
- # if some rules were applied, need to add the missing JOIN for them to make sense, passing the previous
- # list of table in case the inherited table was not in the list before (as that means the corresponding
- # JOIN(s) was(were) not present)
- self._inherits_join_add(inherited_model, previous_tables, where_clause)
- tables = list(set(tables).union(set(previous_tables)))
+ query = self._where_calc(cr, uid, domain, context=context)
+ self._apply_ir_rules(cr, uid, query, 'read', context=context)
# Take care of adding join(s) if groupby is an '_inherits'ed field
groupby_list = groupby
if groupby:
if isinstance(groupby, list):
groupby = groupby[0]
- tables, where_clause, qfield = self._inherits_join_calc(groupby, tables, where_clause)
-
- if len(where_clause):
- where_clause = ' where ' + ' and '.join(where_clause)
- else:
- where_clause = ''
- limit_str = limit and ' limit %d' % limit or ''
- offset_str = offset and ' offset %d' % offset or ''
+ self._inherits_join_calc(groupby, query)
assert not groupby or groupby in fields, "Fields in 'groupby' must appear in the list of fields to read (perhaps it's missing in the list view?)"
@@ -2263,8 +2244,13 @@
flist += ','
flist += operator+'('+f+') as '+f
- gb = groupby and (' group by '+groupby) or ''
- cr.execute('select min(%s.id) as id,' % self._table + flist + ' from ' + ','.join(tables) + where_clause + gb + limit_str + offset_str, where_clause_params)
+ gb = groupby and (' GROUP BY '+groupby) or ''
+
+ from_clause, where_clause, where_clause_params = query.get_sql()
+ where_clause = where_clause and ' WHERE ' + where_clause
+ limit_str = limit and ' limit %d' % limit or ''
+ offset_str = offset and ' offset %d' % offset or ''
+ cr.execute('SELECT min(%s.id) AS id,' % self._table + flist + ' FROM ' + from_clause + where_clause + gb + limit_str + offset_str, where_clause_params)
alldata = {}
groupby = group_by
for r in cr.dictfetchall():
@@ -2303,43 +2289,37 @@
del d['id']
return data
- def _inherits_join_add(self, parent_model_name, tables, where_clause):
+ def _inherits_join_add(self, parent_model_name, query):
"""
- Add missing table SELECT and JOIN clause for reaching the parent table (no duplicates)
+ Add missing table SELECT and JOIN clause to ``query`` for reaching the parent table (no duplicates)
:param parent_model_name: name of the parent model for which the clauses should be added
- :param tables: list of table._table names enclosed in double quotes as returned
- by _where_calc()
- :param where_clause: current list of WHERE clause params
+ :param query: query object on which the JOIN should be added
"""
inherits_field = self._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 tables:
- tables.append(quoted_parent_table_name)
- where_clause.append('("%s".%s = %s.id)' % (self._table, inherits_field, parent_table_name))
- return (tables, where_clause)
+ if quoted_parent_table_name not in query.tables:
+ query.tables.append(quoted_parent_table_name)
+ query.where_clause.append('("%s".%s = %s.id)' % (self._table, inherits_field, parent_table_name))
- def _inherits_join_calc(self, field, tables, where_clause):
+ def _inherits_join_calc(self, field, query):
"""
- Adds missing table select and join clause(s) for reaching
+ Adds missing table select and join clause(s) to ``query`` for reaching
the field coming from an '_inherits' parent table (no duplicates).
- :param tables: list of table._table names enclosed in double quotes as returned
- by _where_calc()
- :param where_clause: current list of WHERE clause params
- :return: (tables, where_clause, qualified_field) where ``tables`` and ``where_clause`` are the updated
- versions of the parameters, and ``qualified_field`` is the qualified name of ``field``
- in the form ``table.field``, to be referenced in queries.
+ :param field: name of inherited field to reach
+ :param query: query object on which the JOIN should be added
+ :return: qualified name of field, to be used in SELECT clause
"""
current_table = self
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(parent_model_name, tables, where_clause)
+ self._inherits_join_add(parent_model_name, query)
current_table = parent_table
- return (tables, where_clause, '"%s".%s' % (current_table._table, field))
+ return '"%s".%s' % (current_table._table, field)
def _parent_store_compute(self, cr):
if not self._parent_store:
@@ -3899,84 +3879,87 @@
raise NotImplementedError(_('This method does not exist anymore'))
# TODO: ameliorer avec NULL
- def _where_calc(self, cr, user, args, active_test=True, context=None):
+ def _where_calc(self, cr, user, domain, active_test=True, context=None):
"""Computes the WHERE clause needed to implement an OpenERP domain.
- :param args: the domain to compute
- :type args: list
+ :param domain: the domain to compute
+ :type domain: list
:param active_test: whether the default filtering of records with ``active``
field set to ``False`` should be applied.
- :return: tuple with 3 elements: (where_clause, where_clause_params, tables) where
- ``where_clause`` contains a list of where clause elements (to be joined with 'AND'),
- ``where_clause_params`` is a list of parameters to be passed to the db layer
- for the where_clause expansion, and ``tables`` is the list of double-quoted
- table names that need to be included in the FROM clause.
- :rtype: tuple
+ :return: the query expressing the given domain as provided in domain
+ :rtype: osv.query.Query
"""
if not context:
context = {}
- args = args[:]
+ domain = domain[:]
# if the object has a field named 'active', filter out all inactive
# records unless they were explicitely asked for
if 'active' in self._columns and (active_test and context.get('active_test', True)):
- if args:
+ if domain:
active_in_args = False
- for a in args:
+ for a in domain:
if a[0] == 'active':
active_in_args = True
if not active_in_args:
- args.insert(0, ('active', '=', 1))
+ domain.insert(0, ('active', '=', 1))
else:
- args = [('active', '=', 1)]
+ domain = [('active', '=', 1)]
- if args:
+ if domain:
import expression
- e = expression.expression(args)
+ e = expression.expression(domain)
e.parse(cr, user, self, context)
tables = e.get_tables()
- qu1, qu2 = e.to_sql()
- qu1 = qu1 and [qu1] or []
+ where_clause, where_params = e.to_sql()
+ where_clause = where_clause and [where_clause] or []
else:
- qu1, qu2, tables = [], [], ['"%s"' % self._table]
+ where_clause, where_params, tables = [], [], ['"%s"' % self._table]
- return (qu1, qu2, tables)
+ return Query(tables, where_clause, where_params)
def _check_qorder(self, word):
if not regex_order.match(word):
raise except_orm(_('AccessError'), _('Invalid "order" specified. A valid "order" specification is a comma-separated list of valid field names (optionally followed by asc/desc for the direction)'))
return True
- def _apply_ir_rules(self, cr, uid, where_clause, where_clause_params, tables, mode='read', model_name=None, context=None):
- """Add what's missing in ``where_clause``, ``where_params``, ``tables`` to implement
- all appropriate ir.rules (on the current object but also from it's _inherits parents)
-
- :param where_clause: list with current elements of the WHERE clause (strings)
- :param where_clause_params: list with parameters for ``where_clause``
- :param tables: list with double-quoted names of the tables that are joined
- in ``where_clause``
- :param model_name: optional name of the model whose ir.rules should be applied (default:``self._name``)
- This could be useful for inheritance for example, but there is no provision to include
- the appropriate JOIN for linking the current model to the one referenced in model_name.
- :return: True if additional clauses where applied.
- """
- added_clause, added_params, added_tables = self.pool.get('ir.rule').domain_get(cr, uid, model_name or self._name, mode, context=context)
- if added_clause:
- where_clause += added_clause
- where_clause_params += added_params
- for table in added_tables:
- if table not in tables:
- tables.append(table)
- return True
- return False
-
- def _generate_m2o_order_by(self, order_field, tables, where_clause):
- """
- Add possibly missing JOIN and generate the ORDER BY clause for m2o fields,
+ def _apply_ir_rules(self, cr, uid, query, mode='read', context=None):
+ """Add what's missing in ``query`` to implement all appropriate ir.rules
+ (using the ``model_name``'s rules or the current model's rules if ``model_name`` is None)
+
+ :param query: the current query object
+ """
+ def apply_rule(added_clause, added_params, added_tables):
+ if added_clause:
+ 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)
+ 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))
+
+ # apply ir.rules from the parents (through _inherits), adding the appropriate JOINs if needed
+ for inherited_model in self._inherits:
+ if apply_rule(*rule_obj.domain_get(cr, uid, inherited_model, mode, context=context)):
+ # if some rules were applied, need to add the missing JOIN for them to make sense, passing the previous
+ # list of table in case the inherited table was not in the list before (as that means the corresponding
+ # JOIN(s) was(were) not present)
+ self._inherits_join_add(inherited_model, query)
+
+ def _generate_m2o_order_by(self, order_field, query):
+ """
+ Add possibly missing JOIN to ``query`` and generate the ORDER BY clause for m2o fields,
either native m2o fields or function/related fields that are stored, including
intermediate JOINs for inheritance if required.
+
+ :return: the qualified field name to use in an ORDER BY clause to sort by ``order_field``
"""
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
- tables, where_clause, qualified_field = self._inherits_join_calc(order_field, tables, where_clause)
+ qualified_field = self._inherits_join_calc(order_field, query)
order_field_column = self._inherit_fields[order_field][2]
else:
qualified_field = '"%s"."%s"' % (self._table, order_field)
@@ -3995,16 +3978,14 @@
# extract the first field name, to be able to qualify it and add desc/asc
m2o_order = m2o_order.split(",",1)[0].strip().split(" ",1)[0]
- # the perhaps missing join:
- quoted_model_table = '"%s"' % dest_model._table
- if quoted_model_table not in tables:
- tables.append(quoted_model_table)
- where_clause.append('%s = %s.id' % (qualified_field, quoted_model_table))
-
- return ('%s.%s' % (quoted_model_table, m2o_order), tables, where_clause)
-
-
- def _generate_order_by(self, order_spec, tables, where_clause):
+ # 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)
+ return '"%s"."%s"' % (dest_model._table, 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
a comma-separated list of valid field names, optionally followed by an ASC or DESC direction.
@@ -4024,16 +4005,16 @@
if order_column._classic_read:
order_by_clause = '"%s"."%s"' % (self._table, order_field)
elif order_column._type == 'many2one':
- order_by_clause, tables, where_clause = self._generate_m2o_order_by(order_field, tables, where_clause)
+ order_by_clause = self._generate_m2o_order_by(order_field, query)
else:
continue # ignore non-readable or "non-joignable" fields
elif order_field in self._inherit_fields:
parent_obj = self.pool.get(self._inherit_fields[order_field][0])
order_column = parent_obj._columns[order_field]
if order_column._classic_read:
- tables, where_clause, order_by_clause = self._inherits_join_calc(order_field, tables, where_clause)
+ order_by_clause = self._inherits_join_calc(order_field, query)
elif order_column._type == 'many2one':
- order_by_clause, tables, where_clause = self._generate_m2o_order_by(order_field, tables, where_clause)
+ order_by_clause = self._generate_m2o_order_by(order_field, query)
else:
continue # ignore non-readable or "non-joignable" fields
order_by_elements.append("%s %s" % (order_by_clause, order_direction))
@@ -4054,34 +4035,21 @@
if context is None:
context = {}
self.pool.get('ir.model.access').check(cr, access_rights_uid or user, self._name, 'read', context=context)
- # compute the where, order by, limit and offset clauses
- (where_clause, where_clause_params, tables) = self._where_calc(cr, user, args, context=context)
-
- # apply direct ir.rules from current model
- self._apply_ir_rules(cr, user, where_clause, where_clause_params, tables, 'read', context=context)
-
- # then apply the ir.rules from the parents (through _inherits), adding the appropriate JOINs if needed
- for inherited_model in self._inherits:
- previous_tables = list(tables)
- if self._apply_ir_rules(cr, user, where_clause, where_clause_params, tables, 'read', model_name=inherited_model, context=context):
- # if some rules were applied, need to add the missing JOIN for them to make sense, passing the previous
- # list of table in case the inherited table was not in the list before (as that means the corresponding
- # JOIN(s) was(were) not present)
- self._inherits_join_add(inherited_model, previous_tables, where_clause)
- tables = list(set(tables).union(set(previous_tables)))
-
- where = where_clause
- order_by = self._generate_order_by(order, tables, where_clause)
+
+ query = self._where_calc(cr, user, args, context=context)
+ self._apply_ir_rules(cr, user, query, 'read', context=context)
+ order_by = self._generate_order_by(order, query)
+ from_clause, where_clause, where_clause_params = query.get_sql()
+
limit_str = limit and ' limit %d' % limit or ''
offset_str = offset and ' offset %d' % offset or ''
- where_str = where and (" WHERE %s" % " AND ".join(where)) or ''
+ where_str = where_clause and (" WHERE %s" % where_clause) or ''
if count:
- cr.execute('select count(%s.id) from ' % self._table +
- ','.join(tables) + where_str + limit_str + offset_str, where_clause_params)
+ cr.execute('SELECT count("%s".id) FROM ' % self._table + from_clause + where_str + limit_str + offset_str, where_clause_params)
res = cr.fetchall()
return res[0][0]
- cr.execute('select %s.id from ' % self._table + ','.join(tables) + where_str + order_by + limit_str+offset_str, where_clause_params)
+ cr.execute('SELECT "%s".id FROM ' % self._table + from_clause + where_str + order_by + limit_str + offset_str, where_clause_params)
res = cr.fetchall()
return [x[0] for x in res]
=== added file 'bin/osv/query.py'
--- bin/osv/query.py 1970-01-01 00:00:00 +0000
+++ bin/osv/query.py 2010-09-30 13:37:41 +0000
@@ -0,0 +1,116 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+
+def _quote(to_quote):
+ if '"' not in to_quote:
+ return '"%s"' % to_quote
+ return to_quote
+
+
+class Query(object):
+ """
+ Dumb implementation of a Query object, using 3 string lists so far
+ for backwards compatibility with the (table, where_clause, where_params) previously used.
+
+ TODO: To be improved after v6.0 to rewrite part of the ORM and add support for:
+ - auto-generated multiple table aliases
+ - multiple joins to the same table with different conditions
+ - dynamic right-hand-side values in domains (e.g. a.name = a.description)
+ - etc.
+ """
+
+ def __init__(self, tables=None, where_clause=None, where_clause_params=None, joins=None):
+
+ # holds the list of tables joined using default JOIN.
+ # the table names are stored double-quoted (backwards compatibility)
+ self.tables = tables or []
+
+ # holds the list of WHERE clause elements, to be joined with
+ # 'AND' when generating the final query
+ self.where_clause = where_clause or []
+
+ # holds the parameters for the formatting of `where_clause`, to be
+ # passed to psycopg's execute method.
+ self.where_clause_params = where_clause_params or []
+
+ # holds table joins done explicitly, supporting outer joins. The JOIN
+ # condition should not be in `where_clause`. The dict is used as follows:
+ # self.joins = {
+ # 'table_a': [
+ # ('table_b', 'table_a_col1', 'table_b_col', 'LEFT JOIN'),
+ # ('table_c', 'table_a_col2', 'table_c_col', 'LEFT JOIN'),
+ # ('table_d', 'table_a_col3', 'table_d_col', 'JOIN'),
+ # ]
+ # }
+ # which should lead to the following SQL:
+ # SELECT ... FROM "table_a" LEFT JOIN "table_b" ON ("table_a"."table_a_col1" = "table_b"."table_b_col")
+ # 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):
+ """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)``
+
+ :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)
+ """
+ (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:
+ # 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 get_sql(self):
+ """Returns (query_from, query_where, query_params)"""
+ query_from = ''
+ tables_to_process = list(self.tables)
+ for table in tables_to_process:
+ query_from += ' %s ' % table
+ if table in self.joins:
+ for (dest_table, lhs_col, col, join) in self.joins[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)
+ query_from += ','
+ query_from = query_from[:-1] # drop last comma
+ return (query_from, " AND ".join(self.where_clause), self.where_clause_params)
+
+ def __str__(self):
+ return '<osv.Query: "SELECT ... FROM %s WHERE %s" with params: %r>' % self.get_sql()
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
=== added file 'bin/run_tests.py'
--- bin/run_tests.py 1970-01-01 00:00:00 +0000
+++ bin/run_tests.py 2010-09-30 13:37:41 +0000
@@ -0,0 +1,27 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import unittest
+
+import test
+
+if __name__ == '__main__':
+ unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromModule(test))
=== added directory 'bin/test'
=== added file 'bin/test/__init__.py'
--- bin/test/__init__.py 1970-01-01 00:00:00 +0000
+++ bin/test/__init__.py 2010-09-30 13:37:41 +0000
@@ -0,0 +1,22 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from test_osv import *
=== added file 'bin/test/test_osv.py'
--- bin/test/test_osv.py 1970-01-01 00:00:00 +0000
+++ bin/test/test_osv.py 2010-09-30 13:37:41 +0000
@@ -0,0 +1,40 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import unittest
+from osv.query import Query
+
+class QueryTestCase(unittest.TestCase):
+
+ def test_basic_query(self):
+ 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
+ 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(), "Incorrect query")
+ self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip(), "Incorrect where clause")
+
+ 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)
\ No newline at end of file
Follow ups