[session / querier] reorganize code to building result set descriptions
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 10 Sep 2012 17:36:22 +0200
changeset 8542 7e264ce34cd4
parent 8541 5b6bc27ece6e
child 8543 b7c9443d8625
[session / querier] reorganize code to building result set descriptions
server/querier.py
server/session.py
server/test/unittest_querier.py
server/test/unittest_session.py
--- a/server/querier.py	Tue Sep 11 12:44:33 2012 +0200
+++ b/server/querier.py	Mon Sep 10 17:36:22 2012 +0200
@@ -26,22 +26,28 @@
 from itertools import repeat
 
 from logilab.common.compat import any
-from rql import RQLSyntaxError
+from rql import RQLSyntaxError, CoercionError
 from rql.stmts import Union, Select
+from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj
 from rql.nodes import (Relation, VariableRef, Constant, SubQuery, Function,
                        Exists, Not)
+from yams import BASE_TYPES
 
 from cubicweb import ValidationError, Unauthorized, QueryError, UnknownEid
-from cubicweb import server, typed_eid
+from cubicweb import Binary, server, typed_eid
 from cubicweb.rset import ResultSet
 
-from cubicweb.utils import QueryCache
+from cubicweb.utils import QueryCache, RepeatList
 from cubicweb.server.utils import cleanup_solutions
 from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata
 from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction
 from cubicweb.server.edition import EditedEntity
 from cubicweb.server.session import security_enabled
 
+
+ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
+
+
 def empty_rset(rql, args, rqlst=None):
     """build an empty result set object"""
     return ResultSet([], rql, args, rqlst=rqlst)
@@ -751,14 +757,22 @@
         if build_descr:
             if rqlst.TYPE == 'select':
                 # sample selection
-                descr = session.build_description(orig_rqlst, args, results)
+                if len(rqlst.children) == 1 and len(rqlst.children[0].solutions) == 1:
+                    # easy, all lines are identical
+                    selected = rqlst.children[0].selection
+                    solution = rqlst.children[0].solutions[0]
+                    description = _make_description(selected, args, solution)
+                    descr = RepeatList(len(results), tuple(description))
+                else:
+                    # hard, delegate the work :o)
+                    descr = manual_build_descr(session, rqlst, args, results)
             elif rqlst.TYPE == 'insert':
                 # on insert plan, some entities may have been auto-casted,
                 # so compute description manually even if there is only
                 # one solution
                 basedescr = [None] * len(plan.selected)
                 todetermine = zip(xrange(len(plan.selected)), repeat(False))
-                descr = session._build_descr(results, basedescr, todetermine)
+                descr = _build_descr(session, results, basedescr, todetermine)
             # FIXME: get number of affected entities / relations on non
             # selection queries ?
         # return a result set object
@@ -772,3 +786,77 @@
 from cubicweb import set_log_methods
 LOGGER = getLogger('cubicweb.querier')
 set_log_methods(QuerierHelper, LOGGER)
+
+
+def manual_build_descr(tx, rqlst, args, result):
+    """build a description for a given result by analysing each row
+
+    XXX could probably be done more efficiently during execution of query
+    """
+    # not so easy, looks for variable which changes from one solution
+    # to another
+    unstables = rqlst.get_variable_indices()
+    basedescr = []
+    todetermine = []
+    for i in xrange(len(rqlst.children[0].selection)):
+        ttype = _selection_idx_type(i, rqlst, args)
+        if ttype is None or ttype == 'Any':
+            ttype = None
+            isfinal = True
+        else:
+            isfinal = ttype in BASE_TYPES
+        if ttype is None or i in unstables:
+            basedescr.append(None)
+            todetermine.append( (i, isfinal) )
+        else:
+            basedescr.append(ttype)
+    if not todetermine:
+        return RepeatList(len(result), tuple(basedescr))
+    return _build_descr(tx, result, basedescr, todetermine)
+
+def _build_descr(tx, result, basedescription, todetermine):
+    description = []
+    etype_from_eid = tx.describe
+    todel = []
+    for i, row in enumerate(result):
+        row_descr = basedescription[:]
+        for index, isfinal in todetermine:
+            value = row[index]
+            if value is None:
+                # None value inserted by an outer join, no type
+                row_descr[index] = None
+                continue
+            if isfinal:
+                row_descr[index] = etype_from_pyobj(value)
+            else:
+                try:
+                    row_descr[index] = etype_from_eid(value)[0]
+                except UnknownEid:
+                    tx.error('wrong eid %s in repository, you should '
+                             'db-check the database' % value)
+                    todel.append(i)
+                    break
+        else:
+            description.append(tuple(row_descr))
+    for i in reversed(todel):
+        del result[i]
+    return description
+
+def _make_description(selected, args, solution):
+    """return a description for a result set"""
+    description = []
+    for term in selected:
+        description.append(term.get_type(solution, args))
+    return description
+
+def _selection_idx_type(i, rqlst, args):
+    """try to return type of term at index `i` of the rqlst's selection"""
+    for select in rqlst.children:
+        term = select.selection[i]
+        for solution in select.solutions:
+            try:
+                ttype = term.get_type(solution, args)
+                if ttype is not None:
+                    return ttype
+            except CoercionError:
+                return None
--- a/server/session.py	Tue Sep 11 12:44:33 2012 +0200
+++ b/server/session.py	Mon Sep 10 17:36:22 2012 +0200
@@ -30,21 +30,16 @@
 from logilab.common.deprecation import deprecated
 from logilab.common.textutils import unormalize
 from logilab.common.registry import objectify_predicate
-from rql import CoercionError
-from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj
-from yams import BASE_TYPES
 
-from cubicweb import Binary, UnknownEid, QueryError, schema
+from cubicweb import UnknownEid, QueryError, schema
 from cubicweb.req import RequestSessionBase
 from cubicweb.dbapi import ConnectionProperties
-from cubicweb.utils import make_uid, RepeatList
+from cubicweb.utils import make_uid
 from cubicweb.rqlrewrite import RQLRewriter
 from cubicweb.server import ShuttingDown
 from cubicweb.server.edition import EditedEntity
 
 
-ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
-
 NO_UNDO_TYPES = schema.SCHEMA_TYPES.copy()
 NO_UNDO_TYPES.add('CWCache')
 # is / is_instance_of are usually added by sql hooks except when using
@@ -55,25 +50,6 @@
 NO_UNDO_TYPES.add('cw_source')
 # XXX rememberme,forgotpwd,apycot,vcsfile
 
-def _make_description(selected, args, solution):
-    """return a description for a result set"""
-    description = []
-    for term in selected:
-        description.append(term.get_type(solution, args))
-    return description
-
-def selection_idx_type(i, rqlst, args):
-    """try to return type of term at index `i` of the rqlst's selection"""
-    for select in rqlst.children:
-        term = select.selection[i]
-        for solution in select.solutions:
-            try:
-                ttype = term.get_type(solution, args)
-                if ttype is not None:
-                    return ttype
-            except CoercionError:
-                return None
-
 @objectify_predicate
 def is_user_session(cls, req, **kwargs):
     """repository side only predicate returning 1 if the session is a regular
--- a/server/test/unittest_querier.py	Tue Sep 11 12:44:33 2012 +0200
+++ b/server/test/unittest_querier.py	Mon Sep 10 17:36:22 2012 +0200
@@ -29,10 +29,10 @@
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.utils import crypt_password
 from cubicweb.server.sources.native import make_schema
+from cubicweb.server.querier import manual_build_descr, _make_description
 from cubicweb.devtools import get_test_db_handler, TestServerConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.repotest import tuplify, BaseQuerierTC
-from unittest_session import Variable
 
 class FixedOffset(tzinfo):
     def __init__(self, hours=0):
@@ -87,6 +87,30 @@
     del repo, cnx
 
 
+class Variable:
+    def __init__(self, name):
+        self.name = name
+        self.children = []
+
+    def get_type(self, solution, args=None):
+        return solution[self.name]
+    def as_string(self):
+        return self.name
+
+class Function:
+    def __init__(self, name, varname):
+        self.name = name
+        self.children = [Variable(varname)]
+    def get_type(self, solution, args=None):
+        return 'Int'
+
+class MakeDescriptionTC(TestCase):
+    def test_known_values(self):
+        solution = {'A': 'Int', 'B': 'CWUser'}
+        self.assertEqual(_make_description((Function('max', 'A'), Variable('B')), {}, solution),
+                          ['Int','CWUser'])
+
+
 class UtilsTC(BaseQuerierTC):
     setUpClass = classmethod(setUpClass)
     tearDownClass = classmethod(tearDownClass)
@@ -242,6 +266,28 @@
         rset = self.execute('Any %(x)s', {'x': u'str'})
         self.assertEqual(rset.description[0][0], 'String')
 
+    def test_build_descr1(self):
+        rset = self.execute('(Any U,L WHERE U login L) UNION (Any G,N WHERE G name N, G is CWGroup)')
+        rset.req = self.transaction
+        orig_length = len(rset)
+        rset.rows[0][0] = 9999999
+        description = manual_build_descr(rset.req, rset.syntax_tree(), None, rset.rows)
+        self.assertEqual(len(description), orig_length - 1)
+        self.assertEqual(len(rset.rows), orig_length - 1)
+        self.assertNotEqual(rset.rows[0][0], 9999999)
+
+    def test_build_descr2(self):
+        rset = self.execute('Any X,Y WITH X,Y BEING ((Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G))')
+        for x, y in rset.description:
+            if y is not None:
+                self.assertEqual(y, 'CWGroup')
+
+    def test_build_descr3(self):
+        rset = self.execute('(Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G)')
+        for x, y in rset.description:
+            if y is not None:
+                self.assertEqual(y, 'CWGroup')
+
 
 class QuerierTC(BaseQuerierTC):
     setUpClass = classmethod(setUpClass)
--- a/server/test/unittest_session.py	Tue Sep 11 12:44:33 2012 +0200
+++ b/server/test/unittest_session.py	Mon Sep 10 17:36:22 2012 +0200
@@ -17,33 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 from __future__ import with_statement
 
-from logilab.common.testlib import TestCase, unittest_main, mock_object
-
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.server.session import _make_description, hooks_control
-
-class Variable:
-    def __init__(self, name):
-        self.name = name
-        self.children = []
-
-    def get_type(self, solution, args=None):
-        return solution[self.name]
-    def as_string(self):
-        return self.name
-
-class Function:
-    def __init__(self, name, varname):
-        self.name = name
-        self.children = [Variable(varname)]
-    def get_type(self, solution, args=None):
-        return 'Int'
-
-class MakeDescriptionTC(TestCase):
-    def test_known_values(self):
-        solution = {'A': 'Int', 'B': 'CWUser'}
-        self.assertEqual(_make_description((Function('max', 'A'), Variable('B')), {}, solution),
-                          ['Int','CWUser'])
+from cubicweb.server.session import hooks_control
 
 
 class InternalSessionTC(CubicWebTC):