# HG changeset patch # User Sylvain Thénault # Date 1347291382 -7200 # Node ID 7e264ce34cd44e24328fa84792a7371c53b46034 # Parent 5b6bc27ece6e1fd95e2c54d391a28c92533ebc31 [session / querier] reorganize code to building result set descriptions diff -r 5b6bc27ece6e -r 7e264ce34cd4 server/querier.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 diff -r 5b6bc27ece6e -r 7e264ce34cd4 server/session.py --- 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 diff -r 5b6bc27ece6e -r 7e264ce34cd4 server/test/unittest_querier.py --- 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) diff -r 5b6bc27ece6e -r 7e264ce34cd4 server/test/unittest_session.py --- 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 . 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):