--- a/rqlrewrite.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,933 +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/>.
-"""RQL rewriting utilities : insert rql expression snippets into rql syntax
-tree.
-
-This is used for instance for read security checking in the repository.
-"""
-__docformat__ = "restructuredtext en"
-
-from six import text_type, string_types
-
-from rql import nodes as n, stmts, TypeResolverException
-from rql.utils import common_parent
-
-from yams import BadSchemaDefinition
-
-from logilab.common import tempattr
-from logilab.common.graph import has_path
-
-from cubicweb import Unauthorized
-from cubicweb.schema import RRQLExpression
-
-def cleanup_solutions(rqlst, solutions):
- for sol in solutions:
- for vname in list(sol):
- if not (vname in rqlst.defined_vars or vname in rqlst.aliases):
- del sol[vname]
-
-
-def add_types_restriction(schema, rqlst, newroot=None, solutions=None):
- if newroot is None:
- assert solutions is None
- if hasattr(rqlst, '_types_restr_added'):
- return
- solutions = rqlst.solutions
- newroot = rqlst
- rqlst._types_restr_added = True
- else:
- assert solutions is not None
- rqlst = rqlst.stmt
- eschema = schema.eschema
- allpossibletypes = {}
- for solution in solutions:
- for varname, etype in solution.items():
- # XXX not considering aliases by design, right ?
- if varname not in newroot.defined_vars or eschema(etype).final:
- continue
- allpossibletypes.setdefault(varname, set()).add(etype)
- # XXX could be factorized with add_etypes_restriction from rql 0.31
- for varname in sorted(allpossibletypes):
- var = newroot.defined_vars[varname]
- stinfo = var.stinfo
- if stinfo.get('uidrel') is not None:
- continue # eid specified, no need for additional type specification
- try:
- typerel = rqlst.defined_vars[varname].stinfo.get('typerel')
- except KeyError:
- assert varname in rqlst.aliases
- continue
- if newroot is rqlst and typerel is not None:
- mytyperel = typerel
- else:
- for vref in var.references():
- rel = vref.relation()
- if rel and rel.is_types_restriction():
- mytyperel = rel
- break
- else:
- mytyperel = None
- possibletypes = allpossibletypes[varname]
- if mytyperel is not None:
- if mytyperel.r_type == 'is_instance_of':
- # turn is_instance_of relation into a is relation since we've
- # all possible solutions and don't want to bother with
- # potential is_instance_of incompatibility
- mytyperel.r_type = 'is'
- if len(possibletypes) > 1:
- node = n.Function('IN')
- for etype in sorted(possibletypes):
- node.append(n.Constant(etype, 'etype'))
- else:
- etype = next(iter(possibletypes))
- node = n.Constant(etype, 'etype')
- comp = mytyperel.children[1]
- comp.replace(comp.children[0], node)
- else:
- # variable has already some strict types restriction. new
- # possible types can only be a subset of existing ones, so only
- # remove no more possible types
- for cst in mytyperel.get_nodes(n.Constant):
- if not cst.value in possibletypes:
- cst.parent.remove(cst)
- else:
- # we have to add types restriction
- if stinfo.get('scope') is not None:
- rel = var.scope.add_type_restriction(var, possibletypes)
- else:
- # tree is not annotated yet, no scope set so add the restriction
- # to the root
- rel = newroot.add_type_restriction(var, possibletypes)
- stinfo['typerel'] = rel
- stinfo['possibletypes'] = possibletypes
-
-
-def remove_solutions(origsolutions, solutions, defined):
- """when a rqlst has been generated from another by introducing security
- assertions, this method returns solutions which are contained in orig
- solutions
- """
- newsolutions = []
- for origsol in origsolutions:
- for newsol in solutions[:]:
- for var, etype in origsol.items():
- try:
- if newsol[var] != etype:
- try:
- defined[var].stinfo['possibletypes'].remove(newsol[var])
- except KeyError:
- pass
- break
- except KeyError:
- # variable has been rewritten
- continue
- else:
- newsolutions.append(newsol)
- solutions.remove(newsol)
- return newsolutions
-
-
-def _add_noinvariant(noinvariant, restricted, select, nbtrees):
- # a variable can actually be invariant if it has not been restricted for
- # security reason or if security assertion hasn't modified the possible
- # solutions for the query
- for vname in restricted:
- try:
- var = select.defined_vars[vname]
- except KeyError:
- # this is an alias
- continue
- if nbtrees != 1 or len(var.stinfo['possibletypes']) != 1:
- noinvariant.add(var)
-
-
-def _expand_selection(terms, selected, aliases, select, newselect):
- for term in terms:
- for vref in term.iget_nodes(n.VariableRef):
- if not vref.name in selected:
- select.append_selected(vref)
- colalias = newselect.get_variable(vref.name, len(aliases))
- aliases.append(n.VariableRef(colalias))
- selected.add(vref.name)
-
-def _has_multiple_cardinality(etypes, rdef, ttypes_func, cardindex):
- """return True if relation definitions from entity types (`etypes`) to
- target types returned by the `ttypes_func` function all have single (1 or ?)
- cardinality.
- """
- for etype in etypes:
- for ttype in ttypes_func(etype):
- if rdef(etype, ttype).cardinality[cardindex] in '+*':
- return True
- return False
-
-def _compatible_relation(relations, stmt, sniprel):
- """Search among given rql relation nodes if there is one 'compatible' with the
- snippet relation, and return it if any, else None.
-
- A relation is compatible if it:
- * belongs to the currently processed statement,
- * isn't negged (i.e. direct parent is a NOT node)
- * isn't optional (outer join) or similarly as the snippet relation
- """
- for rel in relations:
- # don't share if relation's scope is not the current statement
- if rel.scope is not stmt:
- continue
- # don't share neged relation
- if rel.neged(strict=True):
- continue
- # don't share optional relation, unless the snippet relation is
- # similarly optional
- if rel.optional and rel.optional != sniprel.optional:
- continue
- return rel
- return None
-
-
-def iter_relations(stinfo):
- # this is a function so that test may return relation in a predictable order
- return stinfo['relations'] - stinfo['rhsrelations']
-
-
-class Unsupported(Exception):
- """raised when an rql expression can't be inserted in some rql query
- because it create an unresolvable query (eg no solutions found)
- """
-
-class VariableFromSubQuery(Exception):
- """flow control exception to indicate that a variable is coming from a
- subquery, and let parent act accordingly
- """
- def __init__(self, variable):
- self.variable = variable
-
-
-class RQLRewriter(object):
- """Insert some rql snippets into another rql syntax tree, for security /
- relation vocabulary. This implies that it should only restrict results of
- the original query, not generate new ones. Hence, inserted snippets are
- inserted under an EXISTS node.
-
- This class *isn't thread safe*.
- """
-
- def __init__(self, session):
- self.session = session
- vreg = session.vreg
- self.schema = vreg.schema
- self.annotate = vreg.rqlhelper.annotate
- self._compute_solutions = vreg.solutions
-
- def compute_solutions(self):
- self.annotate(self.select)
- try:
- self._compute_solutions(self.session, self.select, self.kwargs)
- except TypeResolverException:
- raise Unsupported(str(self.select))
- if len(self.select.solutions) < len(self.solutions):
- raise Unsupported()
-
- def insert_local_checks(self, select, kwargs,
- localchecks, restricted, noinvariant):
- """
- select: the rql syntax tree Select node
- kwargs: query arguments
-
- localchecks: {(('Var name', (rqlexpr1, rqlexpr2)),
- ('Var name1', (rqlexpr1, rqlexpr23))): [solution]}
-
- (see querier._check_permissions docstring for more information)
-
- restricted: set of variable names to which an rql expression has to be
- applied
-
- noinvariant: set of variable names that can't be considered has
- invariant due to security reason (will be filed by this method)
- """
- nbtrees = len(localchecks)
- myunion = union = select.parent
- # transform in subquery when len(localchecks)>1 and groups
- if nbtrees > 1 and (select.orderby or select.groupby or
- select.having or select.has_aggregat or
- select.distinct or
- select.limit or select.offset):
- newselect = stmts.Select()
- # only select variables in subqueries
- origselection = select.selection
- select.select_only_variables()
- select.has_aggregat = False
- # create subquery first so correct node are used on copy
- # (eg ColumnAlias instead of Variable)
- aliases = [n.VariableRef(newselect.get_variable(vref.name, i))
- for i, vref in enumerate(select.selection)]
- selected = set(vref.name for vref in aliases)
- # now copy original selection and groups
- for term in origselection:
- newselect.append_selected(term.copy(newselect))
- if select.orderby:
- sortterms = []
- for sortterm in select.orderby:
- sortterms.append(sortterm.copy(newselect))
- for fnode in sortterm.get_nodes(n.Function):
- if fnode.name == 'FTIRANK':
- # we've to fetch the has_text relation as well
- var = fnode.children[0].variable
- rel = next(iter(var.stinfo['ftirels']))
- assert not rel.ored(), 'unsupported'
- newselect.add_restriction(rel.copy(newselect))
- # remove relation from the orig select and
- # cleanup variable stinfo
- rel.parent.remove(rel)
- var.stinfo['ftirels'].remove(rel)
- var.stinfo['relations'].remove(rel)
- # XXX not properly re-annotated after security insertion?
- newvar = newselect.get_variable(var.name)
- newvar.stinfo.setdefault('ftirels', set()).add(rel)
- newvar.stinfo.setdefault('relations', set()).add(rel)
- newselect.set_orderby(sortterms)
- _expand_selection(select.orderby, selected, aliases, select, newselect)
- select.orderby = () # XXX dereference?
- if select.groupby:
- newselect.set_groupby([g.copy(newselect) for g in select.groupby])
- _expand_selection(select.groupby, selected, aliases, select, newselect)
- select.groupby = () # XXX dereference?
- if select.having:
- newselect.set_having([g.copy(newselect) for g in select.having])
- _expand_selection(select.having, selected, aliases, select, newselect)
- select.having = () # XXX dereference?
- if select.limit:
- newselect.limit = select.limit
- select.limit = None
- if select.offset:
- newselect.offset = select.offset
- select.offset = 0
- myunion = stmts.Union()
- newselect.set_with([n.SubQuery(aliases, myunion)], check=False)
- newselect.distinct = select.distinct
- solutions = [sol.copy() for sol in select.solutions]
- cleanup_solutions(newselect, solutions)
- newselect.set_possible_types(solutions)
- # if some solutions doesn't need rewriting, insert original
- # select as first union subquery
- if () in localchecks:
- myunion.append(select)
- # we're done, replace original select by the new select with
- # subqueries (more added in the loop below)
- union.replace(select, newselect)
- elif not () in localchecks:
- union.remove(select)
- for lcheckdef, lchecksolutions in localchecks.items():
- if not lcheckdef:
- continue
- myrqlst = select.copy(solutions=lchecksolutions)
- myunion.append(myrqlst)
- # in-place rewrite + annotation / simplification
- lcheckdef = [({var: 'X'}, rqlexprs) for var, rqlexprs in lcheckdef]
- self.rewrite(myrqlst, lcheckdef, kwargs)
- _add_noinvariant(noinvariant, restricted, myrqlst, nbtrees)
- if () in localchecks:
- select.set_possible_types(localchecks[()])
- add_types_restriction(self.schema, select)
- _add_noinvariant(noinvariant, restricted, select, nbtrees)
- self.annotate(union)
-
- def rewrite(self, select, snippets, kwargs, existingvars=None):
- """
- snippets: (varmap, list of rql expression)
- with varmap a *dict* {select var: snippet var}
- """
- self.select = select
- # remove_solutions used below require a copy
- self.solutions = solutions = select.solutions[:]
- self.kwargs = kwargs
- self.u_varname = None
- self.removing_ambiguity = False
- self.exists_snippet = {}
- self.pending_keys = []
- self.existingvars = existingvars
- # we have to annotate the rqlst before inserting snippets, even though
- # we'll have to redo it later
- self.annotate(select)
- self.insert_snippets(snippets)
- if not self.exists_snippet and self.u_varname:
- # U has been inserted than cancelled, cleanup
- select.undefine_variable(select.defined_vars[self.u_varname])
- # clean solutions according to initial solutions
- newsolutions = remove_solutions(solutions, select.solutions,
- select.defined_vars)
- assert len(newsolutions) >= len(solutions), (
- 'rewritten rql %s has lost some solutions, there is probably '
- 'something wrong in your schema permission (for instance using a '
- 'RQLExpression which inserts a relation which doesn\'t exist in '
- 'the schema)\nOrig solutions: %s\nnew solutions: %s' % (
- select, solutions, newsolutions))
- if len(newsolutions) > len(solutions):
- newsolutions = self.remove_ambiguities(snippets, newsolutions)
- assert newsolutions
- select.solutions = newsolutions
- add_types_restriction(self.schema, select)
-
- def insert_snippets(self, snippets, varexistsmap=None):
- self.rewritten = {}
- for varmap, rqlexprs in snippets:
- if isinstance(varmap, dict):
- varmap = tuple(sorted(varmap.items()))
- else:
- assert isinstance(varmap, tuple), varmap
- if varexistsmap is not None and not varmap in varexistsmap:
- continue
- self.insert_varmap_snippets(varmap, rqlexprs, varexistsmap)
-
- def init_from_varmap(self, varmap, varexistsmap=None):
- self.varmap = varmap
- self.revvarmap = {}
- self.varinfos = []
- for i, (selectvar, snippetvar) in enumerate(varmap):
- assert snippetvar in 'SOX'
- self.revvarmap[snippetvar] = (selectvar, i)
- vi = {}
- self.varinfos.append(vi)
- try:
- vi['const'] = int(selectvar)
- vi['rhs_rels'] = vi['lhs_rels'] = {}
- except ValueError:
- try:
- vi['stinfo'] = sti = self.select.defined_vars[selectvar].stinfo
- except KeyError:
- vi['stinfo'] = sti = self._subquery_variable(selectvar)
- if varexistsmap is None:
- # build an index for quick access to relations
- vi['rhs_rels'] = {}
- for rel in sti.get('rhsrelations', []):
- vi['rhs_rels'].setdefault(rel.r_type, []).append(rel)
- vi['lhs_rels'] = {}
- for rel in sti.get('relations', []):
- if not rel in sti.get('rhsrelations', []):
- vi['lhs_rels'].setdefault(rel.r_type, []).append(rel)
- else:
- vi['rhs_rels'] = vi['lhs_rels'] = {}
-
- def _subquery_variable(self, selectvar):
- raise VariableFromSubQuery(selectvar)
-
- def insert_varmap_snippets(self, varmap, rqlexprs, varexistsmap):
- try:
- self.init_from_varmap(varmap, varexistsmap)
- except VariableFromSubQuery as ex:
- # variable may have been moved to a newly inserted subquery
- # we should insert snippet in that subquery
- subquery = self.select.aliases[ex.variable].query
- assert len(subquery.children) == 1, subquery
- subselect = subquery.children[0]
- RQLRewriter(self.session).rewrite(subselect, [(varmap, rqlexprs)],
- self.kwargs)
- return
- self._insert_scope = None
- previous = None
- inserted = False
- for rqlexpr in rqlexprs:
- self.current_expr = rqlexpr
- if varexistsmap is None:
- try:
- new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, previous)
- except Unsupported:
- continue
- inserted = True
- if new is not None and self._insert_scope is None:
- self.exists_snippet[rqlexpr] = new
- previous = previous or new
- else:
- # called to reintroduce snippet due to ambiguity creation,
- # so skip snippets which are not introducing this ambiguity
- exists = varexistsmap[varmap]
- if self.exists_snippet.get(rqlexpr) is exists:
- self.insert_snippet(varmap, rqlexpr.snippet_rqlst, exists)
- if varexistsmap is None and not inserted:
- # no rql expression found matching rql solutions. User has no access right
- raise Unauthorized() # XXX may also be because of bad constraints in schema definition
-
- def insert_snippet(self, varmap, snippetrqlst, previous=None):
- new = snippetrqlst.where.accept(self)
- existing = self.existingvars
- self.existingvars = None
- try:
- return self._insert_snippet(varmap, previous, new)
- finally:
- self.existingvars = existing
-
- def _inserted_root(self, new):
- if not isinstance(new, (n.Exists, n.Not)):
- new = n.Exists(new)
- return new
-
- def _insert_snippet(self, varmap, previous, new):
- """insert `new` snippet into the syntax tree, which have been rewritten
- using `varmap`. In cases where an action is protected by several rql
- expresssion, `previous` will be the first rql expression which has been
- inserted, and so should be ORed with the following expressions.
- """
- if new is not None:
- if self._insert_scope is None:
- insert_scope = None
- for vi in self.varinfos:
- scope = vi.get('stinfo', {}).get('scope', self.select)
- if insert_scope is None:
- insert_scope = scope
- else:
- insert_scope = common_parent(scope, insert_scope)
- else:
- insert_scope = self._insert_scope
- if self._insert_scope is None and any(vi.get('stinfo', {}).get('optrelations')
- for vi in self.varinfos):
- assert previous is None
- self._insert_scope, new = self.snippet_subquery(varmap, new)
- self.insert_pending()
- #self._insert_scope = None
- return new
- new = self._inserted_root(new)
- if previous is None:
- insert_scope.add_restriction(new)
- else:
- grandpa = previous.parent
- or_ = n.Or(previous, new)
- grandpa.replace(previous, or_)
- if not self.removing_ambiguity:
- try:
- self.compute_solutions()
- except Unsupported:
- # some solutions have been lost, can't apply this rql expr
- if previous is None:
- self.current_statement().remove_node(new, undefine=True)
- else:
- grandpa.replace(or_, previous)
- self._cleanup_inserted(new)
- raise
- else:
- with tempattr(self, '_insert_scope', new):
- self.insert_pending()
- return new
- self.insert_pending()
-
- def insert_pending(self):
- """pending_keys hold variable referenced by U has_<action>_permission X
- relation.
-
- Once the snippet introducing this has been inserted and solutions
- recomputed, we have to insert snippet defined for <action> of entity
- types taken by X
- """
- stmt = self.current_statement()
- while self.pending_keys:
- key, action = self.pending_keys.pop()
- try:
- varname = self.rewritten[key]
- except KeyError:
- try:
- varname = self.revvarmap[key[-1]][0]
- except KeyError:
- # variable isn't used anywhere else, we can't insert security
- raise Unauthorized()
- ptypes = stmt.defined_vars[varname].stinfo['possibletypes']
- if len(ptypes) > 1:
- # XXX dunno how to handle this
- self.session.error(
- 'cant check security of %s, ambigous type for %s in %s',
- stmt, varname, key[0]) # key[0] == the rql expression
- raise Unauthorized()
- etype = next(iter(ptypes))
- eschema = self.schema.eschema(etype)
- if not eschema.has_perm(self.session, action):
- rqlexprs = eschema.get_rqlexprs(action)
- if not rqlexprs:
- raise Unauthorized()
- self.insert_snippets([({varname: 'X'}, rqlexprs)])
-
- def snippet_subquery(self, varmap, transformedsnippet):
- """introduce the given snippet in a subquery"""
- subselect = stmts.Select()
- snippetrqlst = n.Exists(transformedsnippet.copy(subselect))
- get_rschema = self.schema.rschema
- aliases = []
- done = set()
- for i, (selectvar, _) in enumerate(varmap):
- need_null_test = False
- subselectvar = subselect.get_variable(selectvar)
- subselect.append_selected(n.VariableRef(subselectvar))
- aliases.append(selectvar)
- todo = [(selectvar, self.varinfos[i]['stinfo'])]
- while todo:
- varname, stinfo = todo.pop()
- done.add(varname)
- for rel in iter_relations(stinfo):
- if rel in done:
- continue
- done.add(rel)
- rschema = get_rschema(rel.r_type)
- if rschema.final or rschema.inlined:
- rel.children[0].name = varname # XXX explain why
- subselect.add_restriction(rel.copy(subselect))
- for vref in rel.children[1].iget_nodes(n.VariableRef):
- if isinstance(vref.variable, n.ColumnAlias):
- # XXX could probably be handled by generating the
- # subquery into the detected subquery
- raise BadSchemaDefinition(
- "cant insert security because of usage two inlined "
- "relations in this query. You should probably at "
- "least uninline %s" % rel.r_type)
- subselect.append_selected(vref.copy(subselect))
- aliases.append(vref.name)
- self.select.remove_node(rel)
- # when some inlined relation has to be copied in the
- # subquery and that relation is optional, we need to
- # test that either value is NULL or that the snippet
- # condition is satisfied
- if varname == selectvar and rel.optional and rschema.inlined:
- need_null_test = True
- # also, if some attributes or inlined relation of the
- # object variable are accessed, we need to get all those
- # from the subquery as well
- if vref.name not in done and rschema.inlined:
- # we can use vref here define in above for loop
- ostinfo = vref.variable.stinfo
- for orel in iter_relations(ostinfo):
- orschema = get_rschema(orel.r_type)
- if orschema.final or orschema.inlined:
- todo.append( (vref.name, ostinfo) )
- break
- if need_null_test:
- snippetrqlst = n.Or(
- n.make_relation(subselect.get_variable(selectvar), 'is',
- (None, None), n.Constant,
- operator='='),
- snippetrqlst)
- subselect.add_restriction(snippetrqlst)
- if self.u_varname:
- # generate an identifier for the substitution
- argname = subselect.allocate_varname()
- while argname in self.kwargs:
- argname = subselect.allocate_varname()
- subselect.add_constant_restriction(subselect.get_variable(self.u_varname),
- 'eid', text_type(argname), 'Substitute')
- self.kwargs[argname] = self.session.user.eid
- add_types_restriction(self.schema, subselect, subselect,
- solutions=self.solutions)
- myunion = stmts.Union()
- myunion.append(subselect)
- aliases = [n.VariableRef(self.select.get_variable(name, i))
- for i, name in enumerate(aliases)]
- self.select.add_subquery(n.SubQuery(aliases, myunion), check=False)
- self._cleanup_inserted(transformedsnippet)
- try:
- self.compute_solutions()
- except Unsupported:
- # some solutions have been lost, can't apply this rql expr
- self.select.remove_subquery(self.select.with_[-1])
- raise
- return subselect, snippetrqlst
-
- def remove_ambiguities(self, snippets, newsolutions):
- # the snippet has introduced some ambiguities, we have to resolve them
- # "manually"
- variantes = self.build_variantes(newsolutions)
- # insert "is" where necessary
- varexistsmap = {}
- self.removing_ambiguity = True
- for (erqlexpr, varmap, oldvarname), etype in variantes[0].items():
- varname = self.rewritten[(erqlexpr, varmap, oldvarname)]
- var = self.select.defined_vars[varname]
- exists = var.references()[0].scope
- exists.add_constant_restriction(var, 'is', etype, 'etype')
- varexistsmap[varmap] = exists
- # insert ORED exists where necessary
- for variante in variantes[1:]:
- self.insert_snippets(snippets, varexistsmap)
- for key, etype in variante.items():
- varname = self.rewritten[key]
- try:
- var = self.select.defined_vars[varname]
- except KeyError:
- # not a newly inserted variable
- continue
- exists = var.references()[0].scope
- exists.add_constant_restriction(var, 'is', etype, 'etype')
- # recompute solutions
- self.compute_solutions()
- # clean solutions according to initial solutions
- return remove_solutions(self.solutions, self.select.solutions,
- self.select.defined_vars)
-
- def build_variantes(self, newsolutions):
- variantes = set()
- for sol in newsolutions:
- variante = []
- for key, newvar in self.rewritten.items():
- variante.append( (key, sol[newvar]) )
- variantes.add(tuple(variante))
- # rebuild variantes as dict
- variantes = [dict(variante) for variante in variantes]
- # remove variable which have always the same type
- for key in self.rewritten:
- it = iter(variantes)
- etype = next(it)[key]
- for variante in it:
- if variante[key] != etype:
- break
- else:
- for variante in variantes:
- del variante[key]
- return variantes
-
- def _cleanup_inserted(self, node):
- # cleanup inserted variable references
- removed = set()
- for vref in node.iget_nodes(n.VariableRef):
- vref.unregister_reference()
- if not vref.variable.stinfo['references']:
- # no more references, undefine the variable
- del self.select.defined_vars[vref.name]
- removed.add(vref.name)
- for key, newvar in list(self.rewritten.items()):
- if newvar in removed:
- del self.rewritten[key]
-
-
- def _may_be_shared_with(self, sniprel, target):
- """if the snippet relation can be skipped to use a relation from the
- original query, return that relation node
- """
- if sniprel.neged(strict=True):
- return None # no way
- rschema = self.schema.rschema(sniprel.r_type)
- stmt = self.current_statement()
- for vi in self.varinfos:
- try:
- if target == 'object':
- orels = vi['lhs_rels'][sniprel.r_type]
- cardindex = 0
- ttypes_func = rschema.objects
- rdef = rschema.rdef
- else: # target == 'subject':
- orels = vi['rhs_rels'][sniprel.r_type]
- cardindex = 1
- ttypes_func = rschema.subjects
- rdef = lambda x, y: rschema.rdef(y, x)
- except KeyError:
- # may be raised by vi['xhs_rels'][sniprel.r_type]
- continue
- # if cardinality isn't in '?1', we can't ignore the snippet relation
- # and use variable from the original query
- if _has_multiple_cardinality(vi['stinfo']['possibletypes'], rdef,
- ttypes_func, cardindex):
- continue
- orel = _compatible_relation(orels, stmt, sniprel)
- if orel is not None:
- return orel
- return None
-
- def _use_orig_term(self, snippet_varname, term):
- key = (self.current_expr, self.varmap, snippet_varname)
- if key in self.rewritten:
- stmt = self.current_statement()
- insertedvar = stmt.defined_vars.pop(self.rewritten[key])
- for inserted_vref in insertedvar.references():
- inserted_vref.parent.replace(inserted_vref, term.copy(stmt))
- self.rewritten[key] = term.name
-
- def _get_varname_or_term(self, vname):
- stmt = self.current_statement()
- if vname == 'U':
- stmt = self.select
- if self.u_varname is None:
- self.u_varname = stmt.allocate_varname()
- # generate an identifier for the substitution
- argname = stmt.allocate_varname()
- while argname in self.kwargs:
- argname = stmt.allocate_varname()
- # insert "U eid %(u)s"
- stmt.add_constant_restriction(
- stmt.get_variable(self.u_varname),
- 'eid', text_type(argname), 'Substitute')
- self.kwargs[argname] = self.session.user.eid
- return self.u_varname
- key = (self.current_expr, self.varmap, vname)
- try:
- return self.rewritten[key]
- except KeyError:
- self.rewritten[key] = newvname = stmt.allocate_varname()
- return newvname
-
- # visitor methods ##########################################################
-
- def _visit_binary(self, node, cls):
- newnode = cls()
- for c in node.children:
- new = c.accept(self)
- if new is None:
- continue
- newnode.append(new)
- if len(newnode.children) == 0:
- return None
- if len(newnode.children) == 1:
- return newnode.children[0]
- return newnode
-
- def _visit_unary(self, node, cls):
- newc = node.children[0].accept(self)
- if newc is None:
- return None
- newnode = cls()
- newnode.append(newc)
- return newnode
-
- def visit_and(self, node):
- return self._visit_binary(node, n.And)
-
- def visit_or(self, node):
- return self._visit_binary(node, n.Or)
-
- def visit_not(self, node):
- return self._visit_unary(node, n.Not)
-
- def visit_exists(self, node):
- return self._visit_unary(node, n.Exists)
-
- def keep_var(self, varname):
- if varname in 'SO':
- return varname in self.existingvars
- if varname == 'U':
- return True
- vargraph = self.current_expr.vargraph
- for existingvar in self.existingvars:
- #path = has_path(vargraph, varname, existingvar)
- if not varname in vargraph or has_path(vargraph, varname, existingvar):
- return True
- # no path from this variable to an existing variable
- return False
-
- def visit_relation(self, node):
- lhs, rhs = node.get_variable_parts()
- # remove relations where an unexistant variable and or a variable linked
- # to an unexistant variable is used.
- if self.existingvars:
- if not self.keep_var(lhs.name):
- return
- if node.r_type in ('has_add_permission', 'has_update_permission',
- 'has_delete_permission', 'has_read_permission'):
- assert lhs.name == 'U'
- action = node.r_type.split('_')[1]
- key = (self.current_expr, self.varmap, rhs.name)
- self.pending_keys.append( (key, action) )
- return
- if isinstance(rhs, n.VariableRef):
- if self.existingvars and not self.keep_var(rhs.name):
- return
- if lhs.name in self.revvarmap and rhs.name != 'U':
- orel = self._may_be_shared_with(node, 'object')
- if orel is not None:
- self._use_orig_term(rhs.name, orel.children[1].children[0])
- return
- elif rhs.name in self.revvarmap and lhs.name != 'U':
- orel = self._may_be_shared_with(node, 'subject')
- if orel is not None:
- self._use_orig_term(lhs.name, orel.children[0])
- return
- rel = n.Relation(node.r_type, node.optional)
- for c in node.children:
- rel.append(c.accept(self))
- return rel
-
- def visit_comparison(self, node):
- cmp_ = n.Comparison(node.operator)
- for c in node.children:
- cmp_.append(c.accept(self))
- return cmp_
-
- def visit_mathexpression(self, node):
- cmp_ = n.MathExpression(node.operator)
- for c in node.children:
- cmp_.append(c.accept(self))
- return cmp_
-
- def visit_function(self, node):
- """generate filter name for a function"""
- function_ = n.Function(node.name)
- for c in node.children:
- function_.append(c.accept(self))
- return function_
-
- def visit_constant(self, node):
- """generate filter name for a constant"""
- return n.Constant(node.value, node.type)
-
- def visit_variableref(self, node):
- """get the sql name for a variable reference"""
- stmt = self.current_statement()
- if node.name in self.revvarmap:
- selectvar, index = self.revvarmap[node.name]
- vi = self.varinfos[index]
- if vi.get('const') is not None:
- return n.Constant(vi['const'], 'Int')
- return n.VariableRef(stmt.get_variable(selectvar))
- vname_or_term = self._get_varname_or_term(node.name)
- if isinstance(vname_or_term, string_types):
- return n.VariableRef(stmt.get_variable(vname_or_term))
- # shared term
- return vname_or_term.copy(stmt)
-
- def current_statement(self):
- if self._insert_scope is None:
- return self.select
- return self._insert_scope.stmt
-
-
-class RQLRelationRewriter(RQLRewriter):
- """Insert some rql snippets into another rql syntax tree, replacing computed
- relations by their associated rule.
-
- This class *isn't thread safe*.
- """
- def __init__(self, session):
- super(RQLRelationRewriter, self).__init__(session)
- self.rules = {}
- for rschema in self.schema.iter_computed_relations():
- self.rules[rschema.type] = RRQLExpression(rschema.rule)
-
- def rewrite(self, union, kwargs=None):
- self.kwargs = kwargs
- self.removing_ambiguity = False
- self.existingvars = None
- self.pending_keys = None
- for relation in union.iget_nodes(n.Relation):
- if relation.r_type in self.rules:
- self.select = relation.stmt
- self.solutions = solutions = self.select.solutions[:]
- self.current_expr = self.rules[relation.r_type]
- self._insert_scope = relation.scope
- self.rewritten = {}
- lhs, rhs = relation.get_variable_parts()
- varmap = {lhs.name: 'S', rhs.name: 'O'}
- self.init_from_varmap(tuple(sorted(varmap.items())))
- self.insert_snippet(varmap, self.current_expr.snippet_rqlst)
- self.select.remove_node(relation)
-
- def _subquery_variable(self, selectvar):
- return self.select.aliases[selectvar].stinfo
-
- def _inserted_root(self, new):
- return new