--- a/server/querier.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,737 +0,0 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb 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 Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Helper classes to execute RQL queries on a set of sources, performing
-security checking and data aggregation.
-"""
-from __future__ import print_function
-
-__docformat__ = "restructuredtext en"
-
-from itertools import repeat
-
-from six import text_type, string_types, integer_types
-from six.moves import range
-
-from rql import RQLSyntaxError, CoercionError
-from rql.stmts import Union
-from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj, Relation, Exists, Not
-from yams import BASE_TYPES
-
-from cubicweb import ValidationError, Unauthorized, UnknownEid
-from cubicweb.rqlrewrite import RQLRelationRewriter
-from cubicweb import Binary, server
-from cubicweb.rset import ResultSet
-
-from cubicweb.utils import QueryCache, RepeatList
-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.ssplanner import SSPlanner
-from cubicweb.statsd_logger import statsd_timeit, statsd_c
-
-ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
-
-
-def empty_rset(rql, args, rqlst=None):
- """build an empty result set object"""
- return ResultSet([], rql, args, rqlst=rqlst)
-
-def update_varmap(varmap, selected, table):
- """return a sql schema to store RQL query result"""
- for i, term in enumerate(selected):
- key = term.as_string()
- value = '%s.C%s' % (table, i)
- if varmap.get(key, value) != value:
- raise Exception('variable name conflict on %s: got %s / %s'
- % (key, value, varmap))
- varmap[key] = value
-
-# permission utilities ########################################################
-
-def check_no_password_selected(rqlst):
- """check that Password entities are not selected"""
- for solution in rqlst.solutions:
- for var, etype in solution.items():
- if etype == 'Password':
- raise Unauthorized('Password selection is not allowed (%s)' % var)
-
-def term_etype(cnx, term, solution, args):
- """return the entity type for the given term (a VariableRef or a Constant
- node)
- """
- try:
- return solution[term.name]
- except AttributeError:
- return cnx.entity_metas(term.eval(args))['type']
-
-def check_relations_read_access(cnx, select, args):
- """Raise :exc:`Unauthorized` if the given user doesn't have credentials to
- read relations used in the given syntax tree
- """
- # use `term_etype` since we've to deal with rewritten constants here,
- # when used as an external source by another repository.
- # XXX what about local read security w/ those rewritten constants...
- # XXX constants can also happen in some queries generated by req.find()
- DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS
- schema = cnx.repo.schema
- user = cnx.user
- if select.where is not None:
- for rel in select.where.iget_nodes(Relation):
- for solution in select.solutions:
- # XXX has_text may have specific perm ?
- if rel.r_type in READ_ONLY_RTYPES:
- continue
- rschema = schema.rschema(rel.r_type)
- if rschema.final:
- eschema = schema.eschema(term_etype(cnx, rel.children[0],
- solution, args))
- rdef = eschema.rdef(rschema)
- else:
- rdef = rschema.rdef(term_etype(cnx, rel.children[0],
- solution, args),
- term_etype(cnx, rel.children[1].children[0],
- solution, args))
- if not user.matching_groups(rdef.get_groups('read')):
- if DBG:
- print('check_read_access: %s %s does not match %s' %
- (rdef, user.groups, rdef.get_groups('read')))
- # XXX rqlexpr not allowed
- raise Unauthorized('read', rel.r_type)
- if DBG:
- print('check_read_access: %s %s matches %s' %
- (rdef, user.groups, rdef.get_groups('read')))
-
-def get_local_checks(cnx, rqlst, solution):
- """Check that the given user has credentials to access data read by the
- query and return a dict defining necessary "local checks" (i.e. rql
- expression in read permission defined in the schema) where no group grants
- him the permission.
-
- Returned dictionary's keys are variable names and values the rql expressions
- for this variable (with the given solution).
-
- Raise :exc:`Unauthorized` if access is known to be defined, i.e. if there is
- no matching group and no local permissions.
- """
- DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS
- schema = cnx.repo.schema
- user = cnx.user
- localchecks = {}
- # iterate on defined_vars and not on solutions to ignore column aliases
- for varname in rqlst.defined_vars:
- eschema = schema.eschema(solution[varname])
- if eschema.final:
- continue
- if not user.matching_groups(eschema.get_groups('read')):
- erqlexprs = eschema.get_rqlexprs('read')
- if not erqlexprs:
- ex = Unauthorized('read', solution[varname])
- ex.var = varname
- if DBG:
- print('check_read_access: %s %s %s %s' %
- (varname, eschema, user.groups, eschema.get_groups('read')))
- raise ex
- # don't insert security on variable only referenced by 'NOT X relation Y' or
- # 'NOT EXISTS(X relation Y)'
- varinfo = rqlst.defined_vars[varname].stinfo
- if varinfo['selected'] or (
- len([r for r in varinfo['relations']
- if (not schema.rschema(r.r_type).final
- and ((isinstance(r.parent, Exists) and r.parent.neged(strict=True))
- or isinstance(r.parent, Not)))])
- !=
- len(varinfo['relations'])):
- localchecks[varname] = erqlexprs
- return localchecks
-
-
-# Plans #######################################################################
-
-class ExecutionPlan(object):
- """the execution model of a rql query, composed of querier steps"""
-
- def __init__(self, querier, rqlst, args, cnx):
- # original rql syntax tree
- self.rqlst = rqlst
- self.args = args or {}
- # cnx executing the query
- self.cnx = cnx
- # quick reference to the system source
- self.syssource = cnx.repo.system_source
- # execution steps
- self.steps = []
- # various resource accesors
- self.querier = querier
- self.schema = querier.schema
- self.sqlannotate = querier.sqlgen_annotate
- self.rqlhelper = cnx.vreg.rqlhelper
-
- def annotate_rqlst(self):
- if not self.rqlst.annotated:
- self.rqlhelper.annotate(self.rqlst)
-
- def add_step(self, step):
- """add a step to the plan"""
- self.steps.append(step)
-
- def sqlexec(self, sql, args=None):
- return self.syssource.sqlexec(self.cnx, sql, args)
-
- def execute(self):
- """execute a plan and return resulting rows"""
- for step in self.steps:
- result = step.execute()
- # the latest executed step contains the full query result
- return result
-
- def preprocess(self, union, security=True):
- """insert security when necessary then annotate rql st for sql generation
-
- return rqlst to actually execute
- """
- cached = None
- if security and self.cnx.read_security:
- # ensure security is turned of when security is inserted,
- # else we may loop for ever...
- if self.cnx.transaction_data.get('security-rqlst-cache'):
- key = self.cache_key
- else:
- key = None
- if key is not None and key in self.cnx.transaction_data:
- cachedunion, args = self.cnx.transaction_data[key]
- union.children[:] = []
- for select in cachedunion.children:
- union.append(select)
- union.has_text_query = cachedunion.has_text_query
- args.update(self.args)
- self.args = args
- cached = True
- else:
- with self.cnx.security_enabled(read=False):
- noinvariant = self._insert_security(union)
- if key is not None:
- self.cnx.transaction_data[key] = (union, self.args)
- else:
- noinvariant = ()
- if cached is None:
- self.rqlhelper.simplify(union)
- self.sqlannotate(union)
- set_qdata(self.schema.rschema, union, noinvariant)
- if union.has_text_query:
- self.cache_key = None
-
- def _insert_security(self, union):
- noinvariant = set()
- for select in union.children[:]:
- for subquery in select.with_:
- self._insert_security(subquery.query)
- localchecks, restricted = self._check_permissions(select)
- if any(localchecks):
- self.cnx.rql_rewriter.insert_local_checks(
- select, self.args, localchecks, restricted, noinvariant)
- return noinvariant
-
- def _check_permissions(self, rqlst):
- """Return a dict defining "local checks", i.e. RQLExpression defined in
- the schema that should be inserted in the original query, together with
- a set of variable names which requires some security to be inserted.
-
- Solutions where a variable has a type which the user can't definitly
- read are removed, else if the user *may* read it (i.e. if an rql
- expression is defined for the "read" permission of the related type),
- the local checks dict is updated.
-
- The local checks dict has entries for each different local check
- necessary, with associated solutions as value, a local check being
- defined by a list of 2-uple (variable name, rql expressions) for each
- variable which has to be checked. Solutions which don't require local
- checks will be associated to the empty tuple key.
-
- Note rqlst should not have been simplified at this point.
- """
- cnx = self.cnx
- msgs = []
- # dict(varname: eid), allowing to check rql expression for variables
- # which have a known eid
- varkwargs = {}
- if not cnx.transaction_data.get('security-rqlst-cache'):
- for var in rqlst.defined_vars.values():
- if var.stinfo['constnode'] is not None:
- eid = var.stinfo['constnode'].eval(self.args)
- varkwargs[var.name] = int(eid)
- # dictionary of variables restricted for security reason
- localchecks = {}
- restricted_vars = set()
- newsolutions = []
- for solution in rqlst.solutions:
- try:
- localcheck = get_local_checks(cnx, rqlst, solution)
- except Unauthorized as ex:
- msg = 'remove %s from solutions since %s has no %s access to %s'
- msg %= (solution, cnx.user.login, ex.args[0], ex.args[1])
- msgs.append(msg)
- LOGGER.info(msg)
- else:
- newsolutions.append(solution)
- # try to benefit of rqlexpr.check cache for entities which
- # are specified by eid in query'args
- for varname, eid in varkwargs.items():
- try:
- rqlexprs = localcheck.pop(varname)
- except KeyError:
- continue
- # if entity has been added in the current transaction, the
- # user can read it whatever rql expressions are associated
- # to its type
- if cnx.added_in_transaction(eid):
- continue
- for rqlexpr in rqlexprs:
- if rqlexpr.check(cnx, eid):
- break
- else:
- raise Unauthorized('No read acces on %r with eid %i.' % (var, eid))
- # mark variables protected by an rql expression
- restricted_vars.update(localcheck)
- # turn local check into a dict key
- localcheck = tuple(sorted(localcheck.items()))
- localchecks.setdefault(localcheck, []).append(solution)
- # raise Unautorized exception if the user can't access to any solution
- if not newsolutions:
- raise Unauthorized('\n'.join(msgs))
- # if there is some message, solutions have been modified and must be
- # reconsidered by the syntax treee
- if msgs:
- rqlst.set_possible_types(newsolutions)
- return localchecks, restricted_vars
-
- def finalize(self, select, solutions, insertedvars):
- rqlst = Union()
- rqlst.append(select)
- for mainvarname, rschema, newvarname in insertedvars:
- nvartype = str(rschema.objects(solutions[0][mainvarname])[0])
- for sol in solutions:
- sol[newvarname] = nvartype
- select.clean_solutions(solutions)
- add_types_restriction(self.schema, select)
- self.rqlhelper.annotate(rqlst)
- self.preprocess(rqlst, security=False)
- return rqlst
-
-
-class InsertPlan(ExecutionPlan):
- """an execution model specific to the INSERT rql query
- """
-
- def __init__(self, querier, rqlst, args, cnx):
- ExecutionPlan.__init__(self, querier, rqlst, args, cnx)
- # save originally selected variable, we may modify this
- # dictionary for substitution (query parameters)
- self.selected = rqlst.selection
- # list of rows of entities definition (ssplanner.EditedEntity)
- self.e_defs = [[]]
- # list of new relation definition (3-uple (from_eid, r_type, to_eid)
- self.r_defs = set()
- # indexes to track entity definitions bound to relation definitions
- self._r_subj_index = {}
- self._r_obj_index = {}
- self._expanded_r_defs = {}
-
- def add_entity_def(self, edef):
- """add an entity definition to build"""
- self.e_defs[-1].append(edef)
-
- def add_relation_def(self, rdef):
- """add an relation definition to build"""
- self.r_defs.add(rdef)
- if not isinstance(rdef[0], int):
- self._r_subj_index.setdefault(rdef[0], []).append(rdef)
- if not isinstance(rdef[2], int):
- self._r_obj_index.setdefault(rdef[2], []).append(rdef)
-
- def substitute_entity_def(self, edef, edefs):
- """substitute an incomplete entity definition by a list of complete
- equivalents
-
- e.g. on queries such as ::
- INSERT Personne X, Societe Y: X nom N, Y nom 'toto', X travaille Y
- WHERE U login 'admin', U login N
-
- X will be inserted as many times as U exists, and so the X travaille Y
- relations as to be added as many time as X is inserted
- """
- if not edefs or not self.e_defs:
- # no result, no entity will be created
- self.e_defs = ()
- return
- # first remove the incomplete entity definition
- colidx = self.e_defs[0].index(edef)
- for i, row in enumerate(self.e_defs[:]):
- self.e_defs[i][colidx] = edefs[0]
- samplerow = self.e_defs[i]
- for edef_ in edefs[1:]:
- row = [ed.clone() for i, ed in enumerate(samplerow)
- if i != colidx]
- row.insert(colidx, edef_)
- self.e_defs.append(row)
- # now, see if this entity def is referenced as subject in some relation
- # definition
- if edef in self._r_subj_index:
- for rdef in self._r_subj_index[edef]:
- expanded = self._expanded(rdef)
- result = []
- for exp_rdef in expanded:
- for edef_ in edefs:
- result.append( (edef_, exp_rdef[1], exp_rdef[2]) )
- self._expanded_r_defs[rdef] = result
- # and finally, see if this entity def is referenced as object in some
- # relation definition
- if edef in self._r_obj_index:
- for rdef in self._r_obj_index[edef]:
- expanded = self._expanded(rdef)
- result = []
- for exp_rdef in expanded:
- for edef_ in edefs:
- result.append( (exp_rdef[0], exp_rdef[1], edef_) )
- self._expanded_r_defs[rdef] = result
-
- def _expanded(self, rdef):
- """return expanded value for the given relation definition"""
- try:
- return self._expanded_r_defs[rdef]
- except KeyError:
- self.r_defs.remove(rdef)
- return [rdef]
-
- def relation_defs(self):
- """return the list for relation definitions to insert"""
- for rdefs in self._expanded_r_defs.values():
- for rdef in rdefs:
- yield rdef
- for rdef in self.r_defs:
- yield rdef
-
- def insert_entity_defs(self):
- """return eids of inserted entities in a suitable form for the resulting
- result set, e.g.:
-
- e.g. on queries such as ::
- INSERT Personne X, Societe Y: X nom N, Y nom 'toto', X travaille Y
- WHERE U login 'admin', U login N
-
- if there is two entities matching U, the result set will look like
- [(eidX1, eidY1), (eidX2, eidY2)]
- """
- cnx = self.cnx
- repo = cnx.repo
- results = []
- for row in self.e_defs:
- results.append([repo.glob_add_entity(cnx, edef)
- for edef in row])
- return results
-
- def insert_relation_defs(self):
- cnx = self.cnx
- repo = cnx.repo
- edited_entities = {}
- relations = {}
- for subj, rtype, obj in self.relation_defs():
- # if a string is given into args instead of an int, we get it here
- if isinstance(subj, string_types):
- subj = int(subj)
- elif not isinstance(subj, integer_types):
- subj = subj.entity.eid
- if isinstance(obj, string_types):
- obj = int(obj)
- elif not isinstance(obj, integer_types):
- obj = obj.entity.eid
- if repo.schema.rschema(rtype).inlined:
- if subj not in edited_entities:
- entity = cnx.entity_from_eid(subj)
- edited = EditedEntity(entity)
- edited_entities[subj] = edited
- else:
- edited = edited_entities[subj]
- edited.edited_attribute(rtype, obj)
- else:
- if rtype in relations:
- relations[rtype].append((subj, obj))
- else:
- relations[rtype] = [(subj, obj)]
- repo.glob_add_relations(cnx, relations)
- for edited in edited_entities.values():
- repo.glob_update_entity(cnx, edited)
-
-
-class QuerierHelper(object):
- """helper class to execute rql queries, putting all things together"""
-
- def __init__(self, repo, schema):
- # system info helper
- self._repo = repo
- # instance schema
- self.set_schema(schema)
-
- def set_schema(self, schema):
- self.schema = schema
- repo = self._repo
- # rql st and solution cache.
- self._rql_cache = QueryCache(repo.config['rql-cache-size'])
- # rql cache key cache. Don't bother using a Cache instance: we should
- # have a limited number of queries in there, since there are no entries
- # in this cache for user queries (which have no args)
- self._rql_ck_cache = {}
- # some cache usage stats
- self.cache_hit, self.cache_miss = 0, 0
- # rql parsing / analysing helper
- self.solutions = repo.vreg.solutions
- rqlhelper = repo.vreg.rqlhelper
- # set backend on the rql helper, will be used for function checking
- rqlhelper.backend = repo.config.system_source_config['db-driver']
- self._parse = rqlhelper.parse
- self._annotate = rqlhelper.annotate
- # rql planner
- self._planner = SSPlanner(schema, rqlhelper)
- # sql generation annotator
- self.sqlgen_annotate = SQLGenAnnotator(schema).annotate
-
- def parse(self, rql, annotate=False):
- """return a rql syntax tree for the given rql"""
- try:
- return self._parse(text_type(rql), annotate=annotate)
- except UnicodeError:
- raise RQLSyntaxError(rql)
-
- def plan_factory(self, rqlst, args, cnx):
- """create an execution plan for an INSERT RQL query"""
- if rqlst.TYPE == 'insert':
- return InsertPlan(self, rqlst, args, cnx)
- return ExecutionPlan(self, rqlst, args, cnx)
-
- @statsd_timeit
- def execute(self, cnx, rql, args=None, build_descr=True):
- """execute a rql query, return resulting rows and their description in
- a `ResultSet` object
-
- * `rql` should be a Unicode string or a plain ASCII string
- * `args` the optional parameters dictionary associated to the query
- * `build_descr` is a boolean flag indicating if the description should
- be built on select queries (if false, the description will be en empty
- list)
-
- on INSERT queries, there will be one row with the eid of each inserted
- entity
-
- result for DELETE and SET queries is undefined yet
-
- to maximize the rql parsing/analyzing cache performance, you should
- always use substitute arguments in queries (i.e. avoid query such as
- 'Any X WHERE X eid 123'!)
- """
- if server.DEBUG & (server.DBG_RQL | server.DBG_SQL):
- if server.DEBUG & (server.DBG_MORE | server.DBG_SQL):
- print('*'*80)
- print('querier input', repr(rql), repr(args))
- # parse the query and binds variables
- cachekey = (rql,)
- try:
- if args:
- # search for named args in query which are eids (hence
- # influencing query's solutions)
- eidkeys = self._rql_ck_cache[rql]
- if eidkeys:
- # if there are some, we need a better cache key, eg (rql +
- # entity type of each eid)
- try:
- cachekey = self._repo.querier_cache_key(cnx, rql,
- args, eidkeys)
- except UnknownEid:
- # we want queries such as "Any X WHERE X eid 9999"
- # return an empty result instead of raising UnknownEid
- return empty_rset(rql, args)
- rqlst = self._rql_cache[cachekey]
- self.cache_hit += 1
- statsd_c('cache_hit')
- except KeyError:
- self.cache_miss += 1
- statsd_c('cache_miss')
- rqlst = self.parse(rql)
- try:
- # compute solutions for rqlst and return named args in query
- # which are eids. Notice that if you may not need `eidkeys`, we
- # have to compute solutions anyway (kept as annotation on the
- # tree)
- eidkeys = self.solutions(cnx, rqlst, args)
- except UnknownEid:
- # we want queries such as "Any X WHERE X eid 9999" return an
- # empty result instead of raising UnknownEid
- return empty_rset(rql, args)
- if args and rql not in self._rql_ck_cache:
- self._rql_ck_cache[rql] = eidkeys
- if eidkeys:
- cachekey = self._repo.querier_cache_key(cnx, rql, args,
- eidkeys)
- self._rql_cache[cachekey] = rqlst
- if rqlst.TYPE != 'select':
- if cnx.read_security:
- check_no_password_selected(rqlst)
- cachekey = None
- else:
- if cnx.read_security:
- for select in rqlst.children:
- check_no_password_selected(select)
- check_relations_read_access(cnx, select, args)
- # on select query, always copy the cached rqlst so we don't have to
- # bother modifying it. This is not necessary on write queries since
- # a new syntax tree is built from them.
- rqlst = rqlst.copy()
- # Rewrite computed relations
- rewriter = RQLRelationRewriter(cnx)
- rewriter.rewrite(rqlst, args)
- self._annotate(rqlst)
- if args:
- # different SQL generated when some argument is None or not (IS
- # NULL). This should be considered when computing sql cache key
- cachekey += tuple(sorted([k for k, v in args.items()
- if v is None]))
- # make an execution plan
- plan = self.plan_factory(rqlst, args, cnx)
- plan.cache_key = cachekey
- self._planner.build_plan(plan)
- # execute the plan
- try:
- results = plan.execute()
- except (Unauthorized, ValidationError):
- # getting an Unauthorized/ValidationError exception means the
- # transaction must be rolled back
- #
- # notes:
- # * we should not reset the connections set here, since we don't want the
- # connection to loose it during processing
- # * don't rollback if we're in the commit process, will be handled
- # by the connection
- if cnx.commit_state is None:
- cnx.commit_state = 'uncommitable'
- raise
- # build a description for the results if necessary
- descr = ()
- if build_descr:
- if rqlst.TYPE == 'select':
- # sample selection
- 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(cnx, 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 = list(zip(range(len(plan.selected)), repeat(False)))
- descr = _build_descr(cnx, results, basedescr, todetermine)
- # FIXME: get number of affected entities / relations on non
- # selection queries ?
- # return a result set object
- return ResultSet(results, rql, args, descr)
-
- # these are overridden by set_log_methods below
- # only defining here to prevent pylint from complaining
- info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
-
-from logging import getLogger
-from cubicweb import set_log_methods
-LOGGER = getLogger('cubicweb.querier')
-set_log_methods(QuerierHelper, LOGGER)
-
-
-def manual_build_descr(cnx, 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 range(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(cnx, result, basedescr, todetermine)
-
-def _build_descr(cnx, result, basedescription, todetermine):
- description = []
- entity_metas = cnx.entity_metas
- 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] = entity_metas(value)['type']
- except UnknownEid:
- cnx.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