--- 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):