[pkg] Use setuptools
Move sources to a 'rql' subdirectory, drop the NO_SETUPTOOLS code path
(essentially rewriting all of setup.py). This avoids a conflict between
our parser.py and the stdlib when running setup.py. Closes #278637.
--- a/__init__.py Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,257 +0,0 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of rql.
-#
-# rql 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.
-#
-# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
-"""RQL library (implementation independant)."""
-__docformat__ = "restructuredtext en"
-
-from rql.__pkginfo__ import version as __version__
-from math import log
-
-import sys
-import threading
-
-from six import StringIO
-
-from rql._exceptions import *
-
-#REQUIRED_TYPES = ['String', 'Float', 'Int', 'Boolean', 'Date']
-
-class RQLHelper(object):
- """Helper class for RQL handling
-
- give access to methods for :
- - parsing RQL strings
- - variables type resolving
- - comparison of two queries
- """
- def __init__(self, schema, uid_func_mapping=None, special_relations=None,
- resolver_class=None, backend=None):
- # chech schema
- #for e_type in REQUIRED_TYPES:
- # if not schema.has_entity(e_type):
- # raise MissingType(e_type)
- # create helpers
- from rql.stcheck import RQLSTChecker, RQLSTAnnotator
- special_relations = special_relations or {}
- if uid_func_mapping:
- for key in uid_func_mapping:
- special_relations[key] = 'uid'
- self._checker = RQLSTChecker(schema, special_relations, backend)
- self._annotator = RQLSTAnnotator(schema, special_relations)
- self._analyser_lock = threading.Lock()
- if resolver_class is None:
- from rql.analyze import ETypeResolver
- resolver_class = ETypeResolver
- self._analyser = resolver_class(schema, uid_func_mapping)
- # IgnoreTypeRestriction analyser
- from rql.analyze import ETypeResolverIgnoreTypeRestriction
- self._itr_analyser_lock = threading.Lock()
- self._itr_analyser = ETypeResolverIgnoreTypeRestriction(schema, uid_func_mapping)
- self.set_schema(schema)
-
- def set_schema(self, schema):
- from rql.utils import is_keyword
- for etype in schema.entities():
- etype = str(etype)
- if is_keyword(etype) or etype.capitalize() == 'Any':
- raise UsesReservedWord(etype)
- for rtype in schema.relations():
- rtype = str(rtype)
- if is_keyword(rtype):
- raise UsesReservedWord(rtype)
- self._checker.schema = schema
- self._annotator.schema = schema
- self._analyser.set_schema(schema)
-
- def get_backend(self):
- return self._checker.backend
- def set_backend(self, backend):
- self._checker.backend = backend
- backend = property(get_backend, set_backend)
-
- def parse(self, rqlstring, annotate=True):
- """Return a syntax tree created from a RQL string."""
- rqlst = parse(rqlstring, False)
- self._checker.check(rqlst)
- if annotate:
- self.annotate(rqlst)
- rqlst.schema = self._annotator.schema
- return rqlst
-
- def annotate(self, rqlst):
- self._annotator.annotate(rqlst)
-
- def compute_solutions(self, rqlst, uid_func_mapping=None, kwargs=None,
- debug=False):
- """Set solutions for variables of the syntax tree.
-
- Each solution is a dictionary with variable's name as key and
- variable's type as value.
- """
- self._analyser_lock.acquire()
- try:
- return self._analyser.visit(rqlst, uid_func_mapping, kwargs,
- debug)
- finally:
- self._analyser_lock.release()
-
- def compute_all_solutions(self, rqlst, uid_func_mapping=None, kwargs=None,
- debug=False):
- """compute syntaxe tree solutions with all types restriction (eg
- is/instance_of relations) ignored
- """
- self._itr_analyser_lock.acquire()
- try:
- self._itr_analyser.visit(rqlst, uid_func_mapping, kwargs,
- debug)
- finally:
- self._itr_analyser_lock.release()
-
-
- def simplify(self, rqlst):
- """Simplify `rqlst` by rewriting non-final variables associated to a const
- node (if annotator say we can...)
-
- The tree is modified in-place.
- """
- #print 'simplify', rqlst.as_string(encoding='UTF8')
- if rqlst.TYPE == 'select':
- from rql import nodes
- for select in rqlst.children:
- self._simplify(select)
-
- def _simplify(self, select):
- # recurse on subqueries first
- for subquery in select.with_:
- for subselect in subquery.query.children:
- self._simplify(subselect)
- rewritten = False
- for var in list(select.defined_vars.values()):
- stinfo = var.stinfo
- if stinfo['constnode'] and not stinfo.get('blocsimplification'):
- uidrel = stinfo['uidrel']
- var = uidrel.children[0].variable
- vconsts = []
- rhs = uidrel.children[1].children[0]
- for vref in var.references():
- rel = vref.relation()
- if rel is None:
- term = vref
- while not term.parent is select:
- term = term.parent
- if term in select.selection:
- rhs = copy_uid_node(select, rhs, vconsts)
- if vref is term:
- select.selection[select.selection.index(vref)] = rhs
- rhs.parent = select
- else:
- vref.parent.replace(vref, rhs)
- elif term in select.orderby:
- # remove from orderby
- select.remove(term)
- elif not select.having:
- # remove from groupby if no HAVING clause
- select.remove(term)
- else:
- rhs = copy_uid_node(select, rhs, vconsts)
- select.groupby[select.groupby.index(vref)] = rhs
- rhs.parent = select
- elif rel is uidrel:
- uidrel.parent.remove(uidrel)
- elif rel.is_types_restriction():
- stinfo['typerel'] = None
- rel.parent.remove(rel)
- else:
- rhs = copy_uid_node(select, rhs, vconsts)
- vref.parent.replace(vref, rhs)
- del select.defined_vars[var.name]
- stinfo['uidrel'] = None
- rewritten = True
- if vconsts:
- select.stinfo['rewritten'][var.name] = vconsts
- if rewritten and select.solutions:
- select.clean_solutions()
-
- def compare(self, rqlstring1, rqlstring2):
- """Compare 2 RQL requests.
-
- Return True if both requests would return the same results.
- """
- from rql.compare import compare_tree
- return compare_tree(self.parse(rqlstring1), self.parse(rqlstring2))
-
-
-def copy_uid_node(select, node, vconsts):
- node = node.copy(select)
- node.uid = True
- vconsts.append(node)
- return node
-
-
-def parse(rqlstring, print_errors=True):
- """Return a syntax tree created from a RQL string."""
- from yapps.runtime import print_error, SyntaxError, NoMoreTokens
- from rql.parser import Hercule, HerculeScanner
- # make sure rql string ends with a semi-colon
- rqlstring = rqlstring.strip()
- if rqlstring and not rqlstring.endswith(';') :
- rqlstring += ';'
- # parse the RQL string
- parser = Hercule(HerculeScanner(rqlstring))
- try:
- return parser.goal()
- except SyntaxError as ex:
- if not print_errors:
- if ex.pos is not None:
- multi_lines_rql = rqlstring.splitlines()
- nb_lines = len(multi_lines_rql)
- if nb_lines > 5:
- width = log(nb_lines, 10)+1
- template = " %%%ii: %%s" % width
- rqlstring = '\n'.join( template % (idx + 1, line) for idx, line in enumerate(multi_lines_rql))
-
-
- msg = '%s\nat: %r\n%s' % (rqlstring, ex.pos, ex.msg)
- else:
- msg = '%s\n%s' % (rqlstring, ex.msg)
- exc = RQLSyntaxError(msg)
- exc.__traceback__ = sys.exc_info()[-1]
- raise exc
- # try to get error message from yapps
- try:
- out = sys.stdout
- sys.stdout = stream = StringIO()
- try:
- print_error(ex, parser._scanner)
- finally:
- sys.stdout = out
- exc = RQLSyntaxError(stream.getvalue())
- exc.__traceback__ = sys.exc_info()[-1]
- raise exc
- except ImportError: # duh?
- sys.stdout = out
- exc = RQLSyntaxError('Syntax Error', ex.msg, 'on line',
- 1 + pinput.count('\n', 0, ex.pos))
- exc.__traceback__ = sys.exc_info()[-1]
- raise exc
- except NoMoreTokens:
- msg = 'Could not complete parsing; stopped around here: \n%s'
- exc = RQLSyntaxError(msg % parser._scanner)
- exc.__traceback__ = sys.exc_info()[-1]
- raise exc
-
-pyparse = parse
--- a/__pkginfo__.py Mon Jul 28 11:12:58 2014 +0200
+++ b/__pkginfo__.py Thu May 28 01:13:28 2015 +0200
@@ -60,15 +60,15 @@
GECODE_VERSION = encode_version(*gecode_version())
if sys.platform != 'win32':
- ext_modules = [Extension('rql_solve',
- ['gecode_solver.cpp'],
+ ext_modules = [Extension('rql.rql_solve',
+ ['rql/gecode_solver.cpp'],
libraries=['gecodeint', 'gecodekernel', 'gecodesearch',],
extra_compile_args=['-DGE_VERSION=%s' % GECODE_VERSION],
)
]
else:
- ext_modules = [ Extension('rql_solve',
- ['gecode_solver.cpp'],
+ ext_modules = [ Extension('rql.rql_solve',
+ ['rql/gecode_solver.cpp'],
libraries=['GecodeInt-3-3-1-r-x86',
'GecodeKernel-3-3-1-r-x86',
'GecodeSearch-3-3-1-r-x86',
@@ -85,4 +85,5 @@
'yapps >= 2.2.0', # XXX to ensure we don't use the broken pypi version
'logilab-constraint >= 0.5.0', # fallback if the gecode compiled module is missing
'six >= 1.4.0',
+ 'setuptools',
]
--- a/_exceptions.py Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of rql.
-#
-# rql 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.
-#
-# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
-"""Exceptions used in the RQL package."""
-
-__docformat__ = "restructuredtext en"
-
-class RQLException(Exception):
- """Base exception for exceptions of the rql module."""
-
-class MissingType(RQLException):
- """Raised when there is some expected type missing from a schema."""
-
-class UsesReservedWord(RQLException):
- """Raised when the schema uses a reserved word as type or relation."""
-
-class RQLSyntaxError(RQLException):
- """Raised when there is a syntax error in the rql string."""
-
-class TypeResolverException(RQLException):
- """Raised when we are unable to guess variables' type."""
-
-class BadRQLQuery(RQLException):
- """Raised when there is a no sense in the rql query."""
-
-class CoercionError(RQLException):
- """Failed to infer type of a math expression."""
--- a/analyze.py Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,588 +0,0 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of rql.
-#
-# rql 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.
-#
-# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
-"""Analyze of the RQL syntax tree to get possible types for RQL variables.
-
-"""
-from __future__ import print_function
-
-__docformat__ = "restructuredtext en"
-
-import os
-
-from six import StringIO, string_types
-from six.moves import zip
-
-from rql import TypeResolverException, nodes
-
-
-try:
- pure = bool(os.environ.get('RQL_USE_PURE_PYTHON_ANALYSE', 0))
- if pure:
- raise ImportError
- from rql import rql_solve
-except ImportError:
- rql_solve = None
- import warnings
- warnings.filterwarnings(action='ignore', module='logilab.constraint.propagation')
- from logilab.constraint import Repository, Solver, fd
-
- # Gecode solver not available
-#rql_solve = None # uncomment to force using logilab-constraint
-
-class ConstraintCSPProblem(object):
- def __init__(self):
- self.constraints = []
- self.domains = {}
- self.scons = []
- self.output = StringIO()
-
- def debug(self):
- print("Domains:", self.domains)
- print("Constraints:", self.constraints)
- print("Scons:", self.scons)
-
- def get_output(self):
- return self.output.getvalue()
-
- def printer(self, *msgs):
- self.output.write(' '.join(str(msg) for msg in msgs))
- self.output.write('\n')
-
- def solve(self):
- repo = Repository(self.domains.keys(), self.domains, self.get_constraints())
- solver = Solver(printer=self.printer)
- # used for timing
- #import time
- #t0=time.time()
- sols = solver.solve(repo, verbose=(True or self.debug))
- #print "RUNTIME:", time.time()-t0
- return sols
-
- def add_var(self, name, values):
- self.domains[name] = fd.FiniteDomain(values)
-
- def end_domain_definition(self):
- pass
-
- def get_domains(self):
- return self.domains
-
- def get_constraints(self):
- return self.constraints
-
- def add_expr( self, vars, expr ):
- self.constraints.append( fd.make_expression( vars, expr ) )
- self.scons.append(expr)
-
- def var_has_type(self, var, etype):
- assert isinstance(etype, string_types)
- self.add_expr( (var,), '%s == %r' % (var, etype) )
-
- def var_has_types(self, var, etypes):
- etypes = tuple(etypes)
- for t in etypes:
- assert isinstance(t, string_types)
- if len(etypes) == 1:
- cstr = '%s == "%s"' % (var, etypes[0])
- else:
- cstr = '%s in %s ' % (var, etypes)
- self.add_expr( (var,), cstr)
-
- def vars_have_same_types(self, varnames, types):
- self.add_expr( varnames, '%s in %s' % ( '=='.join(varnames), types))
-
- def or_and(self, equalities):
- orred = set()
- variables = set()
- for orred_expr in equalities:
- anded = set()
- for vars, types in orred_expr:
- types=tuple(types)
- for t in types:
- assert isinstance(t, string_types)
- if len(types)==1:
- anded.add( '%s == "%s"' % ( '=='.join(vars), types[0]) )
- else:
- anded.add( '%s in %s' % ( '=='.join(vars), types) )
- for var in vars:
- variables.add(var)
- orred.add( '(' + ' and '.join( list(anded) ) + ')' )
- expr = " or ".join( list(orred) )
- self.add_expr( tuple(variables), expr )
-
-# GECODE based constraint solver
-_AND = 0 # symbolic values
-_OR = 1
-_EQ = 2
-_EQV = 3
-
-OPSYM={
- _AND:"and",
- _OR:"or",
- _EQ:"eq",
- _EQV:"eqv"
-}
-
-class GecodeCSPProblem(object):
- """Builds an internal representation of the constraint
- that will be passed to the rql_solve module which implements
- a gecode-based solver
-
- The internal representation is a tree builds with lists of lists
- the first item of the list is the node type (_AND,_OR,_EQ,_EQV)
-
- an example : ["and", [ "eq",0,0 ], ["or", ["eq", 1, 1], ["eq", 1, 2] ] ]
-
- means Var(0) == Value(0) and ( Var(1)==Val(1) or Var(1) == Val(2)
-
- TODO: at the moment the solver makes no type checking on the structure
- of the tree thus can crash badly if something wrong is handled to it
- this should not happend as the building of the tree is done internally
- but it should be fixed anyways.
- When fixing that we should also replace string nodes by integers
- """
- def __init__(self):
- self.constraints = []
- self.op = [ _AND ]
- self.domains = {} # maps var name -> var value
- self.variables = {} # maps var name -> var index
- self.ivariables = [] # maps var index-> var name
- self.values = {} # maps val name -> val index
- self.all_values = set() # this gets turned into a list later
- self.idx_domains = [] # maps var index -> list of val index
- self.ivalues = {} # only used for debugging
-
- def debug(self):
- self.ivalues = {}
- for val_name, val_num in self.values.items():
- self.ivalues[val_num] = val_name
- print("Domains:", self.domains)
- print("Ops:", self.pretty_print_ops(self.op))
- print("Variables:", self.variables)
- print("Values:", self.values)
-
-
- def pretty_print_ops(self, ops):
- if ops[0] in (_AND, _OR):
- res = [ OPSYM[ops[0]], '(' ]
- for op in ops[1:]:
- res.append(self.pretty_print_ops(op))
- res.append(',')
- res.append( ')' )
- return "".join(res)
- elif ops[0] == _EQ:
- return "%s==%s" % (self.ivariables[ops[1]], self.ivalues[ops[2]])
- elif ops[0] == _EQV:
- res = [ self.ivariables[k] for k in ops[1:] ]
- return '~='.join(res)
-
- def get_output(self):
- return ""
-
- def solve(self):
- constraints = self.op
-
- # used for timing
- #import time
- #t0=time.time()
-
- sols = rql_solve.solve( self.idx_domains, len(self.all_values), constraints )
- rql_sols = []
- for s in sols:
- r={}
- for var, val in zip(self.ivariables, s):
- r[var] = self.all_values[val]
- rql_sols.append(r)
- #print "RUNTIME:", time.time()-t0
- return rql_sols
-
- def add_var(self, name, values):
- assert name not in self.variables
- self.all_values.update( values )
- self.variables[name] = len(self.variables)
- self.ivariables.append(name)
- self.domains[name] = values
-
- def end_domain_definition(self):
- # maps integer->value
- self.all_values = list(self.all_values)
- # maps value->integer
- self.values = dict( [ (v,i) for i,v in enumerate(self.all_values)] )
- #print self.values
- #print self.domains
- for var_name in self.ivariables:
- val_domain = self.domains[var_name]
- idx_domain = [ self.values[val] for val in val_domain ]
- self.idx_domains.append( idx_domain )
-
- def and_eq( self, var, value ):
- self.op.append( [_EQ, self.variables[var], self.values[value] ] )
-
- def equal_vars(self, varnames):
- if len(varnames)>1:
- self.op.append( [ _EQV] + [ self.variables[v] for v in varnames ] )
-
- def var_has_type(self, var, etype):
- self.and_eq( var, etype)
-
- def var_has_types(self, var, etypes):
- for t in etypes:
- assert isinstance(t, string_types)
- if len(etypes) == 1:
- self.and_eq( var, tuple(etypes)[0] )
- else:
- orred = [ _OR ]
- for t in etypes:
- try:
- orred.append( [ _EQ, self.variables[var], self.values[t] ] )
- except KeyError:
- # key error may be raised by self.values[t] if self.values
- # reflects constraints from subqueries
- continue
- self.op.append( orred )
-
- def vars_have_same_types(self, varnames, types):
- self.equal_vars( varnames )
- for var in varnames:
- self.var_has_types( var, types )
-
- def or_and(self, equalities):
- orred = [ _OR ]
- for orred_expr in equalities:
- anded = [ _AND ]
- for vars, types in orred_expr:
- self.equal_vars( vars )
- for t in types:
- assert isinstance(t, string_types)
- for var in vars:
- if len(types)==1:
- anded.append( [ _EQ, self.variables[var], self.values[types[0]] ] )
- else:
- or2 = [ _OR ]
- for t in types:
- or2.append( [_EQ, self.variables[var], self.values[t] ] )
- anded.append( or2 )
- orred.append(anded)
- self.op.append(orred)
-
-if rql_solve is None:
- CSPProblem = ConstraintCSPProblem
-else:
- CSPProblem = GecodeCSPProblem
-
-#CSPProblem = ConstraintCSPProblem
-
-
-class ETypeResolver(object):
- """Resolve variables types according to the schema.
-
- CSP modelisation:
- * variable <-> RQL variable
- * domains <-> different entity's types defined in the schema
- * constraints <-> relations between (RQL) variables
- """
- var_solkey = 'possibletypes'
-
- def __init__(self, schema, uid_func_mapping=None):
- """
- :Parameters:
- * `schema`: an object describing entities and relations that implements
- the ISchema interface.
- * `uid_func_mapping`: a dictionary where keys are strings representing an
- attribute used as a Unique IDentifier and values are methods that
- accept attribute values and return entity's types.
- [mapping from relation to function taking rhs value as argument
- and returning an entity type].
- """
- self.debug = 0
- self.set_schema(schema)
- if uid_func_mapping is None:
- self.uid_func_mapping = {}
- self.uid_func = None
- else:
- self.uid_func_mapping = uid_func_mapping
- self.uid_func = next(iter(uid_func_mapping.values()))
-
- def set_schema(self, schema):
- self.schema = schema
- # default domains for a variable
- self._base_domain = set(str(etype) for etype in schema.entities())
- self._nonfinal_domain = set(str(etype) for etype in schema.entities()
- if not etype.final)
-
- def solve(self, node, constraints):
- # debug info
- if self.debug > 1:
- print("- AN1 -"+'-'*80)
- print(node)
- print("CONSTRAINTS:")
- constraints.debug()
-
- sols = constraints.solve()
-
- if not sols:
- rql = node.as_string('utf8', self.kwargs)
- ex_msg = 'Unable to resolve variables types in "%s"' % (rql,)
- if True or self.debug:
- ex_msg += '\n%s' % (constraints.get_output(),)
- raise TypeResolverException(ex_msg)
- node.set_possible_types(sols, self.kwargs, self.var_solkey)
-
- def _visit(self, node, constraints=None):
- """Recurse down the tree.
-
- * node: rql node to process
- * constraints: a XxxCSPProblem object.
- """
- func = getattr(self, 'visit_%s' % node.__class__.__name__.lower())
- if constraints is None:
- func(node)
- elif func(node, constraints) is None:
- for c in node.children:
- self._visit(c, constraints)
-
- def _uid_node_types(self, valnode):
- types = set()
- for cst in valnode.iget_nodes(nodes.Constant):
- assert cst.type
- if cst.type == 'Substitute':
- eid = self.kwargs[cst.value]
- self.deambiguifiers.add(cst.value)
- else:
- eid = cst.value
- cst.uidtype = self.uid_func(cst.eval(self.kwargs))
- types.add(cst.uidtype)
- return types
-
- def _init_stmt(self, node):
- pb = CSPProblem()
- # set domain for all the variables
- for var in node.defined_vars.values():
- pb.add_var( var.name, self._base_domain )
- # no variable short cut
- return pb
-
- def _extract_constraint(self, constraints, var, term, get_target_types):
- if self.uid_func:
- alltypes = set()
- for etype in self._uid_node_types(term):
- for targettypes in get_target_types(etype):
- alltypes.add(targettypes)
- else:
- alltypes = get_target_types()
- domain = constraints.domains[var]
- constraints.var_has_types( var, [str(t) for t in alltypes if t in domain] )
-
- def visit(self, node, uid_func_mapping=None, kwargs=None, debug=False):
- # FIXME: not thread safe
- self.debug = debug
- if uid_func_mapping is not None:
- assert len(uid_func_mapping) <= 1
- self.uid_func_mapping = uid_func_mapping
- self.uid_func = next(iter(uid_func_mapping.values()))
- self.kwargs = kwargs
- self.deambiguifiers = set()
- self._visit(node)
- if uid_func_mapping is not None:
- self.uid_func_mapping = None
- self.uid_func = None
- return self.deambiguifiers
-
- def visit_union(self, node):
- for select in node.children:
- self._visit(select)
-
- def visit_insert(self, node):
- if not node.defined_vars:
- node.set_possible_types([{}])
- return
- constraints = self._init_stmt(node)
- constraints.end_domain_definition()
- for etype, variable in node.main_variables:
- if node.TYPE == 'delete' and etype == 'Any':
- continue
- assert etype in self.schema, etype
- var = variable.name
- constraints.var_has_type( var, etype )
- for relation in node.main_relations:
- self._visit(relation, constraints)
- # get constraints from the restriction subtree
- if node.where is not None:
- self._visit(node.where, constraints)
- self.solve(node, constraints)
-
- visit_delete = visit_insert
-
- def visit_set(self, node):
- if not node.defined_vars:
- node.set_possible_types([{}])
- return
- constraints = self._init_stmt(node)
- constraints.end_domain_definition()
- for relation in node.main_relations:
- self._visit(relation, constraints)
- # get constraints from the restriction subtree
- if node.where is not None:
- self._visit(node.where, constraints)
- self.solve(node, constraints)
-
- def visit_select(self, node):
- if not (node.defined_vars or node.aliases):
- node.set_possible_types([{}])
- return
- for subquery in node.with_: # resolve subqueries first
- self.visit_union(subquery.query)
- constraints = self._init_stmt(node)
- for ca in node.aliases.values():
- etypes = set(stmt.selection[ca.colnum].get_type(sol, self.kwargs)
- for stmt in ca.query.children for sol in stmt.solutions)
- constraints.add_var( ca.name, etypes )
- constraints.end_domain_definition()
- if self.uid_func:
- # check rewritten uid const
- for consts in node.stinfo['rewritten'].values():
- if not consts:
- continue
- uidtype = self.uid_func(consts[0].eval(self.kwargs))
- for const in consts:
- const.uidtype = uidtype
- # get constraints from the restriction subtree
- if node.where is not None:
- self._visit(node.where, constraints)
- elif not node.with_:
- varnames = [v.name for v in node.get_selected_variables()]
- if varnames:
- # add constraint on real relation types if no restriction
- types = [eschema.type for eschema in self.schema.entities()
- if not eschema.final]
- constraints.vars_have_same_types( varnames, types )
- self.solve(node, constraints)
-
- def visit_relation(self, relation, constraints):
- """extract constraints for an relation according to it's type"""
- if relation.is_types_restriction():
- self.visit_type_restriction(relation, constraints)
- return None
- rtype = relation.r_type
- lhs, rhs = relation.get_parts()
- if rtype == 'identity' and relation.neged(strict=True):
- return None
- if rtype in self.uid_func_mapping:
- if isinstance(relation.parent, nodes.Not) or relation.operator() != '=':
- # non final entity types
- etypes = self._nonfinal_domain
- else:
- etypes = self._uid_node_types(rhs)
- if etypes:
- constraints.var_has_types( lhs.name, etypes )
- return None
- if isinstance(rhs, nodes.Comparison):
- rhs = rhs.children[0]
- rschema = self.schema.rschema(rtype)
- if isinstance(lhs, nodes.Constant): # lhs is a constant node (simplified tree)
- if not isinstance(rhs, nodes.VariableRef):
- return None
- self._extract_constraint(constraints, rhs.name, lhs, rschema.objects)
- elif isinstance(rhs, nodes.Constant) and not rschema.final:
- # rhs.type is None <-> NULL
- if not isinstance(lhs, nodes.VariableRef) or rhs.type is None:
- return None
- self._extract_constraint(constraints, lhs.name, rhs, rschema.subjects)
- elif not isinstance(lhs, nodes.VariableRef):
- # XXX: check relation is valid
- return None
- elif isinstance(rhs, nodes.VariableRef):
- lhsvar = lhs.name
- rhsvar = rhs.name
- lhsdomain = constraints.domains[lhsvar]
- # filter according to domain necessary for column aliases
- rhsdomain = constraints.domains[rhsvar]
- res = []
- var_types = []
- same_var = (rhsvar == lhsvar)
-
- for frometype, toetypes in rschema.associations():
- fromtype = str(frometype)
- if fromtype in lhsdomain:
- totypes = set(str(t) for t in toetypes)
- ptypes = totypes & rhsdomain
- res.append( [ ([lhsvar], [str(fromtype)]),
- ([rhsvar], list(ptypes)) ] )
- if same_var and (fromtype in totypes): #ptypes ?
- var_types.append(fromtype)
- constraints.or_and(res)
- if same_var:
- constraints.var_has_types( lhsvar, var_types)
- else:
- # XXX consider rhs.get_type?
- lhsdomain = constraints.domains[lhs.name]
- ptypes = [str(subj) for subj in rschema.subjects()
- if subj in lhsdomain]
- constraints.var_has_types( lhs.name, ptypes )
- return None
-
- def visit_type_restriction(self, relation, constraints):
- lhs, rhs = relation.get_parts()
- etypes = set(c.value for c in rhs.iget_nodes(nodes.Constant)
- if c.type == 'etype')
- if relation.r_type == 'is_instance_of':
- for etype in tuple(etypes):
- for specialization in self.schema.eschema(etype).specialized_by():
- etypes.add(specialization.type)
- if relation.neged(strict=True):
- etypes = frozenset(t for t in self._nonfinal_domain if not t in etypes)
-
- constraints.var_has_types( lhs.name, [ str(t) for t in etypes ] )
-
- def visit_and(self, et, constraints):
- pass
- def visit_or(self, ou, constraints):
- pass
- def visit_not(self, et, constraints):
- pass
- def visit_comparison(self, comparison, constraints):
- pass
- def visit_mathexpression(self, mathexpression, constraints):
- pass
- def visit_function(self, function, constraints):
- pass
- def visit_variableref(self, variableref, constraints):
- pass
- def visit_constant(self, constant, constraints):
- pass
- def visit_keyword(self, keyword, constraints):
- pass
- def visit_exists(self, exists, constraints):
- pass
-
-
-class ETypeResolverIgnoreTypeRestriction(ETypeResolver):
- """same as ETypeResolver but ignore type restriction relation
-
- results are stored in as the 'allpossibletypes' key in variable'stinfo
- """
- var_solkey = 'allpossibletypes'
-
- def visit_type_restriction(self, relation, constraints):
- pass
-
- def visit_not(self, et, constraints):
- child = et.children[0]
- if isinstance(child, nodes.Relation) and \
- not self.schema.rschema(child.r_type).final:
- return True
--- a/base.py Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,201 +0,0 @@
-# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of rql.
-#
-# rql 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.
-#
-# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
-"""Base classes for RQL syntax tree nodes.
-
-Note: this module uses __slots__ to limit memory usage.
-"""
-
-__docformat__ = "restructuredtext en"
-
-from rql.utils import VisitableMixIn
-
-
-class BaseNode(VisitableMixIn):
- __slots__ = ('parent',)
-
- def __str__(self):
- return self.as_string(encoding='utf-8')
-
- def as_string(self, encoding=None, kwargs=None):
- """Return the tree as an encoded rql string."""
- raise NotImplementedError()
-
- def initargs(self, stmt):
- """Return list of arguments to give to __init__ to clone this node.
-
- I don't use __getinitargs__ because I'm not sure it should interfer with
- copy/pickle
- """
- return ()
-
- @property
- def root(self):
- """Return the root node of the tree"""
- return self.parent.root
-
- @property
- def stmt(self):
- """Return the Select node to which this node belong"""
- return self.parent.stmt
-
- @property
- def scope(self):
- """Return the scope node to which this node belong (eg Select or Exists
- node)
- """
- return self.parent.scope
-
- def get_nodes(self, klass):
- """Return the list of nodes of a given class in the subtree.
-
- :type klass: a node class (Relation, Constant, etc.)
- :param klass: the class of nodes to return
-
- :rtype: list
- """
- stack = [self]
- result = []
- while stack:
- node = stack.pop()
- if isinstance(node, klass):
- result.append(node)
- else:
- stack += node.children
- return result
-
- def iget_nodes(self, klass):
- """Return an iterator over nodes of a given class in the subtree.
-
- :type klass: a node class (Relation, Constant, etc.)
- :param klass: the class of nodes to return
-
- :rtype: iterator
- """
- stack = [self]
- while stack:
- node = stack.pop()
- if isinstance(node, klass):
- yield node
- else:
- stack += node.children
-
- def is_equivalent(self, other):
- if not other.__class__ is self.__class__:
- return False
- for i, child in enumerate(self.children):
- try:
- if not child.is_equivalent(other.children[i]):
- return False
- except IndexError:
- return False
- return True
-
- def index_path(self):
- if self.parent is None:
- return []
- myindex = self.parent.children.index(self)
- parentindexpath = self.parent.index_path()
- parentindexpath.append(myindex)
- return parentindexpath
-
- def go_to_index_path(self, path):
- if not path:
- return self
- return self.children[path[0]].go_to_index_path(path[1:])
-
- def copy(self, stmt):
- """Create and return a copy of this node and its descendant.
-
- stmt is the root node, which should be use to get new variables
- """
- new = self.__class__(*self.initargs(stmt))
- for child in self.children:
- new.append(child.copy(stmt))
- return new
-
-
-class Node(BaseNode):
- """Class for nodes of the tree which may have children (almost all...)"""
- __slots__ = ('children',)
-
- def __init__(self) :
- self.parent = None
- self.children = []
-
- def append(self, child):
- """add a node to children"""
- self.children.append(child)
- child.parent = self
-
- def remove(self, child):
- """Remove a child node. Return the removed node, its old parent and
- index in the children list.
- """
- index = self.children.index(child)
- del self.children[index]
- parent = child.parent
- child.parent = None
- return child, parent, index
-
- def insert(self, index, child):
- """insert a child node"""
- self.children.insert(index, child)
- child.parent = self
-
- def replace(self, old_child, new_child):
- """replace a child node with another"""
- i = self.children.index(old_child)
- self.children.pop(i)
- self.children.insert(i, new_child)
- new_child.parent = self
- return old_child, self, i
-
-class BinaryNode(Node):
- __slots__ = ()
-
- def __init__(self, lhs=None, rhs=None):
- Node.__init__(self)
- if not lhs is None:
- self.append(lhs)
- if not rhs is None:
- self.append(rhs)
-
- def remove(self, child):
- """Remove the child and replace this node with the other child."""
- index = self.children.index(child)
- return self.parent.replace(self, self.children[not index])
-
- def get_parts(self):
- """Return the left hand side and the right hand side of this node."""
- return self.children[0], self.children[1]
-
-
-class LeafNode(BaseNode):
- """Class optimized for leaf nodes."""
- __slots__ = ()
-
- @property
- def children(self):
- return ()
-
- def copy(self, stmt):
- """Create and return a copy of this node and its descendant.
-
- stmt is the root node, which should be use to get new variables.
- """
- return self.__class__(*self.initargs(stmt))
--- a/compare.py Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,216 +0,0 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of rql.
-#
-# rql 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.
-#
-# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
-"""Comparing syntax trees.
-
-"""
-__docformat__ = "restructuredtext en"
-
-
-from six.moves import range
-
-from rql.nodes import VariableRef, Variable, Function, Relation, Comparison
-
-def compare_tree(request1, request2):
- """Compares 2 RQL requests.
-
- :rtype: bool
- :return: True if both requests would return the same results.
- """
- return make_canon_dict(request1) == make_canon_dict(request2)
-
-def make_canon_dict(rql_tree, verbose=0):
- """Return a canonical representation of the request as a dictionnary."""
- allvars = {}
- canon = {
- 'all_variables' : allvars,
- 'selected' : [],
- 'restriction' : {},
- }
-
- canon = RQLCanonizer().visit(rql_tree, canon)
-
- # forge variable name
- for var, name_parts in allvars.values():
- name_parts.sort()
- var.name = ':'.join(name_parts)
- sort(canon)
- if verbose:
- print 'CANON FOR', rql_tree
- from pprint import pprint
- pprint(canon)
- return canon
-
-def sort(canon_dict):
- """Remove the all_variables entry and sort other entries in place."""
- del canon_dict['all_variables']
- canon_dict['selection'].sort()
- for values in canon_dict['restriction'].values():
- values.sort()
-
-class SkipChildren(Exception):
- """Signal indicating to ignore the current child."""
-
-class RQLCanonizer(object):
- """Build a dictionnary which represents a RQL syntax tree."""
-
- def visit(self, node, canon):
- try:
- node.accept(self, canon)
- except SkipChildren:
- return canon
- for c in node.children:
- self.visit(c, canon)
- return canon
-
- def visit_select(self, select, canon):
- allvars = canon['all_variables']
- for var in select.defined_vars.values():
- allvars[var] = (Variable(var.name), [])
- canon['selection'] = l = []
- selected = select.selected
- for i in range(len(selected)):
- node = selected[i]
- if isinstance(node, VariableRef):
- node = node.variable
- allvars[node][1].append(str(i))
- l.append(allvars[node][0])
- else: # Function
- l.append(node)
- for var in node.iget_nodes(VariableRef):
- var.parent.replace(var, allvars[var.variable][0])
-
- def visit_group(self, group, canon):
- canon['group'] = group
-
- def visit_sort(self, sort, canon):
- canon['sort'] = sort
-
- def visit_sortterm(self, sortterm, canon):
- pass
-
- def visit_and(self, et, canon):
- pass
-
- def visit_or(self, ou, canon):
- canon_dict = {}
- keys = []
- for expr in ou.get_nodes(Relation):
- key = '%s%s' % (expr.r_type, expr._not)
- canon_dict.setdefault(key, []).append(expr)
- keys.append(key)
- keys.sort()
- r_type = ':'.join(keys)
- r_list = canon['restriction'].setdefault(r_type, [])
- done = {}
- for key in keys:
- if key in done:
- continue
- done[key] = None
- for expr in canon_dict[key]:
- self.manage_relation(expr, canon, r_list)
- raise SkipChildren()
-
- def manage_relation(self, relation, canon, r_list):
- lhs, rhs = relation.get_parts()
- # handle special case of the IN function
- func = rhs.children[0]
- if isinstance(func, Function) and func.name == 'IN':
- if not relation._not:
- base_key = '%s%s' % (relation.r_type, relation._not)
- if not canon['restriction'][base_key]:
- del canon['restriction'][base_key]
- key = ':'.join([base_key] * len(func.children))
- r_list = canon['restriction'].setdefault(key, [])
- for e in func.children:
- eq_expr = Relation(relation.r_type, relation._not)
- eq_expr.append(lhs)
- eq_expr.append(Comparison('=', e))
- self.manage_relation(eq_expr, canon, r_list)
- # restaure parent attribute to avoid problem later
- e.parent = func
- lhs.parent = relation
- return
- # build a canonical representation for this relation
- lhs_expr_reminder = make_lhs_reminder(lhs, canon)
- rhs_expr_reminder = make_rhs_reminder(rhs, canon)
- reminder = (lhs_expr_reminder, rhs_expr_reminder)
- # avoid duplicate
- if reminder in r_list:
- return
- r_list.append(reminder)
- # make a string which represents this relation (we'll use it later
- # to build variables' name)
- expr_reminder = relation.r_type
- lhs_vars = lhs.get_nodes(VariableRef)
- if not lhs_vars:
- expr_reminder = "%s_%s" % (lhs, expr_reminder)
- rhs_vars = rhs.get_nodes(VariableRef)
- if not rhs_vars:
- expr_reminder = "%s_%s" % (expr_reminder, rhs)
-
- for var in lhs_vars + rhs_vars:
- var = var.variable
- canon['all_variables'][var][1].append(expr_reminder)
-
-
- def visit_relation(self, relation, canon):
- key = '%s%s' % (relation.r_type, relation._not)
- r_list = canon['restriction'].setdefault(key, [])
- self.manage_relation(relation, canon, r_list)
-
-
- def visit_comparison(self, comparison, canon):
- """do nothing for this node type"""
-
- def visit_mathexpression(self, mathexpression, canon):
- """do nothing for this node type"""
-
- def visit_function(self, function, canon):
- """do nothing for this node type"""
-
- def visit_variableref(self, varref, canon):
- varref.parent.replace(varref,
- canon['all_variables'][varref.variable][0])
-
- def visit_constant(self, constante, canon):
- """do nothing for this node type"""
-
- def visit_union(self, *args):
- raise NotImplementedError('union comparison not implemented')
-
-
-def make_lhs_reminder(lhs, canon):
- """Return a reminder for a relation's left hand side
- (i.e a VariableRef object).
- """
- try:
- lhs = canon['all_variables'][lhs.variable][0]
- except (KeyError, IndexError):
- pass
- return ('=', lhs)
-
-def make_rhs_reminder(rhs, canon):
- """Return a reminder for a relation's right hand side
- (i.e a Comparison object).
- """
- child = rhs.children[0]
- try:
- child = canon['all_variables'][child.variable][0]
- except AttributeError:
- pass
- return (rhs.operator, child)
--- a/editextensions.py Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of rql.
-#
-# rql 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.
-#
-# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
-"""RQL functions for manipulating syntax trees."""
-
-__docformat__ = "restructuredtext en"
-
-from rql.nodes import Constant, Variable, VariableRef, Relation, make_relation
-
-def switch_selection(rqlst, new_var, old_var):
- """Switch the select variable from old_var (VariableRef instance) to
- new_var (Variable instance).
- """
- rqlst.remove_selected(old_var)
- rqlst.add_selected(new_var, 0)
-
-def add_main_restriction(rqlst, new_type, r_type, direction):
- """The result_tree must represent the same restriction as 'rqlst' and :
- - 'new_varname' IS <new_type>
- - 'old_main_var' <r_type> 'new_varname'
- """
- new_var = rqlst.make_variable(new_type)
- # new_var IS new_type
- rqlst.add_restriction(make_relation(new_var, 'is', (new_type, 'etype'),
- Constant))
- # new_var REL old_var (ou l'inverse)
- old_var = rqlst.selected[0]
- if direction == 'subject':
- rel_rest = make_relation(old_var.variable, r_type, (new_var, 1),
- VariableRef)
- else:
- rel_rest = make_relation(new_var, r_type, (old_var.variable, 1),
- VariableRef)
- rqlst.add_restriction(rel_rest)
- return new_var
-
-def remove_has_text_relation(node):
- """Remove has_text relation."""
- for rel in node.iget_nodes(Relation):
- if rel.r_type == 'has_text':
- node.remove_node(rel)
- return
-
-def get_vars_relations(node):
- """Return a dict with 'var_names' as keys, and the list of relations which
- concern them.
- """
- exp_concerns = {}
- for exp in node.iget_nodes(Relation):
- for vref in exp.iget_nodes(VariableRef):
- exp_concerns.setdefault(vref.name, []).append(exp)
- return exp_concerns
--- a/gecode_solver.cpp Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,544 +0,0 @@
-#include <Python.h>
-#include <iostream>
-#include <string.h>
-#include <exception>
-#include "gecode/kernel.hh"
-#include "gecode/int.hh"
-#include "gecode/search.hh"
-
-#if 1
-#define debug(fmt,...)
-#else
-#define debug(fmt,...) printf(fmt, ##__VA_ARGS__)
-#endif
-
-
-#define PM_VERSION(a,b,c) ((a<<16)+(b<<8)+(c))
-// There is no easy way to test for gecode version here
-// so the build system must pass GE_VERSION accordingly
-// by default we build for 3.1.0 if GECODE_VERSION exists
-
-#ifndef GE_VERSION
-#ifndef GECODE_VERSION
-#define GE_VERSION PM_VERSION(2,1,2)
-#else
-#define GE_VERSION PM_VERSION(3,1,0)
-#endif
-#endif
-
-#if GE_VERSION < PM_VERSION(2,0,0)
-#define SELF this
-#define INT_VAR_NONE BVAR_NONE
-#define INT_VAL_MIN BVAL_MIN
-
-#elif GE_VERSION < PM_VERSION(3,0,0)
-#define SELF this
-#define SET_VAR_SIZE_MAX SET_VAR_MAX_CARD
-#define SET_VAL_MIN_INC SET_VAL_MIN
-#else
-#define SELF (*this)
-#define convexHull convex
-#endif
-
-#if GE_VERSION >= PM_VERSION(4, 0, 0)
-#define INT_VAR_NONE INT_VAR_NONE()
-#define INT_VAL_MIN INT_VAL_MIN()
-#endif
-
-using namespace std;
-using namespace Gecode;
-
-#define USE_CLOCK
-#ifdef USE_CLOCK
-#include <ctime>
-
-/// Timer interface stolen from gecode examples
-class Timer {
-private:
- clock_t t0;
-public:
- void start(void);
- double stop(void);
-};
-
-forceinline void
-Timer::start(void) {
- t0 = clock();
-}
-
-forceinline double
-Timer::stop(void) {
- return (static_cast<double>(clock()-t0) / CLOCKS_PER_SEC) * 1000.0;
-}
-#else
-#include <sys/time.h>
-#include <unistd.h>
-
-/// Timer interface stolen from gecode examples
-class Timer {
-private:
- struct timeval t0;
-public:
- void start(void);
- double stop(void);
-};
-
-forceinline void
-Timer::start(void) {
- gettimeofday( &t0, NULL );
-}
-forceinline double
-Timer::stop(void) {
- struct timeval t1;
- gettimeofday( &t1, NULL );
- return (t1.tv_sec - t0.tv_sec)+1e-6*(t1.tv_usec - t0.tv_usec);
-}
-#endif
-
-enum {
- _AND = 0,
- _OR = 1,
- _EQ = 2,
- _EQV = 3
-};
-
-class RqlError : public exception {
-};
-
-class RqlContext {
- /** Context holding the problem for solving Rql constraints
- we keep the info as Python objects and parse the problem
- during the creation of the Gecode problem
- */
-public:
- RqlContext(long nvars, PyObject* domains,
- long nvalues, PyObject* constraints, PyObject* sols):
- solutions(-1), // return every solutions
- time(-1), // time limit in case the problem is too big
- fails(-1), // ?? used by GecodeStop ...
- nvars(nvars), // Number of variables
- nvalues(nvalues), // Number of values
- constraints(constraints), // A python list, holding the root of the problem
- sols(sols), // an empty list that will receive the solutions
- domains(domains), // A PyList of PyList, one for each var,
- // holding the allowable integer values
- verbosity(false) // can help debugging
- {
- }
-
- long solutions;
- long time;
- long fails;
- long nvars;
- long nvalues;
- PyObject* constraints;
- PyObject* sols;
- PyObject* domains;
- bool verbosity;
-};
-
-class RqlSolver : public Space {
-/* A gecode Space
- this is a strange beast that requires special methods and
- behavior (mostly, copy and (bool,share,space) constructor
-*/
-protected:
- /* The variables we try to find values for
- these are the only 'public' variable of the
- problem.
-
- we use a lot more intermediate variables but
- they shouldn't be member of the space
- */
- IntVarArray variables;
-
-public:
- RqlSolver(const RqlContext& pb):
- variables(SELF, // all gecode variable keep a reference to the space
- pb.nvars, // number of variables
- 0, // minimum domain value
- pb.nvalues-1) // max domain value (included)
- {
- /* Since we manipulate Boolean expression and
- we need to assign truth value to subexpression
- eg (a+b)*(c+d) will be translated as :
- root = x1 * x2
- x1 = a+b
- x2 = c+d
- root = True
- */
- BoolVar root(SELF, 1,1);
-
- set_domains( pb.domains );
- add_constraints( pb.constraints, root );
-
- /* the branching strategy, there must be one,
- changing it might improve performance, but
- in out case, we almost never propagate (ie
- gecode solves the problem during its creation)
- */
- branch(SELF, variables, INT_VAR_NONE, INT_VAL_MIN);
- }
-
- ~RqlSolver() {};
-
- RqlSolver(bool share, RqlSolver& s) : Space(share,s)
- {
- /* this is necessary for the solver to fork space
- while branching
- */
- variables.update(SELF, share, s.variables);
- }
-
- void set_domains( PyObject* domains )
- {
- PyObject* ovalues;
- if (!PyList_Check(domains)) {
- throw RqlError();
- }
- int n = PyList_Size( domains );
- for(int var=0 ;var<n; ++var) {
- /* iterate of domains which should contains
- list of values
- domains[0] contains possible values for var[0]...
- */
- int i, nval;
- ovalues = PyList_GetItem( domains, var );
- if (!PyList_Check(ovalues)) {
- throw RqlError();
- }
- nval = PyList_Size( ovalues );
-
- /* It's a bit cumbersome to construct an IntSet, but
- it's the only way to reduce an integer domain to
- a discrete set
- */
- int* vals = new int[nval];
- for(i=0;i<nval;++i) {
- //refcount ok, borrowed ref
- vals[i] = PyInt_AsLong( PyList_GetItem( ovalues, i ) );
- if (vals[i]<0) {
- /* we don't have negative values and PyInt_AsLong returns
- -1 if the object is not an Int */
- delete [] vals;
- throw RqlError();
- }
- }
- IntSet gvalues(vals,nval);
- dom(SELF, variables[var], gvalues);
- delete [] vals;
- }
- }
-
- /* Dispatch method from Node to specific node type */
- void add_constraints( PyObject* desc, BoolVar& var )
- {
- long type;
-
- if (!PyList_Check(desc)) {
- throw RqlError();
- }
- /* the first element of each list (node) is
- a symbolic Int from _AND, _OR, _EQ, _EQV
- */
- type = PyInt_AsLong( PyList_GetItem( desc, 0 ) );
-
- switch(type) {
- case _AND:
- add_and( desc, var );
- break;
- case _OR:
- add_or( desc, var );
- break;
- case _EQ:
- add_equality( desc, var );
- break;
- case _EQV:
- add_equivalence( desc, var );
- break;
- default:
- throw RqlError();
- }
- }
-
- /* retrieve an int from a list, throw error if int is <0 */
- long get_uint( PyObject* lst, int index ) {
- PyObject* val;
- val = PyList_GetItem(lst, index);
- if (val<0) {
- throw RqlError();
- }
- return PyInt_AsLong(val);
- }
-
- /* post gecode condition for Var == Value
- we can't use domain restriction since this
- condition can be part of an OR clause
-
- so we post (var == value) <=> expr_value
- */
- void add_equality( PyObject* desc, BoolVar& expr_value ) {
- long variable, value;
-
- variable = get_uint( desc, 1 );
- value = get_uint( desc, 2 );
- if (variable==1) {
- debug("RQL:%ld == %ld ***\n", variable, value);
- } else {
- debug("RQL:%ld == %ld\n", variable, value);
- }
- rel(SELF, variables[variable], IRT_EQ, value, expr_value);
- }
-
- /* post gecode condition for Var[i] == Var[j] ... == Var[k]
-
- there's no operator for assigning chained equality to boolean
-
- so we post for 1<=i<=N (var[0] == var[i]) <=> bool[i]
- and bool[1] & ... & bool[N] <=> expr_value
- that means if all vars are equal expr_value is true
- if all vars are different from var[0] expr_value is false
- if some are equals and some false, the constraint is unsatisfiable
- */
- void add_equivalence( PyObject* desc, BoolVar& expr_value ) {
- int len = PyList_Size(desc);
- int var0 = get_uint( desc, 1 );
- BoolVarArray terms(SELF, len-2,0,1);
- debug("RQL:EQV(%d",var0);
- for (int i=1;i<len-1;++i) {
- int var1 = get_uint(desc, i+1);
- debug(",%d",var1);
- rel(SELF, variables[var0], IRT_EQ, variables[var1], terms[i-1] );
- }
- debug(")\n");
-#if GE_VERSION<PM_VERSION(2,0,0)
- BoolVarArgs terms_args(terms);
- bool_and(SELF, terms_args, expr_value);
-#else
- rel(SELF, BOT_AND, terms, expr_value);
-#endif
- }
-
- /* simple and relation between nodes */
- void add_and( PyObject* desc, BoolVar& var ) {
- int len = PyList_Size(desc);
- BoolVarArray terms(SELF, len-1,0,1);
-
- debug("RQL:AND(\n");
- for(int i=0;i<len-1;++i) {
- PyObject* expr = PyList_GetItem(desc, i+1);
- add_constraints( expr, terms[i] );
- }
- debug("RQL:)\n");
-#if GE_VERSION<PM_VERSION(2,0,0)
- BoolVarArgs terms_args(terms);
- bool_and(SELF, terms_args, var);
-#else
- rel(SELF, BOT_AND, terms, var);
-#endif
- }
-
- /* simple or relation between nodes */
- void add_or( PyObject* desc, BoolVar& var ) {
- int len = PyList_Size(desc);
- BoolVarArray terms(SELF, len-1,0,1);
-
- debug("RQL:OR(\n");
- for(int i=0;i<len-1;++i) {
- PyObject* expr = PyList_GetItem(desc, i+1);
- add_constraints( expr, terms[i] );
- }
- debug("RQL:)\n");
-#if GE_VERSION<PM_VERSION(2,0,0)
- BoolVarArgs terms_args(terms);
- bool_or(SELF, terms_args, var);
-#else
- rel(SELF, BOT_OR, terms, var);
-#endif
-
- }
-
- template <template<class> class Engine>
- static void run( RqlContext& pb, Search::Stop* stop )
- {
- double t0 = 0;
- int i = pb.solutions;
- Timer t;
- RqlSolver* s = new RqlSolver( pb );
- t.start();
- unsigned int n_p = 0;
- unsigned int n_b = 0;
- if (s->status() != SS_FAILED) {
- n_p = s->propagators();
-#if GE_VERSION<PM_VERSION(3,2,0)
- n_b = s->branchings();
-#else
- n_b = s->branchers();
-#endif
- }
-#if GE_VERSION<PM_VERSION(2,0,0)
- Engine<RqlSolver> e(s);
-#else
- Search::Options opts;
- //opts.c_d = pb.c_d;
- //opts.a_d = pb.a_d;
- opts.stop = stop;
- Engine<RqlSolver> e(s, opts);
-#endif
- delete s;
- do {
- RqlSolver* ex = e.next();
- if (ex == NULL)
- break;
-
- ex->add_new_solution(pb);
-
- delete ex;
- t0 = t0 + t.stop();
- } while (--i != 0 && (pb.time<0 || t0 < pb.time));
- Search::Statistics stat = e.statistics();
- if (pb.verbosity) {
- cout << endl;
- cout << "Initial" << endl
- << "\tpropagators: " << n_p << endl
- << "\tbranchings: " << n_b << endl
- << endl
- << "Summary" << endl
- << "\truntime: " << t.stop() << endl
- << "\tsolutions: " << abs(static_cast<int>(pb.solutions) - i) << endl
- << "\tpropagations: " << stat.propagate << endl
- << "\tfailures: " << stat.fail << endl
-#if GE_VERSION < PM_VERSION(3,0,0)
- << "\tclones: " << stat.clone << endl
- << "\tcommits: " << stat.commit << endl
-#else
- << "\tdepth: " << stat.depth << endl
- << "\tnode: " << stat.node << endl
-#endif
-#if GE_VERSION < PM_VERSION(4,2,0)
- << "\tpeak memory: "
- << static_cast<int>((stat.memory+1023) / 1024) << " KB"
- << endl
-#endif
- ;
- }
- }
-
- /* We append each solutions to `sols` as a
- tuple `t` of the values assigned to each var
- that is t[i] = solution for var[i]
- */
- virtual void add_new_solution(RqlContext& pb) {
- PyObject *tuple, *ival;
-
- tuple = PyTuple_New( pb.nvars );
-
- for(int i=0;i<pb.nvars;++i) {
- ival = PyInt_FromLong( variables[i].val() );
- PyTuple_SetItem( tuple, i, ival );
- }
- PyList_Append( pb.sols, tuple );
- }
-
- /* another function need by gecode kernel */
- virtual Space* copy(bool share) {
- return new RqlSolver(share, *this);
- }
-
-};
-
-class FailTimeStop : public Search::Stop {
-private:
- Search::TimeStop *ts;
- Search::FailStop *fs;
-public:
- FailTimeStop(int fails, int time):ts(0L),fs(0L) {
- if (time>=0)
- ts = new Search::TimeStop(time);
- if (fails>=0) {
- fs = new Search::FailStop(fails);
- }
- }
-#if GE_VERSION < PM_VERSION(3,1,0)
- bool stop(const Search::Statistics& s) {
- int sigs = PyErr_CheckSignals();
- bool fs_stop = false;
- bool ts_stop = false;
- if (fs) {
- fs_stop = fs->stop(s);
- }
- if (ts) {
- ts_stop = ts->stop(s);
- }
- return sigs || fs_stop || ts_stop;
- }
-#else
- /* from gecode 3.1.0 */
- bool stop(const Search::Statistics& s, const Search::Options &o) {
- int sigs = PyErr_CheckSignals();
- bool fs_stop = false;
- bool ts_stop = false;
- if (fs) {
- fs_stop = fs->stop(s,o);
- }
- if (ts) {
- ts_stop = ts->stop(s,o);
- }
- return sigs || fs_stop || ts_stop;
- }
-#endif
-
- /// Create appropriate stop-object
- static Search::Stop* create(int fails, int time) {
- return new FailTimeStop(fails, time);
- }
-};
-
-static void _solve( RqlContext& ctx )
-{
- Search::Stop *stop = FailTimeStop::create(ctx.fails, ctx.time);
-
- RqlSolver::run<DFS>( ctx, stop );
-}
-
-
-static PyObject *
-rql_solve(PyObject *self, PyObject *args)
-{
- PyObject* sols = 0L;
- PyObject* constraints;
- PyObject* domains;
- long nvars, nvalues;
- if (!PyArg_ParseTuple(args, "OiO", &domains, &nvalues, &constraints))
- return NULL;
- sols = PyList_New(0);
- try {
- if (!PyList_Check(domains)) {
- throw RqlError();
- }
- nvars = PyList_Size(domains);
- RqlContext ctx(nvars, domains, nvalues, constraints, sols );
- _solve( ctx );
- } catch(RqlError& e) {
- Py_DECREF(sols);
- PyErr_SetString(PyExc_RuntimeError, "Error parsing constraints");
- return NULL;
- };
- return sols;
-}
-
-static PyMethodDef SolveRqlMethods[] = {
- {"solve", rql_solve, METH_VARARGS,
- "Solve RQL variable types problem."},
- {NULL, NULL, 0, NULL} /* Sentinel */
-};
-
-
-PyMODINIT_FUNC
-initrql_solve(void)
-{
- PyObject* m;
- m = Py_InitModule("rql_solve", SolveRqlMethods);
- if (m == NULL)
- return;
-}
--- a/interfaces.py Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of rql.
-#
-# rql 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.
-#
-# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
-"""Interfaces used by the RQL package.
-
-"""
-__docformat__ = "restructuredtext en"
-
-from logilab.common.interface import Interface
-
-class ISchema(Interface):
- """RQL expects some base types to exists: String, Float, Int, Boolean, Date
- and a base relation : is
- """
-
- def has_entity(self, etype):
- """Return true if the given type is defined in the schema.
- """
-
- def has_relation(self, rtype):
- """Return true if the given relation's type is defined in the schema.
- """
-
- def entities(self, schema=None):
- """Return the list of possible types.
-
- If schema is not None, return a list of schemas instead of types.
- """
-
- def relations(self, schema=None):
- """Return the list of possible relations.
-
- If schema is not None, return a list of schemas instead of relation's
- types.
- """
-
- def relation_schema(self, rtype):
- """Return the relation schema for the given relation type.
- """
-
-
-class IRelationSchema(Interface):
- """Interface for Relation schema (a relation is a named oriented link
- between two entities).
- """
-
- def associations(self):
- """Return a list of (fromtype, [totypes]) defining between which types
- this relation may exists.
- """
-
- def subjects(self):
- """Return a list of types which can be subject of this relation.
- """
-
- def objects(self):
- """Return a list of types which can be object of this relation.
- """
-
-class IEntitySchema(Interface):
- """Interface for Entity schema."""
-
- def is_final(self):
- """Return true if the entity is a final entity (ie cannot be used
- as subject of a relation).
- """
-
--- a/nodes.py Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1123 +0,0 @@
-# copyright 2004-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of rql.
-#
-# rql 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.
-#
-# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
-"""RQL syntax tree nodes.
-
-This module defines all the nodes we can find in a RQL Syntax tree, except
-root nodes, defined in the `stmts` module.
-"""
-
-__docformat__ = "restructuredtext en"
-
-import sys
-from itertools import chain
-from decimal import Decimal
-from datetime import datetime, date, time, timedelta
-from time import localtime
-
-from logilab.database import DYNAMIC_RTYPE
-
-from rql import CoercionError, RQLException
-from rql.base import BaseNode, Node, BinaryNode, LeafNode
-from rql.utils import (function_description, quote, uquote, common_parent,
- VisitableMixIn)
-
-CONSTANT_TYPES = frozenset((None, 'Date', 'Datetime', 'Boolean', 'Float', 'Int',
- 'String', 'Substitute', 'etype'))
-
-
-ETYPE_PYOBJ_MAP = { bool: 'Boolean',
- int: 'Int',
- float: 'Float',
- Decimal: 'Decimal',
- str: 'String',
- datetime: 'Datetime',
- date: 'Date',
- time: 'Time',
- timedelta: 'Interval',
- }
-if sys.version_info < (3,):
- ETYPE_PYOBJ_MAP[long] = 'Int'
- ETYPE_PYOBJ_MAP[unicode] = 'String'
-
-KEYWORD_MAP = {'NOW' : datetime.now,
- 'TODAY': date.today}
-
-def etype_from_pyobj(value):
- """guess yams type from python value"""
- # note:
- # * Password is not selectable so no problem
- # * use type(value) and not value.__class__ since C instances may have no
- # __class__ attribute
- return ETYPE_PYOBJ_MAP[type(value)]
-
-def variable_ref(var):
- """get a VariableRef"""
- if isinstance(var, Variable):
- return VariableRef(var, noautoref=1)
- assert isinstance(var, VariableRef)
- return var
-
-def variable_refs(node):
- for vref in node.iget_nodes(VariableRef):
- if isinstance(vref.variable, Variable):
- yield vref
-
-
-class OperatorExpressionMixin(object):
-
- def initargs(self, stmt):
- """return list of arguments to give to __init__ to clone this node"""
- return (self.operator,)
-
- def is_equivalent(self, other):
- if not Node.is_equivalent(self, other):
- return False
- return self.operator == other.operator
-
- def get_description(self, mainindex, tr):
- """if there is a variable in the math expr used as rhs of a relation,
- return the name of this relation, else return the type of the math
- expression
- """
- try:
- return tr(self.get_type())
- except CoercionError:
- for vref in self.iget_nodes(VariableRef):
- vtype = vref.get_description(mainindex, tr)
- if vtype != 'Any':
- return tr(vtype)
-
-
-class HSMixin(object):
- """mixin class for classes which may be the lhs or rhs of an expression"""
- __slots__ = ()
-
- def relation(self):
- """return the parent relation where self occurs or None"""
- try:
- return self.parent.relation()
- except AttributeError:
- return None
-
- def get_description(self, mainindex, tr):
- mytype = self.get_type()
- if mytype != 'Any':
- return tr(mytype)
- return 'Any'
-
-
-# rql st edition utilities ####################################################
-
-def make_relation(var, rel, rhsargs, rhsclass, operator='='):
- """build an relation equivalent to '<var> rel = <cst>'"""
- cmpop = Comparison(operator)
- cmpop.append(rhsclass(*rhsargs))
- relation = Relation(rel)
- if hasattr(var, 'variable'):
- var = var.variable
- relation.append(VariableRef(var))
- relation.append(cmpop)
- return relation
-
-def make_constant_restriction(var, rtype, value, ctype, operator='='):
- if ctype is None:
- ctype = etype_from_pyobj(value)
- if isinstance(value, (set, frozenset, tuple, list, dict)):
- if len(value) > 1:
- rel = make_relation(var, rtype, ('IN',), Function, operator)
- infunc = rel.children[1].children[0]
- for atype in sorted(value):
- infunc.append(Constant(atype, ctype))
- return rel
- value = next(iter(value))
- return make_relation(var, rtype, (value, ctype), Constant, operator)
-
-
-class EditableMixIn(object):
- """mixin class to add edition functionalities to some nodes, eg root nodes
- (statement) and Exists nodes
- """
- __slots__ = ()
-
- @property
- def undo_manager(self):
- return self.root.undo_manager
-
- @property
- def should_register_op(self):
- root = self.root
- # root is None during parsing
- return root is not None and root.should_register_op
-
- def remove_node(self, node, undefine=False):
- """remove the given node from the tree
-
- USE THIS METHOD INSTEAD OF .remove to get correct variable references
- handling
- """
- # unregister variable references in the removed subtree
- parent = node.parent
- stmt = parent.stmt
- for varref in node.iget_nodes(VariableRef):
- varref.unregister_reference()
- if undefine and not varref.variable.stinfo['references']:
- stmt.undefine_variable(varref.variable)
- # remove return actually removed node and its parent
- node, parent, index = parent.remove(node)
- if self.should_register_op:
- from rql.undo import RemoveNodeOperation
- self.undo_manager.add_operation(RemoveNodeOperation(node, parent, stmt, index))
-
- def add_restriction(self, relation):
- """add a restriction relation"""
- r = self.where
- if r is not None:
- newnode = And(r, relation)
- self.set_where(newnode)
- if self.should_register_op:
- from rql.undo import ReplaceNodeOperation
- self.undo_manager.add_operation(ReplaceNodeOperation(r, newnode))
- else:
- self.set_where(relation)
- if self.should_register_op:
- from rql.undo import AddNodeOperation
- self.undo_manager.add_operation(AddNodeOperation(relation))
- return relation
-
- def add_constant_restriction(self, var, rtype, value, ctype,
- operator='='):
- """builds a restriction node to express a constant restriction:
-
- variable rtype = value
- """
- restr = make_constant_restriction(var, rtype, value, ctype, operator)
- return self.add_restriction(restr)
-
- def add_relation(self, lhsvar, rtype, rhsvar):
- """builds a restriction node to express '<var> eid <eid>'"""
- return self.add_restriction(make_relation(lhsvar, rtype, (rhsvar,),
- VariableRef))
-
- def add_eid_restriction(self, var, eid, c_type='Int'):
- """builds a restriction node to express '<var> eid <eid>'"""
- assert c_type in ('Int', 'Substitute'), "Error got c_type=%r in eid restriction" % c_type
- return self.add_constant_restriction(var, 'eid', eid, c_type)
-
- def add_type_restriction(self, var, etype):
- """builds a restriction node to express : variable is etype"""
- typerel = var.stinfo.get('typerel', None)
- if typerel:
- if typerel.r_type == 'is':
- istarget = typerel.children[1].children[0]
- if isinstance(istarget, Constant):
- etypes = (istarget.value,)
- else: # Function (IN)
- etypes = [et.value for et in istarget.children]
- if etype not in etypes:
- raise RQLException('%r not in %r' % (etype, etypes))
- if len(etypes) > 1:
- # iterate a copy of children because it's modified inplace
- for child in istarget.children[:]:
- if child.value != etype:
- typerel.stmt.remove_node(child)
- return typerel
- else:
- assert typerel.r_type == 'is_instance_of'
- typerel.stmt.remove_node(typerel)
- return self.add_constant_restriction(var, 'is', etype, 'etype')
-
-
-# base RQL nodes ##############################################################
-
-class SubQuery(BaseNode):
- """WITH clause"""
- __slots__ = ('aliases', 'query')
- def __init__(self, aliases=None, query=None):
- if aliases is not None:
- self.set_aliases(aliases)
- if query is not None:
- self.set_query(query)
-
- def set_aliases(self, aliases):
- self.aliases = aliases
- for node in aliases:
- node.parent = self
-
- def set_query(self, node):
- self.query = node
- node.parent = self
-
- def copy(self, stmt):
- return SubQuery([v.copy(stmt) for v in self.aliases], self.query.copy())
-
- @property
- def children(self):
- return self.aliases + [self.query]
-
- def as_string(self, encoding=None, kwargs=None):
- return '%s BEING (%s)' % (','.join(v.name for v in self.aliases),
- self.query.as_string(encoding, kwargs))
- def __repr__(self):
- return '%s BEING (%s)' % (','.join(repr(v) for v in self.aliases),
- repr(self.query))
-
-class And(BinaryNode):
- """a logical AND node (binary)"""
- __slots__ = ()
-
- def as_string(self, encoding=None, kwargs=None):
- """return the tree as an encoded rql string"""
- return '%s, %s' % (self.children[0].as_string(encoding, kwargs),
- self.children[1].as_string(encoding, kwargs))
- def __repr__(self):
- return '%s AND %s' % (repr(self.children[0]), repr(self.children[1]))
-
- def ored(self, traverse_scope=False, _fromnode=None):
- return self.parent.ored(traverse_scope, _fromnode or self)
-
- def neged(self, traverse_scope=False, _fromnode=None):
- return self.parent.neged(traverse_scope, _fromnode or self)
-
-
-class Or(BinaryNode):
- """a logical OR node (binary)"""
- __slots__ = ()
-
- def as_string(self, encoding=None, kwargs=None):
- return '(%s) OR (%s)' % (self.children[0].as_string(encoding, kwargs),
- self.children[1].as_string(encoding, kwargs))
-
- def __repr__(self):
- return '%s OR %s' % (repr(self.children[0]), repr(self.children[1]))
-
- def ored(self, traverse_scope=False, _fromnode=None):
- return self
-
- def neged(self, traverse_scope=False, _fromnode=None):
- return self.parent.neged(traverse_scope, _fromnode or self)
-
-
-class Not(Node):
- """a logical NOT node (unary)"""
- __slots__ = ()
- def __init__(self, expr=None):
- Node.__init__(self)
- if expr is not None:
- self.append(expr)
-
- def as_string(self, encoding=None, kwargs=None):
- if isinstance(self.children[0], (Exists, Relation)):
- return 'NOT %s' % self.children[0].as_string(encoding, kwargs)
- return 'NOT (%s)' % self.children[0].as_string(encoding, kwargs)
-
- def __repr__(self, encoding=None, kwargs=None):
- return 'NOT (%s)' % repr(self.children[0])
-
- def ored(self, traverse_scope=False, _fromnode=None):
- # XXX consider traverse_scope ?
- return self.parent.ored(traverse_scope, _fromnode or self)
-
- def neged(self, traverse_scope=False, _fromnode=None, strict=False):
- return self
-
- def remove(self, child):
- return self.parent.remove(self)
-
-# def parent_scope_property(attr):
-# def _get_parent_attr(self, attr=attr):
-# return getattr(self.parent.scope, attr)
-# return property(_get_parent_attr)
-# # editable compatibility
-# for method in ('remove_node', 'add_restriction', 'add_constant_restriction',
-# 'add_relation', 'add_eid_restriction', 'add_type_restriction'):
-# setattr(Not, method, parent_scope_property(method))
-
-
-class Exists(EditableMixIn, BaseNode):
- """EXISTS sub query"""
- __slots__ = ('query',)
-
- def __init__(self, restriction=None):
- if restriction is not None:
- self.set_where(restriction)
- else:
- self.query = None
-
- def copy(self, stmt):
- new = self.query.copy(stmt)
- return Exists(new)
-
- @property
- def children(self):
- return (self.query,)
-
- def append(self, node):
- assert self.query is None
- self.query = node
- node.parent = self
-
- def is_equivalent(self, other):
- raise NotImplementedError
-
- def as_string(self, encoding=None, kwargs=None):
- content = self.query and self.query.as_string(encoding, kwargs)
- return 'EXISTS(%s)' % content
-
- def __repr__(self):
- return 'EXISTS(%s)' % repr(self.query)
-
- def set_where(self, node):
- self.query = node
- node.parent = self
-
- @property
- def where(self):
- return self.query
-
- def replace(self, oldnode, newnode):
- assert oldnode is self.query
- self.query = newnode
- newnode.parent = self
- return oldnode, self, None
-
- def remove(self, child):
- return self.parent.remove(self)
-
- @property
- def scope(self):
- return self
-
- def ored(self, traverse_scope=False, _fromnode=None):
- if not traverse_scope:
- if _fromnode is not None: # stop here
- return False
- return self.parent.ored(traverse_scope, self)
- return self.parent.ored(traverse_scope, _fromnode)
-
- def neged(self, traverse_scope=False, _fromnode=None, strict=False):
- if not traverse_scope:
- if _fromnode is not None: # stop here
- return False
- return self.parent.neged(self)
- elif strict:
- return isinstance(self.parent, Not)
- return self.parent.neged(traverse_scope, _fromnode)
-
-
-class Relation(Node):
- """a RQL relation"""
- __slots__ = ('r_type', 'optional',
- '_q_sqltable', '_q_needcast') # XXX cubicweb specific
-
- def __init__(self, r_type, optional=None):
- Node.__init__(self)
- self.r_type = r_type.encode()
- self.optional = None
- self.set_optional(optional)
-
- def initargs(self, stmt):
- """return list of arguments to give to __init__ to clone this node"""
- return self.r_type, self.optional
-
- def is_equivalent(self, other):
- if not Node.is_equivalent(self, other):
- return False
- if self.r_type != other.r_type:
- return False
- return True
-
- def as_string(self, encoding=None, kwargs=None):
- """return the tree as an encoded rql string"""
- try:
- lhs = self.children[0].as_string(encoding, kwargs)
- if self.optional in ('left', 'both'):
- lhs += '?'
- rhs = self.children[1].as_string(encoding, kwargs)
- if self.optional in ('right', 'both'):
- rhs += '?'
- except IndexError:
- return repr(self) # not fully built relation
- return '%s %s %s' % (lhs, self.r_type, rhs)
-
- def __repr__(self):
- if self.optional:
- rtype = '%s[%s]' % (self.r_type, self.optional)
- else:
- rtype = self.r_type
- try:
- return 'Relation(%r %s %r)' % (self.children[0], rtype,
- self.children[1])
- except IndexError:
- return 'Relation(%s)' % self.r_type
-
- def set_optional(self, optional):
- assert optional in (None, 'left', 'right')
- if optional is not None:
- if self.optional and self.optional != optional:
- self.optional = 'both'
- else:
- self.optional = optional
-
- def relation(self):
- """return the parent relation where self occurs or None"""
- return self
-
- def ored(self, traverse_scope=False, _fromnode=None):
- return self.parent.ored(traverse_scope, _fromnode or self)
-
- def neged(self, traverse_scope=False, _fromnode=None, strict=False):
- if strict:
- return isinstance(self.parent, Not)
- return self.parent.neged(traverse_scope, _fromnode or self)
-
- def is_types_restriction(self):
- if self.r_type not in ('is', 'is_instance_of'):
- return False
- rhs = self.children[1]
- if isinstance(rhs, Comparison):
- rhs = rhs.children[0]
- # else: relation used in SET OR DELETE selection
- return ((isinstance(rhs, Constant) and rhs.type == 'etype')
- or (isinstance(rhs, Function) and rhs.name == 'IN'))
-
- def operator(self):
- """return the operator of the relation <, <=, =, >=, > and LIKE
-
- (relations used in SET, INSERT and DELETE definitions don't have
- an operator as rhs)
- """
- rhs = self.children[1]
- if isinstance(rhs, Comparison):
- return rhs.operator
- return '='
-
- def get_parts(self):
- """return the left hand side and the right hand side of this relation
- """
- lhs = self.children[0]
- rhs = self.children[1]
- return lhs, rhs
-
- def get_variable_parts(self):
- """return the left hand side and the right hand side of this relation,
- ignoring comparison
- """
- lhs = self.children[0]
- rhs = self.children[1].children[0]
- return lhs, rhs
-
- def change_optional(self, value):
- root = self.root
- if root is not None and root.should_register_op and value != self.optional:
- from rql.undo import SetOptionalOperation
- root.undo_manager.add_operation(SetOptionalOperation(self, self.optional))
- self.set_optional(value)
-
-
-CMP_OPERATORS = frozenset(('=', '!=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE', 'REGEXP'))
-
-class Comparison(HSMixin, Node):
- """handle comparisons:
-
- <, <=, =, >=, > LIKE and ILIKE operators have a unique children.
- """
- __slots__ = ('operator', 'optional')
-
- def __init__(self, operator, value=None, optional=None):
- Node.__init__(self)
- if operator == '~=':
- operator = 'ILIKE'
- assert operator in CMP_OPERATORS, operator
- self.operator = operator.encode()
- self.optional = optional
- if value is not None:
- self.append(value)
-
- def initargs(self, stmt):
- """return list of arguments to give to __init__ to clone this node"""
- return (self.operator, None, self.optional)
-
- def set_optional(self, left, right):
- if left and right:
- self.optional = 'both'
- elif left:
- self.optional = 'left'
- elif right:
- self.optional = 'right'
-
- def is_equivalent(self, other):
- if not Node.is_equivalent(self, other):
- return False
- return self.operator == other.operator
-
- def as_string(self, encoding=None, kwargs=None):
- """return the tree as an encoded rql string"""
- if len(self.children) == 0:
- return self.operator
- if len(self.children) == 2:
- lhsopt = rhsopt = ''
- if self.optional in ('left', 'both'):
- lhsopt = '?'
- if self.optional in ('right', 'both'):
- rhsopt = '?'
- return '%s%s %s %s%s' % (self.children[0].as_string(encoding, kwargs),
- lhsopt, self.operator.encode(),
- self.children[1].as_string(encoding, kwargs), rhsopt)
- if self.operator == '=':
- return self.children[0].as_string(encoding, kwargs)
- return '%s %s' % (self.operator.encode(),
- self.children[0].as_string(encoding, kwargs))
-
- def __repr__(self):
- return '%s %s' % (self.operator, ', '.join(repr(c) for c in self.children))
-
-
-class MathExpression(OperatorExpressionMixin, HSMixin, BinaryNode):
- """Mathematical Operators"""
- __slots__ = ('operator',)
-
- def __init__(self, operator, lhs=None, rhs=None):
- BinaryNode.__init__(self, lhs, rhs)
- self.operator = operator.encode()
-
- def as_string(self, encoding=None, kwargs=None):
- """return the tree as an encoded rql string"""
- return '(%s %s %s)' % (self.children[0].as_string(encoding, kwargs),
- self.operator.encode(),
- self.children[1].as_string(encoding, kwargs))
-
- def __repr__(self):
- return '(%r %s %r)' % (self.children[0], self.operator,
- self.children[1])
-
- def get_type(self, solution=None, kwargs=None):
- """return the type of object returned by this function if known
-
- solution is an optional variable/etype mapping
- """
- lhstype = self.children[0].get_type(solution, kwargs)
- rhstype = self.children[1].get_type(solution, kwargs)
- key = (self.operator, lhstype, rhstype)
- try:
- return {('-', 'Date', 'Datetime'): 'Interval',
- ('-', 'Datetime', 'Datetime'): 'Interval',
- ('-', 'Date', 'Date'): 'Interval',
- ('-', 'Date', 'Time'): 'Datetime',
- ('+', 'Date', 'Time'): 'Datetime',
- ('-', 'Datetime', 'Time'): 'Datetime',
- ('+', 'Datetime', 'Time'): 'Datetime',
- }[key]
- except KeyError:
- if lhstype == rhstype:
- return rhstype
- if sorted((lhstype, rhstype)) == ['Float', 'Int']:
- return 'Float'
- raise CoercionError(key)
-
-
-class UnaryExpression(OperatorExpressionMixin, Node):
- """Unary Operators"""
- __slots__ = ('operator',)
-
- def __init__(self, operator, child=None):
- Node.__init__(self)
- self.operator = operator.encode()
- if child is not None:
- self.append(child)
-
- def as_string(self, encoding=None, kwargs=None):
- """return the tree as an encoded rql string"""
- return '%s%s' % (self.operator.encode(),
- self.children[0].as_string(encoding, kwargs))
-
- def __repr__(self):
- return '%s%r' % (self.operator, self.children[0])
-
- def get_type(self, solution=None, kwargs=None):
- """return the type of object returned by this expression if known
-
- solution is an optional variable/etype mapping
- """
- return self.children[0].get_type(solution, kwargs)
-
-
-class Function(HSMixin, Node):
- """Class used to deal with aggregat functions (sum, min, max, count, avg)
- and latter upper(), lower() and other RQL transformations functions
- """
- __slots__ = ('name',)
-
- def __init__(self, name):
- Node.__init__(self)
- self.name = name.strip().upper().encode()
-
- def initargs(self, stmt):
- """return list of arguments to give to __init__ to clone this node"""
- return (self.name,)
-
- def is_equivalent(self, other):
- if not Node.is_equivalent(self, other):
- return False
- return self.name == other.name
-
- def as_string(self, encoding=None, kwargs=None):
- """return the tree as an encoded rql string"""
- return '%s(%s)' % (self.name, ', '.join(c.as_string(encoding, kwargs)
- for c in self.children))
-
- def __repr__(self):
- return '%s(%s)' % (self.name, ', '.join(repr(c) for c in self.children))
-
- def get_type(self, solution=None, kwargs=None):
- """return the type of object returned by this function if known
-
- solution is an optional variable/etype mapping
- """
- func_descr = self.descr()
- rtype = func_descr.rql_return_type(self)
- if rtype is None:
- # XXX support one variable ref child
- try:
- rtype = solution and solution.get(self.children[0].name)
- except AttributeError:
- pass
- return rtype or 'Any'
-
- def get_description(self, mainindex, tr):
- return self.descr().st_description(self, mainindex, tr)
-
- def descr(self):
- """return the type of object returned by this function if known"""
- return function_description(self.name)
-
-
-class Constant(HSMixin, LeafNode):
- """String, Int, TRUE, FALSE, TODAY, NULL..."""
- __slots__ = ('value', 'type', 'uid', 'uidtype')
-
- def __init__(self, value, c_type, _uid=False, _uidtype=None):
- assert c_type in CONSTANT_TYPES, "Error got c_type="+repr(c_type)
- LeafNode.__init__(self) # don't care about Node attributes
- self.value = value
- self.type = c_type
- # updated by the annotator/analyzer if necessary
- self.uid = _uid
- self.uidtype = _uidtype
-
- def initargs(self, stmt):
- """return list of arguments to give to __init__ to clone this node"""
- return (self.value, self.type, self.uid, self.uidtype)
-
- def is_equivalent(self, other):
- if not LeafNode.is_equivalent(self, other):
- return False
- return self.type == other.type and self.value == other.value
-
- def as_string(self, encoding=None, kwargs=None):
- """return the tree as an encoded rql string (an unicode string is
- returned if encoding is None)
- """
- if self.type is None:
- return 'NULL'
- if self.type in ('etype', 'Date', 'Datetime', 'Int', 'Float'):
- return str(self.value)
- if self.type == 'Boolean':
- return self.value and 'TRUE' or 'FALSE'
- if self.type == 'Substitute':
- # XXX could get some type information from self.root().schema()
- # and linked relation
- if kwargs is not None:
- value = kwargs.get(self.value, '???')
- if isinstance(value, unicode):
- if encoding:
- value = quote(value.encode(encoding))
- else:
- value = uquote(value)
- elif isinstance(value, str):
- value = quote(value)
- else:
- value = repr(value)
- return value
- return '%%(%s)s' % self.value
- if isinstance(self.value, unicode):
- if encoding is not None:
- return quote(self.value.encode(encoding))
- return uquote(self.value)
- return repr(self.value)
-
- def __repr__(self):
- return self.as_string('utf8')
-
- def eval(self, kwargs):
- if self.type == 'Substitute':
- return kwargs[self.value]
- if self.type in ('Date', 'Datetime'): # TODAY, NOW
- return KEYWORD_MAP[self.value]()
- return self.value
-
- def get_type(self, solution=None, kwargs=None):
- if self.uid:
- return self.uidtype
- if self.type == 'Substitute':
- if kwargs is not None:
- return etype_from_pyobj(self.eval(kwargs))
- return 'String'
- return self.type
-
-
-class VariableRef(HSMixin, LeafNode):
- """a reference to a variable in the syntax tree"""
- __slots__ = ('variable', 'name')
-
- def __init__(self, variable, noautoref=None):
- LeafNode.__init__(self) # don't care about Node attributes
- self.variable = variable
- self.name = variable.name
- if noautoref is None:
- self.register_reference()
-
- def initargs(self, stmt):
- """return list of arguments to give to __init__ to clone this node"""
- var = self.variable
- if isinstance(var, ColumnAlias):
- newvar = stmt.get_variable(self.name, var.colnum)
- else:
- newvar = stmt.get_variable(self.name)
- newvar.init_copy(var)
- return (newvar,)
-
- def is_equivalent(self, other):
- if not LeafNode.is_equivalent(self, other):
- return False
- return self.name == other.name
-
- def as_string(self, encoding=None, kwargs=None):
- """return the tree as an encoded rql string"""
- return self.name
-
- def __repr__(self):
- return 'VarRef(%r)' % self.variable
-
- def __cmp__(self, other):
- return not self.is_equivalent(other)
-
- def register_reference(self):
- self.variable.register_reference(self)
-
- def unregister_reference(self):
- self.variable.unregister_reference(self)
-
- def get_type(self, solution=None, kwargs=None):
- return self.variable.get_type(solution, kwargs)
-
- def get_description(self, mainindex, tr):
- return self.variable.get_description(mainindex, tr)
-
- def root_selection_index(self):
- """return the index of this variable reference *in the root selection*
- if it's selected, else None
- """
- myidx = self.variable.selected_index()
- if myidx is None:
- return None
- stmt = self.stmt
- union = stmt.parent
- if union.parent is None:
- return myidx
- # first .parent is the SubQuery node, we want the Select node
- parentselect = union.parent.parent
- for ca in parentselect.aliases.itervalues():
- if ca.query is union and ca.colnum == myidx:
- caidx = ca.selected_index()
- if caidx is None:
- return None
- return parentselect.selection[caidx].root_selection_index()
-
-
-class SortTerm(Node):
- """a sort term bind a variable to the boolean <asc>
- if <asc> ascendant sort
- else descendant sort
- """
- __slots__ = ('asc',)
-
- def __init__(self, variable, asc=1, copy=None):
- Node.__init__(self)
- self.asc = asc
- if copy is None:
- self.append(variable)
-
- def initargs(self, stmt):
- """return list of arguments to give to __init__ to clone this node"""
- return (None, self.asc, True)
-
- def is_equivalent(self, other):
- if not Node.is_equivalent(self, other):
- return False
- return self.asc == other.asc
-
- def as_string(self, encoding=None, kwargs=None):
- if self.asc:
- return '%s' % self.term
- return '%s DESC' % self.term
-
- def __repr__(self):
- if self.asc:
- return '%r ASC' % self.term
- return '%r DESC' % self.term
-
- @property
- def term(self):
- return self.children[0]
-
-
-
-###############################################################################
-
-class Referenceable(VisitableMixIn):
- __slots__ = ('name', 'stinfo', 'stmt')
-
- def __init__(self, name):
- self.name = name.strip().encode()
- # used to collect some global information about the syntax tree
- self.stinfo = {
- # link to VariableReference objects in the syntax tree
- 'references': set(),
- }
- # reference to the selection
- self.stmt = None
-
- @property
- def schema(self):
- return self.stmt.root.schema
-
- def init_copy(self, old):
- # should copy variable's possibletypes on copy
- if not self.stinfo.get('possibletypes'):
- self.stinfo['possibletypes'] = old.stinfo.get('possibletypes')
-
- def as_string(self, encoding=None, kwargs=None):
- """return the tree as an encoded rql string"""
- return self.name
-
- def register_reference(self, vref):
- """add a reference to this variable"""
- self.stinfo['references'].add(vref)
-
- def unregister_reference(self, vref):
- """remove a reference to this variable"""
- try:
- self.stinfo['references'].remove(vref)
- except KeyError:
- # this may occur on hairy undoing
- pass
-
- def references(self):
- """return all references on this variable"""
- return tuple(self.stinfo['references'])
-
- def prepare_annotation(self):
- self.stinfo.update({
- 'scope': None,
- # relations where this variable is used on the lhs/rhs
- 'relations': set(),
- 'rhsrelations': set(),
- # selection indexes if any
- 'selected': set(),
- # type restriction (e.g. "is" / "is_instance_of") where this
- # variable is used on the lhs
- 'typerel': None,
- # uid relations (e.g. "eid") where this variable is used on the lhs
- 'uidrel': None,
- # if this variable is an attribute variable (ie final entity), link
- # to the (prefered) attribute owner variable
- 'attrvar': None,
- # constant node linked to an uid variable if any
- 'constnode': None,
- })
- # remove optional st infos
- for key in ('optrelations', 'blocsimplification', 'ftirels'):
- self.stinfo.pop(key, None)
-
- def _set_scope(self, key, scopenode):
- if scopenode is self.stmt or self.stinfo[key] is None:
- self.stinfo[key] = scopenode
- elif not (self.stinfo[key] is self.stmt or scopenode is self.stinfo[key]):
- self.stinfo[key] = common_parent(self.stinfo[key], scopenode).scope
-
- def set_scope(self, scopenode):
- self._set_scope('scope', scopenode)
- def get_scope(self):
- return self.stinfo['scope']
- scope = property(get_scope, set_scope)
-
- def add_optional_relation(self, relation):
- try:
- self.stinfo['optrelations'].add(relation)
- except KeyError:
- self.stinfo['optrelations'] = set((relation,))
-
- def get_type(self, solution=None, kwargs=None):
- """return entity type of this object, 'Any' if not found"""
- if solution:
- return solution[self.name]
- if self.stinfo['typerel']:
- rhs = self.stinfo['typerel'].children[1].children[0]
- if isinstance(rhs, Constant):
- return str(rhs.value)
- schema = self.schema
- if schema is not None:
- for rel in self.stinfo['rhsrelations']:
- try:
- lhstype = rel.children[0].get_type(solution, kwargs)
- return schema.eschema(lhstype).destination(rel.r_type)
- except: # CoertionError, AssertionError :(
- pass
- return 'Any'
-
- def get_description(self, mainindex, tr, none_allowed=False):
- """return :
- * the name of a relation where this variable is used as lhs,
- * the entity type of this object if specified by a 'is' relation,
- * 'Any' if nothing nicer has been found...
-
- give priority to relation name
- """
- if mainindex is not None:
- if mainindex in self.stinfo['selected']:
- return ', '.join(sorted(
- tr(etype) for etype in self.stinfo['possibletypes']))
- rtype = frtype = None
- schema = self.schema
- for rel in self.stinfo['relations']:
- if schema is not None:
- rschema = schema.rschema(rel.r_type)
- if rschema.final:
- if self.name == rel.children[0].name:
- # ignore final relation where this variable is used as subject
- continue
- # final relation where this variable is used as object
- frtype = rel.r_type
- rtype = rel.r_type
- lhs, rhs = rel.get_variable_parts()
- # use getattr, may not be a variable ref (rewritten, constant...)
- rhsvar = getattr(rhs, 'variable', None)
- if mainindex is not None:
- # relation to the main variable, stop searching
- lhsvar = getattr(lhs, 'variable', None)
- context = None
- if lhsvar is not None and mainindex in lhsvar.stinfo['selected']:
- if len(lhsvar.stinfo['possibletypes']) == 1:
- context = next(iter(lhsvar.stinfo['possibletypes']))
- return tr(rtype, context=context)
- if rhsvar is not None and mainindex in rhsvar.stinfo['selected']:
- if len(rhsvar.stinfo['possibletypes']) == 1:
- context = next(iter(rhsvar.stinfo['possibletypes']))
- if schema is not None and rschema.symmetric:
- return tr(rtype, context=context)
- return tr(rtype + '_object', context=context)
- if rhsvar is self:
- rtype += '_object'
- if frtype is not None:
- return tr(frtype)
- if mainindex is None and rtype is not None:
- return tr(rtype)
- if none_allowed:
- return None
- return ', '.join(sorted(
- tr(etype) for etype in self.stinfo['possibletypes']))
-
- def selected_index(self):
- """return the index of this variable in the selection if it's selected,
- else None
- """
- if not self.stinfo['selected']:
- return None
- return next(iter(self.stinfo['selected']))
-
- def main_relation(self):
- """Return the relation where this variable is used in the rhs.
-
- It is useful for cases where this variable is final and we are
- looking for the entity to which it belongs.
- """
- for ref in self.references():
- rel = ref.relation()
- if rel is None:
- continue
- if rel.r_type != 'is' and self.name != rel.children[0].name:
- return rel
- return None
-
- def valuable_references(self):
- """return the number of "valuable" references :
- references is in selection or in a non type (is) relations
- """
- stinfo = self.stinfo
- return len(stinfo['selected']) + len(stinfo['relations'])
-
-
-class ColumnAlias(Referenceable):
- __slots__ = ('colnum', 'query',
- '_q_sql', '_q_sqltable') # XXX cubicweb specific
- def __init__(self, alias, colnum, query=None):
- super(ColumnAlias, self).__init__(alias)
- self.colnum = int(colnum)
- self.query = query
-
- def __repr__(self):
- return 'alias %s' % self.name
-
- def get_type(self, solution=None, kwargs=None):
- """return entity type of this object, 'Any' if not found"""
- vtype = super(ColumnAlias, self).get_type(solution, kwargs)
- if vtype == 'Any':
- for select in self.query.children:
- vtype = select.selection[self.colnum].get_type(solution, kwargs)
- if vtype != 'Any':
- return vtype
- return vtype
-
- def get_description(self, mainindex, tr):
- """return entity type of this object, 'Any' if not found"""
- vtype = super(ColumnAlias, self).get_description(mainindex, tr,
- none_allowed=True)
- if vtype is None:
- vtypes = set()
- for select in self.query.children:
- vtype = select.selection[self.colnum].get_description(mainindex, tr)
- if vtype is not None:
- vtypes.add(vtype)
- if vtypes:
- return ', '.join(sorted(vtype for vtype in vtypes))
- return vtype
-
-
-class Variable(Referenceable):
- """
- a variable definition, should not be directly added to the syntax tree (use
- VariableRef instead)
-
- collects information about a variable use in a syntax tree
- """
- __slots__ = ('_q_invariant', '_q_sql', '_q_sqltable') # XXX ginco specific
-
- def __repr__(self):
- return '%s' % self.name
-
-
--- a/parser.g Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,362 +0,0 @@
-"""yapps input grammar for RQL.
-
-:organization: Logilab
-:copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-
-
-Select statement grammar
-------------------------
-
-query = <squery> | <union>
-
-union = (<squery>) UNION (<squery>) [UNION (<squery>)]*
-
-squery = Any <selection>
- [GROUPBY <variables>]
- [ORDERBY <sortterms>]
- [LIMIT <nb> OFFSET <nb>]
- [WHERE <restriction>]
- [HAVING <aggregat restriction>]
- [WITH <subquery> [,<subquery]*]
-
-subquery = <variables> BEING (<query>)
-
-variables = <variable> [, <variable>]*
-
-
-Abbreviations in this code
---------------------------
-
-rules:
-* rel -> relation
-* decl -> declaration
-* expr -> expression
-* restr -> restriction
-* var -> variable
-* func -> function
-* const -> constant
-* cmp -> comparison
-
-variables:
-* R -> syntax tree root
-* S -> select node
-* P -> parent node
-
-"""
-%%
-
-parser Hercule:
-
- ignore: r'\s+'
- # C-like comments
- ignore: r'/\*(?:[^*]|\*(?!/))*\*/'
-
- token DELETE: r'(?i)DELETE'
- token SET: r'(?i)SET'
- token INSERT: r'(?i)INSERT'
- token UNION: r'(?i)UNION'
- token DISTINCT: r'(?i)DISTINCT'
- token WITH: r'(?i)WITH'
- token WHERE: r'(?i)WHERE'
- token BEING: r'(?i)BEING'
- token OR: r'(?i)OR'
- token AND: r'(?i)AND'
- token NOT: r'(?i)NOT'
- token GROUPBY: r'(?i)GROUPBY'
- token HAVING: r'(?i)HAVING'
- token ORDERBY: r'(?i)ORDERBY'
- token SORT_ASC: r'(?i)ASC'
- token SORT_DESC: r'(?i)DESC'
- token LIMIT: r'(?i)LIMIT'
- token OFFSET: r'(?i)OFFSET'
- token DATE: r'(?i)TODAY'
- token DATETIME: r'(?i)NOW'
- token TRUE: r'(?i)TRUE'
- token FALSE: r'(?i)FALSE'
- token NULL: r'(?i)NULL'
- token EXISTS: r'(?i)EXISTS'
- token CMP_OP: r'(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP'
- token ADD_OP: r'\+|-|\||#'
- token MUL_OP: r'\*|/|%|&'
- token POW_OP: r'\^|>>|<<'
- token UNARY_OP: r'-|~'
- token FUNCTION: r'[A-Za-z_]+\s*(?=\()'
- token R_TYPE: r'[a-z_][a-z0-9_]*'
- token E_TYPE: r'[A-Z][A-Za-z0-9]*[a-z]+[A-Z0-9]*'
- token VARIABLE: r'[A-Z][A-Z0-9_]*'
- token COLALIAS: r'[A-Z][A-Z0-9_]*\.\d+'
- token QMARK: r'\?'
-
- token STRING: r"'([^\'\\]|\\.)*'|\"([^\\\"\\]|\\.)*\""
- token FLOAT: r'-?\d+\.\d*'
- token INT: r'-?\d+'
- token SUBSTITUTE: r'%\([A-Za-z_0-9]+\)s'
-
-
-#// Grammar entry ###############################################################
-
-
-rule goal: DELETE _delete<<Delete()>> ';' {{ return _delete }}
-
- | INSERT _insert<<Insert()>> ';' {{ return _insert }}
-
- | SET update<<Set()>> ';' {{ return update }}
-
- | union<<Union()>> ';' {{ return union }}
-
-#// Deletion ###################################################################
-
-rule _delete<<R>>: decl_rels<<R>> where<<R>> having<<R>> {{ return R }}
-
- | decl_vars<<R>> where<<R>> having<<R>> {{ return R }}
-
-
-#// Insertion ##################################################################
-
-rule _insert<<R>>: decl_vars<<R>> insert_rels<<R>> {{ return R }}
-
-
-rule insert_rels<<R>>: ":" decl_rels<<R>> where<<R>> having<<R>> {{ return R }}
-
- |
-
-
-#// Update #####################################################################
-
-rule update<<R>>: decl_rels<<R>> where<<R>> having<<R>> {{ return R }}
-
-
-#// Selection ##################################################################
-
-rule union<<R>>: select<<Select()>> {{ R.append(select); return R }}
-
- | r"\(" select<<Select()>> r"\)" {{ R.append(select) }}
- ( UNION
- r"\(" select<<Select()>> r"\)" {{ R.append(select) }}
- )* {{ return R }}
-
-rule select<<S>>: DISTINCT select_<<S>> {{ S.distinct = True ; return S }}
- | select_<<S>> {{ return S }}
-
-rule select_<<S>>: E_TYPE selection<<S>>
- groupby<<S>>
- orderby<<S>>
- limit_offset<<S>>
- where<<S>>
- having<<S>>
- with_<<S>> {{ S.set_statement_type(E_TYPE); return S }}
-
-rule selection<<S>>: expr_add<<S>> {{ S.append_selected(expr_add) }}
- ( ',' expr_add<<S>> {{ S.append_selected(expr_add) }}
- )*
-
-
-
-#// other clauses (groupby, orderby, with, having) ##############################
-
-rule groupby<<S>>: GROUPBY {{ nodes = [] }}
- expr_add<<S>> {{ nodes.append(expr_add) }}
- ( ',' expr_add<<S>> {{ nodes.append(expr_add) }}
- )* {{ S.set_groupby(nodes); return True }}
- |
-
-rule having<<S>>: HAVING logical_expr<<S>> {{ S.set_having([logical_expr]) }}
- |
-
-rule orderby<<S>>: ORDERBY {{ nodes = [] }}
- sort_term<<S>> {{ nodes.append(sort_term) }}
- ( ',' sort_term<<S>> {{ nodes.append(sort_term) }}
- )* {{ S.set_orderby(nodes); return True }}
- |
-
-rule with_<<S>>: WITH {{ nodes = [] }}
- subquery<<S>> {{ nodes.append(subquery) }}
- ( ',' subquery<<S>> {{ nodes.append(subquery) }}
- )* {{ S.set_with(nodes) }}
- |
-
-rule subquery<<S>>: variables<<S>> {{ node = SubQuery() ; node.set_aliases(variables) }}
- BEING r"\(" union<<Union()>> r"\)" {{ node.set_query(union); return node }}
-
-
-rule sort_term<<S>>: expr_add<<S>> sort_meth {{ return SortTerm(expr_add, sort_meth) }}
-
-
-rule sort_meth: SORT_DESC {{ return 0 }}
-
- | SORT_ASC {{ return 1 }}
-
- | {{ return 1 # default to SORT_ASC }}
-
-
-#// Limit and offset ############################################################
-
-rule limit_offset<<R>> : limit<<R>> offset<<R>> {{ return limit or offset }}
-
-rule limit<<R>> : LIMIT INT {{ R.set_limit(int(INT)); return True }}
- |
-
-rule offset<<R>> : OFFSET INT {{ R.set_offset(int(INT)); return True }}
- |
-
-
-#// Restriction statements ######################################################
-
-rule where<<S>>: WHERE restriction<<S>> {{ S.set_where(restriction) }}
- |
-
-rule restriction<<S>>: rels_or<<S>> {{ node = rels_or }}
- ( ',' rels_or<<S>> {{ node = And(node, rels_or) }}
- )* {{ return node }}
-
-rule rels_or<<S>>: rels_and<<S>> {{ node = rels_and }}
- ( OR rels_and<<S>> {{ node = Or(node, rels_and) }}
- )* {{ return node }}
-
-rule rels_and<<S>>: rels_not<<S>> {{ node = rels_not }}
- ( AND rels_not<<S>> {{ node = And(node, rels_not) }}
- )* {{ return node }}
-
-rule rels_not<<S>>: NOT rel<<S>> {{ return Not(rel) }}
- | rel<<S>> {{ return rel }}
-
-rule rel<<S>>: rel_base<<S>> {{ return rel_base }}
- | r"\(" restriction<<S>> r"\)" {{ return restriction }}
-
-
-rule rel_base<<S>>: var<<S>> opt_left<<S>> rtype {{ rtype.append(var) ; rtype.set_optional(opt_left) }}
- expr<<S>> opt_right<<S>> {{ rtype.append(expr) ; rtype.set_optional(opt_right) ; return rtype }}
- | EXISTS r"\(" restriction<<S>> r"\)" {{ return Exists(restriction) }}
-
-rule rtype: R_TYPE {{ return Relation(R_TYPE) }}
-
-rule opt_left<<S>>: QMARK {{ return 'left' }}
- |
-rule opt_right<<S>>: QMARK {{ return 'right' }}
- |
-
-#// restriction expressions ####################################################
-
-rule logical_expr<<S>>: exprs_or<<S>> {{ node = exprs_or }}
- ( ',' exprs_or<<S>> {{ node = And(node, exprs_or) }}
- )* {{ return node }}
-
-rule exprs_or<<S>>: exprs_and<<S>> {{ node = exprs_and }}
- ( OR exprs_and<<S>> {{ node = Or(node, exprs_and) }}
- )* {{ return node }}
-
-rule exprs_and<<S>>: exprs_not<<S>> {{ node = exprs_not }}
- ( AND exprs_not<<S>> {{ node = And(node, exprs_not) }}
- )* {{ return node }}
-
-rule exprs_not<<S>>: NOT balanced_expr<<S>> {{ return Not(balanced_expr) }}
- | balanced_expr<<S>> {{ return balanced_expr }}
-
-#// XXX ambiguity, expr_add may also have '(' as first token. Hence
-#// put "(" logical_expr<<S>> ")" rule first. We can then parse:
-#//
-#// Any T2 WHERE T1 relation T2 HAVING (1 < COUNT(T1));
-#//
-#// but not
-#//
-#// Any T2 WHERE T1 relation T2 HAVING (1+2) < COUNT(T1);
-rule balanced_expr<<S>>: r"\(" logical_expr<<S>> r"\)" {{ return logical_expr }}
- | expr_add<<S>> opt_left<<S>>
- expr_op<<S>> opt_right<<S>> {{ expr_op.insert(0, expr_add); expr_op.set_optional(opt_left, opt_right); return expr_op }}
-
-# // cant use expr<<S>> without introducing some ambiguities
-rule expr_op<<S>>: CMP_OP expr_add<<S>> {{ return Comparison(CMP_OP.upper(), expr_add) }}
- | in_expr<<S>> {{ return Comparison('=', in_expr) }}
-
-
-#// common statements ###########################################################
-
-rule variables<<S>>: {{ vars = [] }}
- var<<S>> {{ vars.append(var) }}
- ( ',' var<<S>> {{ vars.append(var) }}
- )* {{ return vars }}
-
-rule decl_vars<<R>>: E_TYPE var<<R>> ( {{ R.add_main_variable(E_TYPE, var) }}
- ',' E_TYPE var<<R>>)* {{ R.add_main_variable(E_TYPE, var) }}
-
-
-rule decl_rels<<R>>: simple_rel<<R>> ( {{ R.add_main_relation(simple_rel) }}
- ',' simple_rel<<R>>)* {{ R.add_main_relation(simple_rel) }}
-
-
-rule simple_rel<<R>>: var<<R>> R_TYPE {{ e = Relation(R_TYPE) ; e.append(var) }}
- expr_add<<R>> {{ e.append(Comparison('=', expr_add)) ; return e }}
-
-
-rule expr<<S>>: CMP_OP expr_add<<S>> {{ return Comparison(CMP_OP.upper(), expr_add) }}
-
- | expr_add<<S>> {{ return Comparison('=', expr_add) }}
-
-
-rule expr_add<<S>>: expr_mul<<S>> {{ node = expr_mul }}
- ( ADD_OP expr_mul<<S>> {{ node = MathExpression( ADD_OP, node, expr_mul ) }}
- )* {{ return node }}
- | UNARY_OP expr_mul<<S>> {{ node = UnaryExpression( UNARY_OP, expr_mul ) }}
- ( ADD_OP expr_mul<<S>> {{ node = MathExpression( ADD_OP, node, expr_mul ) }}
- )* {{ return node }}
-
-
-rule expr_mul<<S>>: expr_pow<<S>> {{ node = expr_pow }}
- ( MUL_OP expr_pow<<S>> {{ node = MathExpression( MUL_OP, node, expr_pow) }}
- )* {{ return node }}
-
-rule expr_pow<<S>>: expr_base<<S>> {{ node = expr_base }}
- ( POW_OP expr_base<<S>> {{ node = MathExpression( MUL_OP, node, expr_base) }}
- )* {{ return node }}
-
-
-rule expr_base<<S>>: const {{ return const }}
- | var<<S>> {{ return var }}
- | etype<<S>> {{ return etype }}
- | func<<S>> {{ return func }}
- | r"\(" expr_add<<S>> r"\)" {{ return expr_add }}
-
-
-rule func<<S>>: FUNCTION r"\(" {{ F = Function(FUNCTION) }}
- ( expr_add<<S>> ( {{ F.append(expr_add) }}
- ',' expr_add<<S>>
- )* {{ F.append(expr_add) }}
- )?
- r"\)" {{ return F }}
-
-rule in_expr<<S>>: 'IN' r"\(" {{ F = Function('IN') }}
- ( expr_add<<S>> ( {{ F.append(expr_add) }}
- ',' expr_add<<S>>
- )* {{ F.append(expr_add) }}
- )?
- r"\)" {{ return F }}
-
-
-rule var<<S>>: VARIABLE {{ return VariableRef(S.get_variable(VARIABLE)) }}
-
-rule etype<<S>>: E_TYPE {{ return S.get_etype(E_TYPE) }}
-
-
-rule const: NULL {{ return Constant(None, None) }}
- | DATE {{ return Constant(DATE.upper(), 'Date') }}
- | DATETIME {{ return Constant(DATETIME.upper(), 'Datetime') }}
- | TRUE {{ return Constant(True, 'Boolean') }}
- | FALSE {{ return Constant(False, 'Boolean') }}
- | FLOAT {{ return Constant(float(FLOAT), 'Float') }}
- | INT {{ return Constant(int(INT), 'Int') }}
- | STRING {{ return Constant(unquote(STRING), 'String') }}
- | SUBSTITUTE {{ return Constant(SUBSTITUTE[2:-2], 'Substitute') }}
-%%
-
-from warnings import warn
-from rql.stmts import Union, Select, Delete, Insert, Set
-from rql.nodes import *
-
-
-def unquote(string):
- """Remove quotes from a string."""
- if string.startswith('"'):
- return string[1:-1].replace('\\\\', '\\').replace('\\"', '"')
- elif string.startswith("'"):
- return string[1:-1].replace('\\\\', '\\').replace("\\'", "'")
--- a/parser.py Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,708 +0,0 @@
-"""yapps input grammar for RQL.
-
-:organization: Logilab
-:copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-
-
-Select statement grammar
-------------------------
-
-query = <squery> | <union>
-
-union = (<squery>) UNION (<squery>) [UNION (<squery>)]*
-
-squery = Any <selection>
- [GROUPBY <variables>]
- [ORDERBY <sortterms>]
- [LIMIT <nb> OFFSET <nb>]
- [WHERE <restriction>]
- [HAVING <aggregat restriction>]
- [WITH <subquery> [,<subquery]*]
-
-subquery = <variables> BEING (<query>)
-
-variables = <variable> [, <variable>]*
-
-
-Abbreviations in this code
---------------------------
-
-rules:
-* rel -> relation
-* decl -> declaration
-* expr -> expression
-* restr -> restriction
-* var -> variable
-* func -> function
-* const -> constant
-* cmp -> comparison
-
-variables:
-* R -> syntax tree root
-* S -> select node
-* P -> parent node
-
-"""
-
-# Begin -- grammar generated by Yapps
-from __future__ import print_function
-import sys, re
-from yapps import runtime
-
-class HerculeScanner(runtime.Scanner):
- patterns = [
- ("'IN'", re.compile('IN')),
- ("','", re.compile(',')),
- ('r"\\)"', re.compile('\\)')),
- ('r"\\("', re.compile('\\(')),
- ('":"', re.compile(':')),
- ("';'", re.compile(';')),
- ('\\s+', re.compile('\\s+')),
- ('/\\*(?:[^*]|\\*(?!/))*\\*/', re.compile('/\\*(?:[^*]|\\*(?!/))*\\*/')),
- ('DELETE', re.compile('(?i)DELETE')),
- ('SET', re.compile('(?i)SET')),
- ('INSERT', re.compile('(?i)INSERT')),
- ('UNION', re.compile('(?i)UNION')),
- ('DISTINCT', re.compile('(?i)DISTINCT')),
- ('WITH', re.compile('(?i)WITH')),
- ('WHERE', re.compile('(?i)WHERE')),
- ('BEING', re.compile('(?i)BEING')),
- ('OR', re.compile('(?i)OR')),
- ('AND', re.compile('(?i)AND')),
- ('NOT', re.compile('(?i)NOT')),
- ('GROUPBY', re.compile('(?i)GROUPBY')),
- ('HAVING', re.compile('(?i)HAVING')),
- ('ORDERBY', re.compile('(?i)ORDERBY')),
- ('SORT_ASC', re.compile('(?i)ASC')),
- ('SORT_DESC', re.compile('(?i)DESC')),
- ('LIMIT', re.compile('(?i)LIMIT')),
- ('OFFSET', re.compile('(?i)OFFSET')),
- ('DATE', re.compile('(?i)TODAY')),
- ('DATETIME', re.compile('(?i)NOW')),
- ('TRUE', re.compile('(?i)TRUE')),
- ('FALSE', re.compile('(?i)FALSE')),
- ('NULL', re.compile('(?i)NULL')),
- ('EXISTS', re.compile('(?i)EXISTS')),
- ('CMP_OP', re.compile('(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP')),
- ('ADD_OP', re.compile('\\+|-|\\||#')),
- ('MUL_OP', re.compile('\\*|/|%|&')),
- ('POW_OP', re.compile('\\^|>>|<<')),
- ('UNARY_OP', re.compile('-|~')),
- ('FUNCTION', re.compile('[A-Za-z_]+\\s*(?=\\()')),
- ('R_TYPE', re.compile('[a-z_][a-z0-9_]*')),
- ('E_TYPE', re.compile('[A-Z][A-Za-z0-9]*[a-z]+[A-Z0-9]*')),
- ('VARIABLE', re.compile('[A-Z][A-Z0-9_]*')),
- ('COLALIAS', re.compile('[A-Z][A-Z0-9_]*\\.\\d+')),
- ('QMARK', re.compile('\\?')),
- ('STRING', re.compile('\'([^\\\'\\\\]|\\\\.)*\'|\\"([^\\\\\\"\\\\]|\\\\.)*\\"')),
- ('FLOAT', re.compile('-?\\d+\\.\\d*')),
- ('INT', re.compile('-?\\d+')),
- ('SUBSTITUTE', re.compile('%\\([A-Za-z_0-9]+\\)s')),
- ]
- def __init__(self, str,*args,**kw):
- runtime.Scanner.__init__(self,None,{'\\s+':None,'/\\*(?:[^*]|\\*(?!/))*\\*/':None,},str,*args,**kw)
-
-class Hercule(runtime.Parser):
- Context = runtime.Context
- def goal(self, _parent=None):
- _context = self.Context(_parent, self._scanner, 'goal', [])
- _token = self._peek('DELETE', 'INSERT', 'SET', 'r"\\("', 'DISTINCT', 'E_TYPE', context=_context)
- if _token == 'DELETE':
- DELETE = self._scan('DELETE', context=_context)
- _delete = self._delete(Delete(), _context)
- self._scan("';'", context=_context)
- return _delete
- elif _token == 'INSERT':
- INSERT = self._scan('INSERT', context=_context)
- _insert = self._insert(Insert(), _context)
- self._scan("';'", context=_context)
- return _insert
- elif _token == 'SET':
- SET = self._scan('SET', context=_context)
- update = self.update(Set(), _context)
- self._scan("';'", context=_context)
- return update
- else: # in ['r"\\("', 'DISTINCT', 'E_TYPE']
- union = self.union(Union(), _context)
- self._scan("';'", context=_context)
- return union
-
- def _delete(self, R, _parent=None):
- _context = self.Context(_parent, self._scanner, '_delete', [R])
- _token = self._peek('E_TYPE', 'VARIABLE', context=_context)
- if _token == 'VARIABLE':
- decl_rels = self.decl_rels(R, _context)
- where = self.where(R, _context)
- having = self.having(R, _context)
- return R
- else: # == 'E_TYPE'
- decl_vars = self.decl_vars(R, _context)
- where = self.where(R, _context)
- having = self.having(R, _context)
- return R
-
- def _insert(self, R, _parent=None):
- _context = self.Context(_parent, self._scanner, '_insert', [R])
- decl_vars = self.decl_vars(R, _context)
- insert_rels = self.insert_rels(R, _context)
- return R
-
- def insert_rels(self, R, _parent=None):
- _context = self.Context(_parent, self._scanner, 'insert_rels', [R])
- _token = self._peek('":"', "';'", context=_context)
- if _token == '":"':
- self._scan('":"', context=_context)
- decl_rels = self.decl_rels(R, _context)
- where = self.where(R, _context)
- having = self.having(R, _context)
- return R
- else: # == "';'"
- pass
-
- def update(self, R, _parent=None):
- _context = self.Context(_parent, self._scanner, 'update', [R])
- decl_rels = self.decl_rels(R, _context)
- where = self.where(R, _context)
- having = self.having(R, _context)
- return R
-
- def union(self, R, _parent=None):
- _context = self.Context(_parent, self._scanner, 'union', [R])
- _token = self._peek('r"\\("', 'DISTINCT', 'E_TYPE', context=_context)
- if _token != 'r"\\("':
- select = self.select(Select(), _context)
- R.append(select); return R
- else: # == 'r"\\("'
- self._scan('r"\\("', context=_context)
- select = self.select(Select(), _context)
- self._scan('r"\\)"', context=_context)
- R.append(select)
- while self._peek('UNION', "';'", 'r"\\)"', context=_context) == 'UNION':
- UNION = self._scan('UNION', context=_context)
- self._scan('r"\\("', context=_context)
- select = self.select(Select(), _context)
- self._scan('r"\\)"', context=_context)
- R.append(select)
- return R
-
- def select(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'select', [S])
- _token = self._peek('DISTINCT', 'E_TYPE', context=_context)
- if _token == 'DISTINCT':
- DISTINCT = self._scan('DISTINCT', context=_context)
- select_ = self.select_(S, _context)
- S.distinct = True ; return S
- else: # == 'E_TYPE'
- select_ = self.select_(S, _context)
- return S
-
- def select_(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'select_', [S])
- E_TYPE = self._scan('E_TYPE', context=_context)
- selection = self.selection(S, _context)
- groupby = self.groupby(S, _context)
- orderby = self.orderby(S, _context)
- limit_offset = self.limit_offset(S, _context)
- where = self.where(S, _context)
- having = self.having(S, _context)
- with_ = self.with_(S, _context)
- S.set_statement_type(E_TYPE); return S
-
- def selection(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'selection', [S])
- expr_add = self.expr_add(S, _context)
- S.append_selected(expr_add)
- while self._peek("','", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
- self._scan("','", context=_context)
- expr_add = self.expr_add(S, _context)
- S.append_selected(expr_add)
-
- def groupby(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'groupby', [S])
- _token = self._peek('GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
- if _token == 'GROUPBY':
- GROUPBY = self._scan('GROUPBY', context=_context)
- nodes = []
- expr_add = self.expr_add(S, _context)
- nodes.append(expr_add)
- while self._peek("','", 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
- self._scan("','", context=_context)
- expr_add = self.expr_add(S, _context)
- nodes.append(expr_add)
- S.set_groupby(nodes); return True
- else:
- pass
-
- def having(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'having', [S])
- _token = self._peek('HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
- if _token == 'HAVING':
- HAVING = self._scan('HAVING', context=_context)
- logical_expr = self.logical_expr(S, _context)
- S.set_having([logical_expr])
- else: # in ['WITH', "';'", 'r"\\)"']
- pass
-
- def orderby(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'orderby', [S])
- _token = self._peek('ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
- if _token == 'ORDERBY':
- ORDERBY = self._scan('ORDERBY', context=_context)
- nodes = []
- sort_term = self.sort_term(S, _context)
- nodes.append(sort_term)
- while self._peek("','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
- self._scan("','", context=_context)
- sort_term = self.sort_term(S, _context)
- nodes.append(sort_term)
- S.set_orderby(nodes); return True
- else:
- pass
-
- def with_(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'with_', [S])
- _token = self._peek('WITH', 'r"\\)"', "';'", context=_context)
- if _token == 'WITH':
- WITH = self._scan('WITH', context=_context)
- nodes = []
- subquery = self.subquery(S, _context)
- nodes.append(subquery)
- while self._peek("','", 'r"\\)"', "';'", context=_context) == "','":
- self._scan("','", context=_context)
- subquery = self.subquery(S, _context)
- nodes.append(subquery)
- S.set_with(nodes)
- else: # in ['r"\\)"', "';'"]
- pass
-
- def subquery(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'subquery', [S])
- variables = self.variables(S, _context)
- node = SubQuery() ; node.set_aliases(variables)
- BEING = self._scan('BEING', context=_context)
- self._scan('r"\\("', context=_context)
- union = self.union(Union(), _context)
- self._scan('r"\\)"', context=_context)
- node.set_query(union); return node
-
- def sort_term(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'sort_term', [S])
- expr_add = self.expr_add(S, _context)
- sort_meth = self.sort_meth(_context)
- return SortTerm(expr_add, sort_meth)
-
- def sort_meth(self, _parent=None):
- _context = self.Context(_parent, self._scanner, 'sort_meth', [])
- _token = self._peek('SORT_DESC', 'SORT_ASC', "','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
- if _token == 'SORT_DESC':
- SORT_DESC = self._scan('SORT_DESC', context=_context)
- return 0
- elif _token == 'SORT_ASC':
- SORT_ASC = self._scan('SORT_ASC', context=_context)
- return 1
- else:
- return 1 # default to SORT_ASC
-
- def limit_offset(self, R, _parent=None):
- _context = self.Context(_parent, self._scanner, 'limit_offset', [R])
- limit = self.limit(R, _context)
- offset = self.offset(R, _context)
- return limit or offset
-
- def limit(self, R, _parent=None):
- _context = self.Context(_parent, self._scanner, 'limit', [R])
- _token = self._peek('LIMIT', 'OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
- if _token == 'LIMIT':
- LIMIT = self._scan('LIMIT', context=_context)
- INT = self._scan('INT', context=_context)
- R.set_limit(int(INT)); return True
- else: # in ['OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"']
- pass
-
- def offset(self, R, _parent=None):
- _context = self.Context(_parent, self._scanner, 'offset', [R])
- _token = self._peek('OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
- if _token == 'OFFSET':
- OFFSET = self._scan('OFFSET', context=_context)
- INT = self._scan('INT', context=_context)
- R.set_offset(int(INT)); return True
- else: # in ['WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"']
- pass
-
- def where(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'where', [S])
- _token = self._peek('WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
- if _token == 'WHERE':
- WHERE = self._scan('WHERE', context=_context)
- restriction = self.restriction(S, _context)
- S.set_where(restriction)
- else: # in ['HAVING', 'WITH', "';'", 'r"\\)"']
- pass
-
- def restriction(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'restriction', [S])
- rels_or = self.rels_or(S, _context)
- node = rels_or
- while self._peek("','", 'r"\\)"', 'HAVING', 'WITH', "';'", context=_context) == "','":
- self._scan("','", context=_context)
- rels_or = self.rels_or(S, _context)
- node = And(node, rels_or)
- return node
-
- def rels_or(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'rels_or', [S])
- rels_and = self.rels_and(S, _context)
- node = rels_and
- while self._peek('OR', "','", 'r"\\)"', 'HAVING', 'WITH', "';'", context=_context) == 'OR':
- OR = self._scan('OR', context=_context)
- rels_and = self.rels_and(S, _context)
- node = Or(node, rels_and)
- return node
-
- def rels_and(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'rels_and', [S])
- rels_not = self.rels_not(S, _context)
- node = rels_not
- while self._peek('AND', 'OR', "','", 'r"\\)"', 'HAVING', 'WITH', "';'", context=_context) == 'AND':
- AND = self._scan('AND', context=_context)
- rels_not = self.rels_not(S, _context)
- node = And(node, rels_not)
- return node
-
- def rels_not(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'rels_not', [S])
- _token = self._peek('NOT', 'r"\\("', 'EXISTS', 'VARIABLE', context=_context)
- if _token == 'NOT':
- NOT = self._scan('NOT', context=_context)
- rel = self.rel(S, _context)
- return Not(rel)
- else: # in ['r"\\("', 'EXISTS', 'VARIABLE']
- rel = self.rel(S, _context)
- return rel
-
- def rel(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'rel', [S])
- _token = self._peek('r"\\("', 'EXISTS', 'VARIABLE', context=_context)
- if _token != 'r"\\("':
- rel_base = self.rel_base(S, _context)
- return rel_base
- else: # == 'r"\\("'
- self._scan('r"\\("', context=_context)
- restriction = self.restriction(S, _context)
- self._scan('r"\\)"', context=_context)
- return restriction
-
- def rel_base(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'rel_base', [S])
- _token = self._peek('EXISTS', 'VARIABLE', context=_context)
- if _token == 'VARIABLE':
- var = self.var(S, _context)
- opt_left = self.opt_left(S, _context)
- rtype = self.rtype(_context)
- rtype.append(var) ; rtype.set_optional(opt_left)
- expr = self.expr(S, _context)
- opt_right = self.opt_right(S, _context)
- rtype.append(expr) ; rtype.set_optional(opt_right) ; return rtype
- else: # == 'EXISTS'
- EXISTS = self._scan('EXISTS', context=_context)
- self._scan('r"\\("', context=_context)
- restriction = self.restriction(S, _context)
- self._scan('r"\\)"', context=_context)
- return Exists(restriction)
-
- def rtype(self, _parent=None):
- _context = self.Context(_parent, self._scanner, 'rtype', [])
- R_TYPE = self._scan('R_TYPE', context=_context)
- return Relation(R_TYPE)
-
- def opt_left(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'opt_left', [S])
- _token = self._peek('QMARK', 'R_TYPE', 'CMP_OP', "'IN'", context=_context)
- if _token == 'QMARK':
- QMARK = self._scan('QMARK', context=_context)
- return 'left'
- else: # in ['R_TYPE', 'CMP_OP', "'IN'"]
- pass
-
- def opt_right(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'opt_right', [S])
- _token = self._peek('QMARK', 'AND', 'OR', "','", 'r"\\)"', 'WITH', "';'", 'HAVING', context=_context)
- if _token == 'QMARK':
- QMARK = self._scan('QMARK', context=_context)
- return 'right'
- else: # in ['AND', 'OR', "','", 'r"\\)"', 'WITH', "';'", 'HAVING']
- pass
-
- def logical_expr(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'logical_expr', [S])
- exprs_or = self.exprs_or(S, _context)
- node = exprs_or
- while self._peek("','", 'r"\\)"', 'WITH', "';'", context=_context) == "','":
- self._scan("','", context=_context)
- exprs_or = self.exprs_or(S, _context)
- node = And(node, exprs_or)
- return node
-
- def exprs_or(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'exprs_or', [S])
- exprs_and = self.exprs_and(S, _context)
- node = exprs_and
- while self._peek('OR', "','", 'r"\\)"', 'WITH', "';'", context=_context) == 'OR':
- OR = self._scan('OR', context=_context)
- exprs_and = self.exprs_and(S, _context)
- node = Or(node, exprs_and)
- return node
-
- def exprs_and(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'exprs_and', [S])
- exprs_not = self.exprs_not(S, _context)
- node = exprs_not
- while self._peek('AND', 'OR', "','", 'r"\\)"', 'WITH', "';'", context=_context) == 'AND':
- AND = self._scan('AND', context=_context)
- exprs_not = self.exprs_not(S, _context)
- node = And(node, exprs_not)
- return node
-
- def exprs_not(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'exprs_not', [S])
- _token = self._peek('NOT', 'r"\\("', 'UNARY_OP', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
- if _token == 'NOT':
- NOT = self._scan('NOT', context=_context)
- balanced_expr = self.balanced_expr(S, _context)
- return Not(balanced_expr)
- else:
- balanced_expr = self.balanced_expr(S, _context)
- return balanced_expr
-
- def balanced_expr(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'balanced_expr', [S])
- _token = self._peek('r"\\("', 'UNARY_OP', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
- if _token == 'r"\\("':
- self._scan('r"\\("', context=_context)
- logical_expr = self.logical_expr(S, _context)
- self._scan('r"\\)"', context=_context)
- return logical_expr
- elif 1:
- expr_add = self.expr_add(S, _context)
- opt_left = self.opt_left(S, _context)
- expr_op = self.expr_op(S, _context)
- opt_right = self.opt_right(S, _context)
- expr_op.insert(0, expr_add); expr_op.set_optional(opt_left, opt_right); return expr_op
- else:
- raise runtime.SyntaxError(_token[0], 'Could not match balanced_expr')
-
- def expr_op(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'expr_op', [S])
- _token = self._peek('CMP_OP', "'IN'", context=_context)
- if _token == 'CMP_OP':
- CMP_OP = self._scan('CMP_OP', context=_context)
- expr_add = self.expr_add(S, _context)
- return Comparison(CMP_OP.upper(), expr_add)
- else: # == "'IN'"
- in_expr = self.in_expr(S, _context)
- return Comparison('=', in_expr)
-
- def variables(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'variables', [S])
- vars = []
- var = self.var(S, _context)
- vars.append(var)
- while self._peek("','", 'BEING', context=_context) == "','":
- self._scan("','", context=_context)
- var = self.var(S, _context)
- vars.append(var)
- return vars
-
- def decl_vars(self, R, _parent=None):
- _context = self.Context(_parent, self._scanner, 'decl_vars', [R])
- E_TYPE = self._scan('E_TYPE', context=_context)
- var = self.var(R, _context)
- while self._peek("','", 'R_TYPE', 'QMARK', 'WHERE', '":"', 'CMP_OP', 'HAVING', "'IN'", "';'", 'POW_OP', 'BEING', 'WITH', 'MUL_OP', 'r"\\)"', 'ADD_OP', 'SORT_DESC', 'SORT_ASC', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'AND', 'OR', context=_context) == "','":
- R.add_main_variable(E_TYPE, var)
- self._scan("','", context=_context)
- E_TYPE = self._scan('E_TYPE', context=_context)
- var = self.var(R, _context)
- R.add_main_variable(E_TYPE, var)
-
- def decl_rels(self, R, _parent=None):
- _context = self.Context(_parent, self._scanner, 'decl_rels', [R])
- simple_rel = self.simple_rel(R, _context)
- while self._peek("','", 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
- R.add_main_relation(simple_rel)
- self._scan("','", context=_context)
- simple_rel = self.simple_rel(R, _context)
- R.add_main_relation(simple_rel)
-
- def simple_rel(self, R, _parent=None):
- _context = self.Context(_parent, self._scanner, 'simple_rel', [R])
- var = self.var(R, _context)
- R_TYPE = self._scan('R_TYPE', context=_context)
- e = Relation(R_TYPE) ; e.append(var)
- expr_add = self.expr_add(R, _context)
- e.append(Comparison('=', expr_add)) ; return e
-
- def expr(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'expr', [S])
- _token = self._peek('CMP_OP', 'UNARY_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
- if _token == 'CMP_OP':
- CMP_OP = self._scan('CMP_OP', context=_context)
- expr_add = self.expr_add(S, _context)
- return Comparison(CMP_OP.upper(), expr_add)
- else:
- expr_add = self.expr_add(S, _context)
- return Comparison('=', expr_add)
-
- def expr_add(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'expr_add', [S])
- _token = self._peek('UNARY_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
- if _token != 'UNARY_OP':
- expr_mul = self.expr_mul(S, _context)
- node = expr_mul
- while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'ADD_OP':
- ADD_OP = self._scan('ADD_OP', context=_context)
- expr_mul = self.expr_mul(S, _context)
- node = MathExpression( ADD_OP, node, expr_mul )
- return node
- else: # == 'UNARY_OP'
- UNARY_OP = self._scan('UNARY_OP', context=_context)
- expr_mul = self.expr_mul(S, _context)
- node = UnaryExpression( UNARY_OP, expr_mul )
- while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'ADD_OP':
- ADD_OP = self._scan('ADD_OP', context=_context)
- expr_mul = self.expr_mul(S, _context)
- node = MathExpression( ADD_OP, node, expr_mul )
- return node
-
- def expr_mul(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'expr_mul', [S])
- expr_pow = self.expr_pow(S, _context)
- node = expr_pow
- while self._peek('MUL_OP', 'ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'MUL_OP':
- MUL_OP = self._scan('MUL_OP', context=_context)
- expr_pow = self.expr_pow(S, _context)
- node = MathExpression( MUL_OP, node, expr_pow)
- return node
-
- def expr_pow(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'expr_pow', [S])
- expr_base = self.expr_base(S, _context)
- node = expr_base
- while self._peek('POW_OP', 'MUL_OP', 'ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'POW_OP':
- POW_OP = self._scan('POW_OP', context=_context)
- expr_base = self.expr_base(S, _context)
- node = MathExpression( MUL_OP, node, expr_base)
- return node
-
- def expr_base(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'expr_base', [S])
- _token = self._peek('r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
- if _token not in ['r"\\("', 'VARIABLE', 'E_TYPE', 'FUNCTION']:
- const = self.const(_context)
- return const
- elif _token == 'VARIABLE':
- var = self.var(S, _context)
- return var
- elif _token == 'E_TYPE':
- etype = self.etype(S, _context)
- return etype
- elif _token == 'FUNCTION':
- func = self.func(S, _context)
- return func
- else: # == 'r"\\("'
- self._scan('r"\\("', context=_context)
- expr_add = self.expr_add(S, _context)
- self._scan('r"\\)"', context=_context)
- return expr_add
-
- def func(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'func', [S])
- FUNCTION = self._scan('FUNCTION', context=_context)
- self._scan('r"\\("', context=_context)
- F = Function(FUNCTION)
- if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
- expr_add = self.expr_add(S, _context)
- while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == "','":
- F.append(expr_add)
- self._scan("','", context=_context)
- expr_add = self.expr_add(S, _context)
- F.append(expr_add)
- self._scan('r"\\)"', context=_context)
- return F
-
- def in_expr(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'in_expr', [S])
- self._scan("'IN'", context=_context)
- self._scan('r"\\("', context=_context)
- F = Function('IN')
- if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
- expr_add = self.expr_add(S, _context)
- while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == "','":
- F.append(expr_add)
- self._scan("','", context=_context)
- expr_add = self.expr_add(S, _context)
- F.append(expr_add)
- self._scan('r"\\)"', context=_context)
- return F
-
- def var(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'var', [S])
- VARIABLE = self._scan('VARIABLE', context=_context)
- return VariableRef(S.get_variable(VARIABLE))
-
- def etype(self, S, _parent=None):
- _context = self.Context(_parent, self._scanner, 'etype', [S])
- E_TYPE = self._scan('E_TYPE', context=_context)
- return S.get_etype(E_TYPE)
-
- def const(self, _parent=None):
- _context = self.Context(_parent, self._scanner, 'const', [])
- _token = self._peek('NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', context=_context)
- if _token == 'NULL':
- NULL = self._scan('NULL', context=_context)
- return Constant(None, None)
- elif _token == 'DATE':
- DATE = self._scan('DATE', context=_context)
- return Constant(DATE.upper(), 'Date')
- elif _token == 'DATETIME':
- DATETIME = self._scan('DATETIME', context=_context)
- return Constant(DATETIME.upper(), 'Datetime')
- elif _token == 'TRUE':
- TRUE = self._scan('TRUE', context=_context)
- return Constant(True, 'Boolean')
- elif _token == 'FALSE':
- FALSE = self._scan('FALSE', context=_context)
- return Constant(False, 'Boolean')
- elif _token == 'FLOAT':
- FLOAT = self._scan('FLOAT', context=_context)
- return Constant(float(FLOAT), 'Float')
- elif _token == 'INT':
- INT = self._scan('INT', context=_context)
- return Constant(int(INT), 'Int')
- elif _token == 'STRING':
- STRING = self._scan('STRING', context=_context)
- return Constant(unquote(STRING), 'String')
- else: # == 'SUBSTITUTE'
- SUBSTITUTE = self._scan('SUBSTITUTE', context=_context)
- return Constant(SUBSTITUTE[2:-2], 'Substitute')
-
-
-def parse(rule, text):
- P = Hercule(HerculeScanner(text))
- return runtime.wrap_error_reporter(P, rule)
-
-# End -- grammar generated by Yapps
-
-
-
-from warnings import warn
-from rql.stmts import Union, Select, Delete, Insert, Set
-from rql.nodes import *
-
-
-def unquote(string):
- """Remove quotes from a string."""
- if string.startswith('"'):
- return string[1:-1].replace('\\\\', '\\').replace('\\"', '"')
- elif string.startswith("'"):
- return string[1:-1].replace('\\\\', '\\').replace("\\'", "'")
--- a/parser_main.py Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of rql.
-#
-# rql 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.
-#
-# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
-"""Main parser command.
-
-"""
-__docformat__ = "restructuredtext en"
-
-if __name__ == '__main__':
- from sys import argv
-
- parser = Hercule(HerculeScanner(argv[1]))
- e_types = {}
- # parse the RQL string
- try:
- tree = parser.goal(e_types)
- print '-'*80
- print tree
- print '-'*80
- print repr(tree)
- print e_types
- except SyntaxError, ex:
- # try to get error message from yapps
- from yapps.runtime import print_error
- print_error(ex, parser._scanner)
--- a/pygments_ext.py Mon Jul 28 11:12:58 2014 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
- pygments.lexers.rql
- ~~~~~~~~~~~~~~~~~~~
-
- Lexer for RQL the relation query language
-
- http://www.logilab.org/project/rql
-"""
-
-import re
-
-from pygments.lexer import RegexLexer, _mapping
-from pygments.token import Punctuation, \
- Text, Comment, Operator, Keyword, Name, String, Number
-
-__all__ = ['RqlLexer']
-
-class RqlLexer(RegexLexer):
- """
- Lexer for Relation Query Language.
- """
-
- name = 'RQL'
- aliases = ['rql']
- filenames = ['*.rql']
- mimetypes = ['text/x-rql']
-
- flags = re.IGNORECASE
- tokens = {
- 'root': [
- (r'\s+', Text),
- (r'(DELETE|SET|INSERT|UNION|DISTINCT|WITH|WHERE|BEING|OR'
- r'|AND|NOT|GROUPBY|HAVING|ORDERBY|ASC|DESC|LIMIT|OFFSET'
- r'|TODAY|NOW|TRUE|FALSE|NULL|EXISTS)\b', Keyword),
- (r'[+*/<>=%-]', Operator),
- (r'(Any|is|instance_of)\b', Name.Builtin),
- (r'[0-9]+', Number.Integer),
- (r'[A-Z_][A-Z0-9_]*\??', Name),
- (r"'(''|[^'])*'", String.Single),
- (r'"(""|[^"])*"', String.Single),
- (r'[;:()\[\],\.]', Punctuation)
- ],
- }
-
-_mapping.LEXERS['RqlLexer'] = ('rql.pygments_ext', 'RQL', ('rql',), ('*.rql',), ('text/x-rql',))
-
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/__init__.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,260 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of rql.
+#
+# rql 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.
+#
+# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
+"""RQL library (implementation independant)."""
+__docformat__ = "restructuredtext en"
+
+from math import log
+
+import sys
+import threading
+
+import pkg_resources
+from six import StringIO
+
+from rql._exceptions import *
+
+
+__version__ = pkg_resources.get_distribution('rql').version
+#REQUIRED_TYPES = ['String', 'Float', 'Int', 'Boolean', 'Date']
+
+
+class RQLHelper(object):
+ """Helper class for RQL handling
+
+ give access to methods for :
+ - parsing RQL strings
+ - variables type resolving
+ - comparison of two queries
+ """
+ def __init__(self, schema, uid_func_mapping=None, special_relations=None,
+ resolver_class=None, backend=None):
+ # chech schema
+ #for e_type in REQUIRED_TYPES:
+ # if not schema.has_entity(e_type):
+ # raise MissingType(e_type)
+ # create helpers
+ from rql.stcheck import RQLSTChecker, RQLSTAnnotator
+ special_relations = special_relations or {}
+ if uid_func_mapping:
+ for key in uid_func_mapping:
+ special_relations[key] = 'uid'
+ self._checker = RQLSTChecker(schema, special_relations, backend)
+ self._annotator = RQLSTAnnotator(schema, special_relations)
+ self._analyser_lock = threading.Lock()
+ if resolver_class is None:
+ from rql.analyze import ETypeResolver
+ resolver_class = ETypeResolver
+ self._analyser = resolver_class(schema, uid_func_mapping)
+ # IgnoreTypeRestriction analyser
+ from rql.analyze import ETypeResolverIgnoreTypeRestriction
+ self._itr_analyser_lock = threading.Lock()
+ self._itr_analyser = ETypeResolverIgnoreTypeRestriction(schema, uid_func_mapping)
+ self.set_schema(schema)
+
+ def set_schema(self, schema):
+ from rql.utils import is_keyword
+ for etype in schema.entities():
+ etype = str(etype)
+ if is_keyword(etype) or etype.capitalize() == 'Any':
+ raise UsesReservedWord(etype)
+ for rtype in schema.relations():
+ rtype = str(rtype)
+ if is_keyword(rtype):
+ raise UsesReservedWord(rtype)
+ self._checker.schema = schema
+ self._annotator.schema = schema
+ self._analyser.set_schema(schema)
+
+ def get_backend(self):
+ return self._checker.backend
+ def set_backend(self, backend):
+ self._checker.backend = backend
+ backend = property(get_backend, set_backend)
+
+ def parse(self, rqlstring, annotate=True):
+ """Return a syntax tree created from a RQL string."""
+ rqlst = parse(rqlstring, False)
+ self._checker.check(rqlst)
+ if annotate:
+ self.annotate(rqlst)
+ rqlst.schema = self._annotator.schema
+ return rqlst
+
+ def annotate(self, rqlst):
+ self._annotator.annotate(rqlst)
+
+ def compute_solutions(self, rqlst, uid_func_mapping=None, kwargs=None,
+ debug=False):
+ """Set solutions for variables of the syntax tree.
+
+ Each solution is a dictionary with variable's name as key and
+ variable's type as value.
+ """
+ self._analyser_lock.acquire()
+ try:
+ return self._analyser.visit(rqlst, uid_func_mapping, kwargs,
+ debug)
+ finally:
+ self._analyser_lock.release()
+
+ def compute_all_solutions(self, rqlst, uid_func_mapping=None, kwargs=None,
+ debug=False):
+ """compute syntaxe tree solutions with all types restriction (eg
+ is/instance_of relations) ignored
+ """
+ self._itr_analyser_lock.acquire()
+ try:
+ self._itr_analyser.visit(rqlst, uid_func_mapping, kwargs,
+ debug)
+ finally:
+ self._itr_analyser_lock.release()
+
+
+ def simplify(self, rqlst):
+ """Simplify `rqlst` by rewriting non-final variables associated to a const
+ node (if annotator say we can...)
+
+ The tree is modified in-place.
+ """
+ #print 'simplify', rqlst.as_string(encoding='UTF8')
+ if rqlst.TYPE == 'select':
+ from rql import nodes
+ for select in rqlst.children:
+ self._simplify(select)
+
+ def _simplify(self, select):
+ # recurse on subqueries first
+ for subquery in select.with_:
+ for subselect in subquery.query.children:
+ self._simplify(subselect)
+ rewritten = False
+ for var in list(select.defined_vars.values()):
+ stinfo = var.stinfo
+ if stinfo['constnode'] and not stinfo.get('blocsimplification'):
+ uidrel = stinfo['uidrel']
+ var = uidrel.children[0].variable
+ vconsts = []
+ rhs = uidrel.children[1].children[0]
+ for vref in var.references():
+ rel = vref.relation()
+ if rel is None:
+ term = vref
+ while not term.parent is select:
+ term = term.parent
+ if term in select.selection:
+ rhs = copy_uid_node(select, rhs, vconsts)
+ if vref is term:
+ select.selection[select.selection.index(vref)] = rhs
+ rhs.parent = select
+ else:
+ vref.parent.replace(vref, rhs)
+ elif term in select.orderby:
+ # remove from orderby
+ select.remove(term)
+ elif not select.having:
+ # remove from groupby if no HAVING clause
+ select.remove(term)
+ else:
+ rhs = copy_uid_node(select, rhs, vconsts)
+ select.groupby[select.groupby.index(vref)] = rhs
+ rhs.parent = select
+ elif rel is uidrel:
+ uidrel.parent.remove(uidrel)
+ elif rel.is_types_restriction():
+ stinfo['typerel'] = None
+ rel.parent.remove(rel)
+ else:
+ rhs = copy_uid_node(select, rhs, vconsts)
+ vref.parent.replace(vref, rhs)
+ del select.defined_vars[var.name]
+ stinfo['uidrel'] = None
+ rewritten = True
+ if vconsts:
+ select.stinfo['rewritten'][var.name] = vconsts
+ if rewritten and select.solutions:
+ select.clean_solutions()
+
+ def compare(self, rqlstring1, rqlstring2):
+ """Compare 2 RQL requests.
+
+ Return True if both requests would return the same results.
+ """
+ from rql.compare import compare_tree
+ return compare_tree(self.parse(rqlstring1), self.parse(rqlstring2))
+
+
+def copy_uid_node(select, node, vconsts):
+ node = node.copy(select)
+ node.uid = True
+ vconsts.append(node)
+ return node
+
+
+def parse(rqlstring, print_errors=True):
+ """Return a syntax tree created from a RQL string."""
+ from yapps.runtime import print_error, SyntaxError, NoMoreTokens
+ from rql.parser import Hercule, HerculeScanner
+ # make sure rql string ends with a semi-colon
+ rqlstring = rqlstring.strip()
+ if rqlstring and not rqlstring.endswith(';') :
+ rqlstring += ';'
+ # parse the RQL string
+ parser = Hercule(HerculeScanner(rqlstring))
+ try:
+ return parser.goal()
+ except SyntaxError as ex:
+ if not print_errors:
+ if ex.pos is not None:
+ multi_lines_rql = rqlstring.splitlines()
+ nb_lines = len(multi_lines_rql)
+ if nb_lines > 5:
+ width = log(nb_lines, 10)+1
+ template = " %%%ii: %%s" % width
+ rqlstring = '\n'.join( template % (idx + 1, line) for idx, line in enumerate(multi_lines_rql))
+
+
+ msg = '%s\nat: %r\n%s' % (rqlstring, ex.pos, ex.msg)
+ else:
+ msg = '%s\n%s' % (rqlstring, ex.msg)
+ exc = RQLSyntaxError(msg)
+ exc.__traceback__ = sys.exc_info()[-1]
+ raise exc
+ # try to get error message from yapps
+ try:
+ out = sys.stdout
+ sys.stdout = stream = StringIO()
+ try:
+ print_error(ex, parser._scanner)
+ finally:
+ sys.stdout = out
+ exc = RQLSyntaxError(stream.getvalue())
+ exc.__traceback__ = sys.exc_info()[-1]
+ raise exc
+ except ImportError: # duh?
+ sys.stdout = out
+ exc = RQLSyntaxError('Syntax Error', ex.msg, 'on line',
+ 1 + pinput.count('\n', 0, ex.pos))
+ exc.__traceback__ = sys.exc_info()[-1]
+ raise exc
+ except NoMoreTokens:
+ msg = 'Could not complete parsing; stopped around here: \n%s'
+ exc = RQLSyntaxError(msg % parser._scanner)
+ exc.__traceback__ = sys.exc_info()[-1]
+ raise exc
+
+pyparse = parse
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/_exceptions.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,41 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of rql.
+#
+# rql 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.
+#
+# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
+"""Exceptions used in the RQL package."""
+
+__docformat__ = "restructuredtext en"
+
+class RQLException(Exception):
+ """Base exception for exceptions of the rql module."""
+
+class MissingType(RQLException):
+ """Raised when there is some expected type missing from a schema."""
+
+class UsesReservedWord(RQLException):
+ """Raised when the schema uses a reserved word as type or relation."""
+
+class RQLSyntaxError(RQLException):
+ """Raised when there is a syntax error in the rql string."""
+
+class TypeResolverException(RQLException):
+ """Raised when we are unable to guess variables' type."""
+
+class BadRQLQuery(RQLException):
+ """Raised when there is a no sense in the rql query."""
+
+class CoercionError(RQLException):
+ """Failed to infer type of a math expression."""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/analyze.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,588 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of rql.
+#
+# rql 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.
+#
+# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
+"""Analyze of the RQL syntax tree to get possible types for RQL variables.
+
+"""
+from __future__ import print_function
+
+__docformat__ = "restructuredtext en"
+
+import os
+
+from six import StringIO, string_types
+from six.moves import zip
+
+from rql import TypeResolverException, nodes
+
+
+try:
+ pure = bool(os.environ.get('RQL_USE_PURE_PYTHON_ANALYSE', 0))
+ if pure:
+ raise ImportError
+ from rql import rql_solve
+except ImportError:
+ rql_solve = None
+ import warnings
+ warnings.filterwarnings(action='ignore', module='logilab.constraint.propagation')
+ from logilab.constraint import Repository, Solver, fd
+
+ # Gecode solver not available
+#rql_solve = None # uncomment to force using logilab-constraint
+
+class ConstraintCSPProblem(object):
+ def __init__(self):
+ self.constraints = []
+ self.domains = {}
+ self.scons = []
+ self.output = StringIO()
+
+ def debug(self):
+ print("Domains:", self.domains)
+ print("Constraints:", self.constraints)
+ print("Scons:", self.scons)
+
+ def get_output(self):
+ return self.output.getvalue()
+
+ def printer(self, *msgs):
+ self.output.write(' '.join(str(msg) for msg in msgs))
+ self.output.write('\n')
+
+ def solve(self):
+ repo = Repository(self.domains.keys(), self.domains, self.get_constraints())
+ solver = Solver(printer=self.printer)
+ # used for timing
+ #import time
+ #t0=time.time()
+ sols = solver.solve(repo, verbose=(True or self.debug))
+ #print "RUNTIME:", time.time()-t0
+ return sols
+
+ def add_var(self, name, values):
+ self.domains[name] = fd.FiniteDomain(values)
+
+ def end_domain_definition(self):
+ pass
+
+ def get_domains(self):
+ return self.domains
+
+ def get_constraints(self):
+ return self.constraints
+
+ def add_expr( self, vars, expr ):
+ self.constraints.append( fd.make_expression( vars, expr ) )
+ self.scons.append(expr)
+
+ def var_has_type(self, var, etype):
+ assert isinstance(etype, string_types)
+ self.add_expr( (var,), '%s == %r' % (var, etype) )
+
+ def var_has_types(self, var, etypes):
+ etypes = tuple(etypes)
+ for t in etypes:
+ assert isinstance(t, string_types)
+ if len(etypes) == 1:
+ cstr = '%s == "%s"' % (var, etypes[0])
+ else:
+ cstr = '%s in %s ' % (var, etypes)
+ self.add_expr( (var,), cstr)
+
+ def vars_have_same_types(self, varnames, types):
+ self.add_expr( varnames, '%s in %s' % ( '=='.join(varnames), types))
+
+ def or_and(self, equalities):
+ orred = set()
+ variables = set()
+ for orred_expr in equalities:
+ anded = set()
+ for vars, types in orred_expr:
+ types=tuple(types)
+ for t in types:
+ assert isinstance(t, string_types)
+ if len(types)==1:
+ anded.add( '%s == "%s"' % ( '=='.join(vars), types[0]) )
+ else:
+ anded.add( '%s in %s' % ( '=='.join(vars), types) )
+ for var in vars:
+ variables.add(var)
+ orred.add( '(' + ' and '.join( list(anded) ) + ')' )
+ expr = " or ".join( list(orred) )
+ self.add_expr( tuple(variables), expr )
+
+# GECODE based constraint solver
+_AND = 0 # symbolic values
+_OR = 1
+_EQ = 2
+_EQV = 3
+
+OPSYM={
+ _AND:"and",
+ _OR:"or",
+ _EQ:"eq",
+ _EQV:"eqv"
+}
+
+class GecodeCSPProblem(object):
+ """Builds an internal representation of the constraint
+ that will be passed to the rql_solve module which implements
+ a gecode-based solver
+
+ The internal representation is a tree builds with lists of lists
+ the first item of the list is the node type (_AND,_OR,_EQ,_EQV)
+
+ an example : ["and", [ "eq",0,0 ], ["or", ["eq", 1, 1], ["eq", 1, 2] ] ]
+
+ means Var(0) == Value(0) and ( Var(1)==Val(1) or Var(1) == Val(2)
+
+ TODO: at the moment the solver makes no type checking on the structure
+ of the tree thus can crash badly if something wrong is handled to it
+ this should not happend as the building of the tree is done internally
+ but it should be fixed anyways.
+ When fixing that we should also replace string nodes by integers
+ """
+ def __init__(self):
+ self.constraints = []
+ self.op = [ _AND ]
+ self.domains = {} # maps var name -> var value
+ self.variables = {} # maps var name -> var index
+ self.ivariables = [] # maps var index-> var name
+ self.values = {} # maps val name -> val index
+ self.all_values = set() # this gets turned into a list later
+ self.idx_domains = [] # maps var index -> list of val index
+ self.ivalues = {} # only used for debugging
+
+ def debug(self):
+ self.ivalues = {}
+ for val_name, val_num in self.values.items():
+ self.ivalues[val_num] = val_name
+ print("Domains:", self.domains)
+ print("Ops:", self.pretty_print_ops(self.op))
+ print("Variables:", self.variables)
+ print("Values:", self.values)
+
+
+ def pretty_print_ops(self, ops):
+ if ops[0] in (_AND, _OR):
+ res = [ OPSYM[ops[0]], '(' ]
+ for op in ops[1:]:
+ res.append(self.pretty_print_ops(op))
+ res.append(',')
+ res.append( ')' )
+ return "".join(res)
+ elif ops[0] == _EQ:
+ return "%s==%s" % (self.ivariables[ops[1]], self.ivalues[ops[2]])
+ elif ops[0] == _EQV:
+ res = [ self.ivariables[k] for k in ops[1:] ]
+ return '~='.join(res)
+
+ def get_output(self):
+ return ""
+
+ def solve(self):
+ constraints = self.op
+
+ # used for timing
+ #import time
+ #t0=time.time()
+
+ sols = rql_solve.solve( self.idx_domains, len(self.all_values), constraints )
+ rql_sols = []
+ for s in sols:
+ r={}
+ for var, val in zip(self.ivariables, s):
+ r[var] = self.all_values[val]
+ rql_sols.append(r)
+ #print "RUNTIME:", time.time()-t0
+ return rql_sols
+
+ def add_var(self, name, values):
+ assert name not in self.variables
+ self.all_values.update( values )
+ self.variables[name] = len(self.variables)
+ self.ivariables.append(name)
+ self.domains[name] = values
+
+ def end_domain_definition(self):
+ # maps integer->value
+ self.all_values = list(self.all_values)
+ # maps value->integer
+ self.values = dict( [ (v,i) for i,v in enumerate(self.all_values)] )
+ #print self.values
+ #print self.domains
+ for var_name in self.ivariables:
+ val_domain = self.domains[var_name]
+ idx_domain = [ self.values[val] for val in val_domain ]
+ self.idx_domains.append( idx_domain )
+
+ def and_eq( self, var, value ):
+ self.op.append( [_EQ, self.variables[var], self.values[value] ] )
+
+ def equal_vars(self, varnames):
+ if len(varnames)>1:
+ self.op.append( [ _EQV] + [ self.variables[v] for v in varnames ] )
+
+ def var_has_type(self, var, etype):
+ self.and_eq( var, etype)
+
+ def var_has_types(self, var, etypes):
+ for t in etypes:
+ assert isinstance(t, string_types)
+ if len(etypes) == 1:
+ self.and_eq( var, tuple(etypes)[0] )
+ else:
+ orred = [ _OR ]
+ for t in etypes:
+ try:
+ orred.append( [ _EQ, self.variables[var], self.values[t] ] )
+ except KeyError:
+ # key error may be raised by self.values[t] if self.values
+ # reflects constraints from subqueries
+ continue
+ self.op.append( orred )
+
+ def vars_have_same_types(self, varnames, types):
+ self.equal_vars( varnames )
+ for var in varnames:
+ self.var_has_types( var, types )
+
+ def or_and(self, equalities):
+ orred = [ _OR ]
+ for orred_expr in equalities:
+ anded = [ _AND ]
+ for vars, types in orred_expr:
+ self.equal_vars( vars )
+ for t in types:
+ assert isinstance(t, string_types)
+ for var in vars:
+ if len(types)==1:
+ anded.append( [ _EQ, self.variables[var], self.values[types[0]] ] )
+ else:
+ or2 = [ _OR ]
+ for t in types:
+ or2.append( [_EQ, self.variables[var], self.values[t] ] )
+ anded.append( or2 )
+ orred.append(anded)
+ self.op.append(orred)
+
+if rql_solve is None:
+ CSPProblem = ConstraintCSPProblem
+else:
+ CSPProblem = GecodeCSPProblem
+
+#CSPProblem = ConstraintCSPProblem
+
+
+class ETypeResolver(object):
+ """Resolve variables types according to the schema.
+
+ CSP modelisation:
+ * variable <-> RQL variable
+ * domains <-> different entity's types defined in the schema
+ * constraints <-> relations between (RQL) variables
+ """
+ var_solkey = 'possibletypes'
+
+ def __init__(self, schema, uid_func_mapping=None):
+ """
+ :Parameters:
+ * `schema`: an object describing entities and relations that implements
+ the ISchema interface.
+ * `uid_func_mapping`: a dictionary where keys are strings representing an
+ attribute used as a Unique IDentifier and values are methods that
+ accept attribute values and return entity's types.
+ [mapping from relation to function taking rhs value as argument
+ and returning an entity type].
+ """
+ self.debug = 0
+ self.set_schema(schema)
+ if uid_func_mapping is None:
+ self.uid_func_mapping = {}
+ self.uid_func = None
+ else:
+ self.uid_func_mapping = uid_func_mapping
+ self.uid_func = next(iter(uid_func_mapping.values()))
+
+ def set_schema(self, schema):
+ self.schema = schema
+ # default domains for a variable
+ self._base_domain = set(str(etype) for etype in schema.entities())
+ self._nonfinal_domain = set(str(etype) for etype in schema.entities()
+ if not etype.final)
+
+ def solve(self, node, constraints):
+ # debug info
+ if self.debug > 1:
+ print("- AN1 -"+'-'*80)
+ print(node)
+ print("CONSTRAINTS:")
+ constraints.debug()
+
+ sols = constraints.solve()
+
+ if not sols:
+ rql = node.as_string('utf8', self.kwargs)
+ ex_msg = 'Unable to resolve variables types in "%s"' % (rql,)
+ if True or self.debug:
+ ex_msg += '\n%s' % (constraints.get_output(),)
+ raise TypeResolverException(ex_msg)
+ node.set_possible_types(sols, self.kwargs, self.var_solkey)
+
+ def _visit(self, node, constraints=None):
+ """Recurse down the tree.
+
+ * node: rql node to process
+ * constraints: a XxxCSPProblem object.
+ """
+ func = getattr(self, 'visit_%s' % node.__class__.__name__.lower())
+ if constraints is None:
+ func(node)
+ elif func(node, constraints) is None:
+ for c in node.children:
+ self._visit(c, constraints)
+
+ def _uid_node_types(self, valnode):
+ types = set()
+ for cst in valnode.iget_nodes(nodes.Constant):
+ assert cst.type
+ if cst.type == 'Substitute':
+ eid = self.kwargs[cst.value]
+ self.deambiguifiers.add(cst.value)
+ else:
+ eid = cst.value
+ cst.uidtype = self.uid_func(cst.eval(self.kwargs))
+ types.add(cst.uidtype)
+ return types
+
+ def _init_stmt(self, node):
+ pb = CSPProblem()
+ # set domain for all the variables
+ for var in node.defined_vars.values():
+ pb.add_var( var.name, self._base_domain )
+ # no variable short cut
+ return pb
+
+ def _extract_constraint(self, constraints, var, term, get_target_types):
+ if self.uid_func:
+ alltypes = set()
+ for etype in self._uid_node_types(term):
+ for targettypes in get_target_types(etype):
+ alltypes.add(targettypes)
+ else:
+ alltypes = get_target_types()
+ domain = constraints.domains[var]
+ constraints.var_has_types( var, [str(t) for t in alltypes if t in domain] )
+
+ def visit(self, node, uid_func_mapping=None, kwargs=None, debug=False):
+ # FIXME: not thread safe
+ self.debug = debug
+ if uid_func_mapping is not None:
+ assert len(uid_func_mapping) <= 1
+ self.uid_func_mapping = uid_func_mapping
+ self.uid_func = next(iter(uid_func_mapping.values()))
+ self.kwargs = kwargs
+ self.deambiguifiers = set()
+ self._visit(node)
+ if uid_func_mapping is not None:
+ self.uid_func_mapping = None
+ self.uid_func = None
+ return self.deambiguifiers
+
+ def visit_union(self, node):
+ for select in node.children:
+ self._visit(select)
+
+ def visit_insert(self, node):
+ if not node.defined_vars:
+ node.set_possible_types([{}])
+ return
+ constraints = self._init_stmt(node)
+ constraints.end_domain_definition()
+ for etype, variable in node.main_variables:
+ if node.TYPE == 'delete' and etype == 'Any':
+ continue
+ assert etype in self.schema, etype
+ var = variable.name
+ constraints.var_has_type( var, etype )
+ for relation in node.main_relations:
+ self._visit(relation, constraints)
+ # get constraints from the restriction subtree
+ if node.where is not None:
+ self._visit(node.where, constraints)
+ self.solve(node, constraints)
+
+ visit_delete = visit_insert
+
+ def visit_set(self, node):
+ if not node.defined_vars:
+ node.set_possible_types([{}])
+ return
+ constraints = self._init_stmt(node)
+ constraints.end_domain_definition()
+ for relation in node.main_relations:
+ self._visit(relation, constraints)
+ # get constraints from the restriction subtree
+ if node.where is not None:
+ self._visit(node.where, constraints)
+ self.solve(node, constraints)
+
+ def visit_select(self, node):
+ if not (node.defined_vars or node.aliases):
+ node.set_possible_types([{}])
+ return
+ for subquery in node.with_: # resolve subqueries first
+ self.visit_union(subquery.query)
+ constraints = self._init_stmt(node)
+ for ca in node.aliases.values():
+ etypes = set(stmt.selection[ca.colnum].get_type(sol, self.kwargs)
+ for stmt in ca.query.children for sol in stmt.solutions)
+ constraints.add_var( ca.name, etypes )
+ constraints.end_domain_definition()
+ if self.uid_func:
+ # check rewritten uid const
+ for consts in node.stinfo['rewritten'].values():
+ if not consts:
+ continue
+ uidtype = self.uid_func(consts[0].eval(self.kwargs))
+ for const in consts:
+ const.uidtype = uidtype
+ # get constraints from the restriction subtree
+ if node.where is not None:
+ self._visit(node.where, constraints)
+ elif not node.with_:
+ varnames = [v.name for v in node.get_selected_variables()]
+ if varnames:
+ # add constraint on real relation types if no restriction
+ types = [eschema.type for eschema in self.schema.entities()
+ if not eschema.final]
+ constraints.vars_have_same_types( varnames, types )
+ self.solve(node, constraints)
+
+ def visit_relation(self, relation, constraints):
+ """extract constraints for an relation according to it's type"""
+ if relation.is_types_restriction():
+ self.visit_type_restriction(relation, constraints)
+ return None
+ rtype = relation.r_type
+ lhs, rhs = relation.get_parts()
+ if rtype == 'identity' and relation.neged(strict=True):
+ return None
+ if rtype in self.uid_func_mapping:
+ if isinstance(relation.parent, nodes.Not) or relation.operator() != '=':
+ # non final entity types
+ etypes = self._nonfinal_domain
+ else:
+ etypes = self._uid_node_types(rhs)
+ if etypes:
+ constraints.var_has_types( lhs.name, etypes )
+ return None
+ if isinstance(rhs, nodes.Comparison):
+ rhs = rhs.children[0]
+ rschema = self.schema.rschema(rtype)
+ if isinstance(lhs, nodes.Constant): # lhs is a constant node (simplified tree)
+ if not isinstance(rhs, nodes.VariableRef):
+ return None
+ self._extract_constraint(constraints, rhs.name, lhs, rschema.objects)
+ elif isinstance(rhs, nodes.Constant) and not rschema.final:
+ # rhs.type is None <-> NULL
+ if not isinstance(lhs, nodes.VariableRef) or rhs.type is None:
+ return None
+ self._extract_constraint(constraints, lhs.name, rhs, rschema.subjects)
+ elif not isinstance(lhs, nodes.VariableRef):
+ # XXX: check relation is valid
+ return None
+ elif isinstance(rhs, nodes.VariableRef):
+ lhsvar = lhs.name
+ rhsvar = rhs.name
+ lhsdomain = constraints.domains[lhsvar]
+ # filter according to domain necessary for column aliases
+ rhsdomain = constraints.domains[rhsvar]
+ res = []
+ var_types = []
+ same_var = (rhsvar == lhsvar)
+
+ for frometype, toetypes in rschema.associations():
+ fromtype = str(frometype)
+ if fromtype in lhsdomain:
+ totypes = set(str(t) for t in toetypes)
+ ptypes = totypes & rhsdomain
+ res.append( [ ([lhsvar], [str(fromtype)]),
+ ([rhsvar], list(ptypes)) ] )
+ if same_var and (fromtype in totypes): #ptypes ?
+ var_types.append(fromtype)
+ constraints.or_and(res)
+ if same_var:
+ constraints.var_has_types( lhsvar, var_types)
+ else:
+ # XXX consider rhs.get_type?
+ lhsdomain = constraints.domains[lhs.name]
+ ptypes = [str(subj) for subj in rschema.subjects()
+ if subj in lhsdomain]
+ constraints.var_has_types( lhs.name, ptypes )
+ return None
+
+ def visit_type_restriction(self, relation, constraints):
+ lhs, rhs = relation.get_parts()
+ etypes = set(c.value for c in rhs.iget_nodes(nodes.Constant)
+ if c.type == 'etype')
+ if relation.r_type == 'is_instance_of':
+ for etype in tuple(etypes):
+ for specialization in self.schema.eschema(etype).specialized_by():
+ etypes.add(specialization.type)
+ if relation.neged(strict=True):
+ etypes = frozenset(t for t in self._nonfinal_domain if not t in etypes)
+
+ constraints.var_has_types( lhs.name, [ str(t) for t in etypes ] )
+
+ def visit_and(self, et, constraints):
+ pass
+ def visit_or(self, ou, constraints):
+ pass
+ def visit_not(self, et, constraints):
+ pass
+ def visit_comparison(self, comparison, constraints):
+ pass
+ def visit_mathexpression(self, mathexpression, constraints):
+ pass
+ def visit_function(self, function, constraints):
+ pass
+ def visit_variableref(self, variableref, constraints):
+ pass
+ def visit_constant(self, constant, constraints):
+ pass
+ def visit_keyword(self, keyword, constraints):
+ pass
+ def visit_exists(self, exists, constraints):
+ pass
+
+
+class ETypeResolverIgnoreTypeRestriction(ETypeResolver):
+ """same as ETypeResolver but ignore type restriction relation
+
+ results are stored in as the 'allpossibletypes' key in variable'stinfo
+ """
+ var_solkey = 'allpossibletypes'
+
+ def visit_type_restriction(self, relation, constraints):
+ pass
+
+ def visit_not(self, et, constraints):
+ child = et.children[0]
+ if isinstance(child, nodes.Relation) and \
+ not self.schema.rschema(child.r_type).final:
+ return True
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/base.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,201 @@
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of rql.
+#
+# rql 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.
+#
+# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
+"""Base classes for RQL syntax tree nodes.
+
+Note: this module uses __slots__ to limit memory usage.
+"""
+
+__docformat__ = "restructuredtext en"
+
+from rql.utils import VisitableMixIn
+
+
+class BaseNode(VisitableMixIn):
+ __slots__ = ('parent',)
+
+ def __str__(self):
+ return self.as_string(encoding='utf-8')
+
+ def as_string(self, encoding=None, kwargs=None):
+ """Return the tree as an encoded rql string."""
+ raise NotImplementedError()
+
+ def initargs(self, stmt):
+ """Return list of arguments to give to __init__ to clone this node.
+
+ I don't use __getinitargs__ because I'm not sure it should interfer with
+ copy/pickle
+ """
+ return ()
+
+ @property
+ def root(self):
+ """Return the root node of the tree"""
+ return self.parent.root
+
+ @property
+ def stmt(self):
+ """Return the Select node to which this node belong"""
+ return self.parent.stmt
+
+ @property
+ def scope(self):
+ """Return the scope node to which this node belong (eg Select or Exists
+ node)
+ """
+ return self.parent.scope
+
+ def get_nodes(self, klass):
+ """Return the list of nodes of a given class in the subtree.
+
+ :type klass: a node class (Relation, Constant, etc.)
+ :param klass: the class of nodes to return
+
+ :rtype: list
+ """
+ stack = [self]
+ result = []
+ while stack:
+ node = stack.pop()
+ if isinstance(node, klass):
+ result.append(node)
+ else:
+ stack += node.children
+ return result
+
+ def iget_nodes(self, klass):
+ """Return an iterator over nodes of a given class in the subtree.
+
+ :type klass: a node class (Relation, Constant, etc.)
+ :param klass: the class of nodes to return
+
+ :rtype: iterator
+ """
+ stack = [self]
+ while stack:
+ node = stack.pop()
+ if isinstance(node, klass):
+ yield node
+ else:
+ stack += node.children
+
+ def is_equivalent(self, other):
+ if not other.__class__ is self.__class__:
+ return False
+ for i, child in enumerate(self.children):
+ try:
+ if not child.is_equivalent(other.children[i]):
+ return False
+ except IndexError:
+ return False
+ return True
+
+ def index_path(self):
+ if self.parent is None:
+ return []
+ myindex = self.parent.children.index(self)
+ parentindexpath = self.parent.index_path()
+ parentindexpath.append(myindex)
+ return parentindexpath
+
+ def go_to_index_path(self, path):
+ if not path:
+ return self
+ return self.children[path[0]].go_to_index_path(path[1:])
+
+ def copy(self, stmt):
+ """Create and return a copy of this node and its descendant.
+
+ stmt is the root node, which should be use to get new variables
+ """
+ new = self.__class__(*self.initargs(stmt))
+ for child in self.children:
+ new.append(child.copy(stmt))
+ return new
+
+
+class Node(BaseNode):
+ """Class for nodes of the tree which may have children (almost all...)"""
+ __slots__ = ('children',)
+
+ def __init__(self) :
+ self.parent = None
+ self.children = []
+
+ def append(self, child):
+ """add a node to children"""
+ self.children.append(child)
+ child.parent = self
+
+ def remove(self, child):
+ """Remove a child node. Return the removed node, its old parent and
+ index in the children list.
+ """
+ index = self.children.index(child)
+ del self.children[index]
+ parent = child.parent
+ child.parent = None
+ return child, parent, index
+
+ def insert(self, index, child):
+ """insert a child node"""
+ self.children.insert(index, child)
+ child.parent = self
+
+ def replace(self, old_child, new_child):
+ """replace a child node with another"""
+ i = self.children.index(old_child)
+ self.children.pop(i)
+ self.children.insert(i, new_child)
+ new_child.parent = self
+ return old_child, self, i
+
+class BinaryNode(Node):
+ __slots__ = ()
+
+ def __init__(self, lhs=None, rhs=None):
+ Node.__init__(self)
+ if not lhs is None:
+ self.append(lhs)
+ if not rhs is None:
+ self.append(rhs)
+
+ def remove(self, child):
+ """Remove the child and replace this node with the other child."""
+ index = self.children.index(child)
+ return self.parent.replace(self, self.children[not index])
+
+ def get_parts(self):
+ """Return the left hand side and the right hand side of this node."""
+ return self.children[0], self.children[1]
+
+
+class LeafNode(BaseNode):
+ """Class optimized for leaf nodes."""
+ __slots__ = ()
+
+ @property
+ def children(self):
+ return ()
+
+ def copy(self, stmt):
+ """Create and return a copy of this node and its descendant.
+
+ stmt is the root node, which should be use to get new variables.
+ """
+ return self.__class__(*self.initargs(stmt))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/compare.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,216 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of rql.
+#
+# rql 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.
+#
+# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
+"""Comparing syntax trees.
+
+"""
+__docformat__ = "restructuredtext en"
+
+
+from six.moves import range
+
+from rql.nodes import VariableRef, Variable, Function, Relation, Comparison
+
+def compare_tree(request1, request2):
+ """Compares 2 RQL requests.
+
+ :rtype: bool
+ :return: True if both requests would return the same results.
+ """
+ return make_canon_dict(request1) == make_canon_dict(request2)
+
+def make_canon_dict(rql_tree, verbose=0):
+ """Return a canonical representation of the request as a dictionnary."""
+ allvars = {}
+ canon = {
+ 'all_variables' : allvars,
+ 'selected' : [],
+ 'restriction' : {},
+ }
+
+ canon = RQLCanonizer().visit(rql_tree, canon)
+
+ # forge variable name
+ for var, name_parts in allvars.values():
+ name_parts.sort()
+ var.name = ':'.join(name_parts)
+ sort(canon)
+ if verbose:
+ print 'CANON FOR', rql_tree
+ from pprint import pprint
+ pprint(canon)
+ return canon
+
+def sort(canon_dict):
+ """Remove the all_variables entry and sort other entries in place."""
+ del canon_dict['all_variables']
+ canon_dict['selection'].sort()
+ for values in canon_dict['restriction'].values():
+ values.sort()
+
+class SkipChildren(Exception):
+ """Signal indicating to ignore the current child."""
+
+class RQLCanonizer(object):
+ """Build a dictionnary which represents a RQL syntax tree."""
+
+ def visit(self, node, canon):
+ try:
+ node.accept(self, canon)
+ except SkipChildren:
+ return canon
+ for c in node.children:
+ self.visit(c, canon)
+ return canon
+
+ def visit_select(self, select, canon):
+ allvars = canon['all_variables']
+ for var in select.defined_vars.values():
+ allvars[var] = (Variable(var.name), [])
+ canon['selection'] = l = []
+ selected = select.selected
+ for i in range(len(selected)):
+ node = selected[i]
+ if isinstance(node, VariableRef):
+ node = node.variable
+ allvars[node][1].append(str(i))
+ l.append(allvars[node][0])
+ else: # Function
+ l.append(node)
+ for var in node.iget_nodes(VariableRef):
+ var.parent.replace(var, allvars[var.variable][0])
+
+ def visit_group(self, group, canon):
+ canon['group'] = group
+
+ def visit_sort(self, sort, canon):
+ canon['sort'] = sort
+
+ def visit_sortterm(self, sortterm, canon):
+ pass
+
+ def visit_and(self, et, canon):
+ pass
+
+ def visit_or(self, ou, canon):
+ canon_dict = {}
+ keys = []
+ for expr in ou.get_nodes(Relation):
+ key = '%s%s' % (expr.r_type, expr._not)
+ canon_dict.setdefault(key, []).append(expr)
+ keys.append(key)
+ keys.sort()
+ r_type = ':'.join(keys)
+ r_list = canon['restriction'].setdefault(r_type, [])
+ done = {}
+ for key in keys:
+ if key in done:
+ continue
+ done[key] = None
+ for expr in canon_dict[key]:
+ self.manage_relation(expr, canon, r_list)
+ raise SkipChildren()
+
+ def manage_relation(self, relation, canon, r_list):
+ lhs, rhs = relation.get_parts()
+ # handle special case of the IN function
+ func = rhs.children[0]
+ if isinstance(func, Function) and func.name == 'IN':
+ if not relation._not:
+ base_key = '%s%s' % (relation.r_type, relation._not)
+ if not canon['restriction'][base_key]:
+ del canon['restriction'][base_key]
+ key = ':'.join([base_key] * len(func.children))
+ r_list = canon['restriction'].setdefault(key, [])
+ for e in func.children:
+ eq_expr = Relation(relation.r_type, relation._not)
+ eq_expr.append(lhs)
+ eq_expr.append(Comparison('=', e))
+ self.manage_relation(eq_expr, canon, r_list)
+ # restaure parent attribute to avoid problem later
+ e.parent = func
+ lhs.parent = relation
+ return
+ # build a canonical representation for this relation
+ lhs_expr_reminder = make_lhs_reminder(lhs, canon)
+ rhs_expr_reminder = make_rhs_reminder(rhs, canon)
+ reminder = (lhs_expr_reminder, rhs_expr_reminder)
+ # avoid duplicate
+ if reminder in r_list:
+ return
+ r_list.append(reminder)
+ # make a string which represents this relation (we'll use it later
+ # to build variables' name)
+ expr_reminder = relation.r_type
+ lhs_vars = lhs.get_nodes(VariableRef)
+ if not lhs_vars:
+ expr_reminder = "%s_%s" % (lhs, expr_reminder)
+ rhs_vars = rhs.get_nodes(VariableRef)
+ if not rhs_vars:
+ expr_reminder = "%s_%s" % (expr_reminder, rhs)
+
+ for var in lhs_vars + rhs_vars:
+ var = var.variable
+ canon['all_variables'][var][1].append(expr_reminder)
+
+
+ def visit_relation(self, relation, canon):
+ key = '%s%s' % (relation.r_type, relation._not)
+ r_list = canon['restriction'].setdefault(key, [])
+ self.manage_relation(relation, canon, r_list)
+
+
+ def visit_comparison(self, comparison, canon):
+ """do nothing for this node type"""
+
+ def visit_mathexpression(self, mathexpression, canon):
+ """do nothing for this node type"""
+
+ def visit_function(self, function, canon):
+ """do nothing for this node type"""
+
+ def visit_variableref(self, varref, canon):
+ varref.parent.replace(varref,
+ canon['all_variables'][varref.variable][0])
+
+ def visit_constant(self, constante, canon):
+ """do nothing for this node type"""
+
+ def visit_union(self, *args):
+ raise NotImplementedError('union comparison not implemented')
+
+
+def make_lhs_reminder(lhs, canon):
+ """Return a reminder for a relation's left hand side
+ (i.e a VariableRef object).
+ """
+ try:
+ lhs = canon['all_variables'][lhs.variable][0]
+ except (KeyError, IndexError):
+ pass
+ return ('=', lhs)
+
+def make_rhs_reminder(rhs, canon):
+ """Return a reminder for a relation's right hand side
+ (i.e a Comparison object).
+ """
+ child = rhs.children[0]
+ try:
+ child = canon['all_variables'][child.variable][0]
+ except AttributeError:
+ pass
+ return (rhs.operator, child)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/editextensions.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,66 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of rql.
+#
+# rql 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.
+#
+# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
+"""RQL functions for manipulating syntax trees."""
+
+__docformat__ = "restructuredtext en"
+
+from rql.nodes import Constant, Variable, VariableRef, Relation, make_relation
+
+def switch_selection(rqlst, new_var, old_var):
+ """Switch the select variable from old_var (VariableRef instance) to
+ new_var (Variable instance).
+ """
+ rqlst.remove_selected(old_var)
+ rqlst.add_selected(new_var, 0)
+
+def add_main_restriction(rqlst, new_type, r_type, direction):
+ """The result_tree must represent the same restriction as 'rqlst' and :
+ - 'new_varname' IS <new_type>
+ - 'old_main_var' <r_type> 'new_varname'
+ """
+ new_var = rqlst.make_variable(new_type)
+ # new_var IS new_type
+ rqlst.add_restriction(make_relation(new_var, 'is', (new_type, 'etype'),
+ Constant))
+ # new_var REL old_var (ou l'inverse)
+ old_var = rqlst.selected[0]
+ if direction == 'subject':
+ rel_rest = make_relation(old_var.variable, r_type, (new_var, 1),
+ VariableRef)
+ else:
+ rel_rest = make_relation(new_var, r_type, (old_var.variable, 1),
+ VariableRef)
+ rqlst.add_restriction(rel_rest)
+ return new_var
+
+def remove_has_text_relation(node):
+ """Remove has_text relation."""
+ for rel in node.iget_nodes(Relation):
+ if rel.r_type == 'has_text':
+ node.remove_node(rel)
+ return
+
+def get_vars_relations(node):
+ """Return a dict with 'var_names' as keys, and the list of relations which
+ concern them.
+ """
+ exp_concerns = {}
+ for exp in node.iget_nodes(Relation):
+ for vref in exp.iget_nodes(VariableRef):
+ exp_concerns.setdefault(vref.name, []).append(exp)
+ return exp_concerns
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/gecode_solver.cpp Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,544 @@
+#include <Python.h>
+#include <iostream>
+#include <string.h>
+#include <exception>
+#include "gecode/kernel.hh"
+#include "gecode/int.hh"
+#include "gecode/search.hh"
+
+#if 1
+#define debug(fmt,...)
+#else
+#define debug(fmt,...) printf(fmt, ##__VA_ARGS__)
+#endif
+
+
+#define PM_VERSION(a,b,c) ((a<<16)+(b<<8)+(c))
+// There is no easy way to test for gecode version here
+// so the build system must pass GE_VERSION accordingly
+// by default we build for 3.1.0 if GECODE_VERSION exists
+
+#ifndef GE_VERSION
+#ifndef GECODE_VERSION
+#define GE_VERSION PM_VERSION(2,1,2)
+#else
+#define GE_VERSION PM_VERSION(3,1,0)
+#endif
+#endif
+
+#if GE_VERSION < PM_VERSION(2,0,0)
+#define SELF this
+#define INT_VAR_NONE BVAR_NONE
+#define INT_VAL_MIN BVAL_MIN
+
+#elif GE_VERSION < PM_VERSION(3,0,0)
+#define SELF this
+#define SET_VAR_SIZE_MAX SET_VAR_MAX_CARD
+#define SET_VAL_MIN_INC SET_VAL_MIN
+#else
+#define SELF (*this)
+#define convexHull convex
+#endif
+
+#if GE_VERSION >= PM_VERSION(4, 0, 0)
+#define INT_VAR_NONE INT_VAR_NONE()
+#define INT_VAL_MIN INT_VAL_MIN()
+#endif
+
+using namespace std;
+using namespace Gecode;
+
+#define USE_CLOCK
+#ifdef USE_CLOCK
+#include <ctime>
+
+/// Timer interface stolen from gecode examples
+class Timer {
+private:
+ clock_t t0;
+public:
+ void start(void);
+ double stop(void);
+};
+
+forceinline void
+Timer::start(void) {
+ t0 = clock();
+}
+
+forceinline double
+Timer::stop(void) {
+ return (static_cast<double>(clock()-t0) / CLOCKS_PER_SEC) * 1000.0;
+}
+#else
+#include <sys/time.h>
+#include <unistd.h>
+
+/// Timer interface stolen from gecode examples
+class Timer {
+private:
+ struct timeval t0;
+public:
+ void start(void);
+ double stop(void);
+};
+
+forceinline void
+Timer::start(void) {
+ gettimeofday( &t0, NULL );
+}
+forceinline double
+Timer::stop(void) {
+ struct timeval t1;
+ gettimeofday( &t1, NULL );
+ return (t1.tv_sec - t0.tv_sec)+1e-6*(t1.tv_usec - t0.tv_usec);
+}
+#endif
+
+enum {
+ _AND = 0,
+ _OR = 1,
+ _EQ = 2,
+ _EQV = 3
+};
+
+class RqlError : public exception {
+};
+
+class RqlContext {
+ /** Context holding the problem for solving Rql constraints
+ we keep the info as Python objects and parse the problem
+ during the creation of the Gecode problem
+ */
+public:
+ RqlContext(long nvars, PyObject* domains,
+ long nvalues, PyObject* constraints, PyObject* sols):
+ solutions(-1), // return every solutions
+ time(-1), // time limit in case the problem is too big
+ fails(-1), // ?? used by GecodeStop ...
+ nvars(nvars), // Number of variables
+ nvalues(nvalues), // Number of values
+ constraints(constraints), // A python list, holding the root of the problem
+ sols(sols), // an empty list that will receive the solutions
+ domains(domains), // A PyList of PyList, one for each var,
+ // holding the allowable integer values
+ verbosity(false) // can help debugging
+ {
+ }
+
+ long solutions;
+ long time;
+ long fails;
+ long nvars;
+ long nvalues;
+ PyObject* constraints;
+ PyObject* sols;
+ PyObject* domains;
+ bool verbosity;
+};
+
+class RqlSolver : public Space {
+/* A gecode Space
+ this is a strange beast that requires special methods and
+ behavior (mostly, copy and (bool,share,space) constructor
+*/
+protected:
+ /* The variables we try to find values for
+ these are the only 'public' variable of the
+ problem.
+
+ we use a lot more intermediate variables but
+ they shouldn't be member of the space
+ */
+ IntVarArray variables;
+
+public:
+ RqlSolver(const RqlContext& pb):
+ variables(SELF, // all gecode variable keep a reference to the space
+ pb.nvars, // number of variables
+ 0, // minimum domain value
+ pb.nvalues-1) // max domain value (included)
+ {
+ /* Since we manipulate Boolean expression and
+ we need to assign truth value to subexpression
+ eg (a+b)*(c+d) will be translated as :
+ root = x1 * x2
+ x1 = a+b
+ x2 = c+d
+ root = True
+ */
+ BoolVar root(SELF, 1,1);
+
+ set_domains( pb.domains );
+ add_constraints( pb.constraints, root );
+
+ /* the branching strategy, there must be one,
+ changing it might improve performance, but
+ in out case, we almost never propagate (ie
+ gecode solves the problem during its creation)
+ */
+ branch(SELF, variables, INT_VAR_NONE, INT_VAL_MIN);
+ }
+
+ ~RqlSolver() {};
+
+ RqlSolver(bool share, RqlSolver& s) : Space(share,s)
+ {
+ /* this is necessary for the solver to fork space
+ while branching
+ */
+ variables.update(SELF, share, s.variables);
+ }
+
+ void set_domains( PyObject* domains )
+ {
+ PyObject* ovalues;
+ if (!PyList_Check(domains)) {
+ throw RqlError();
+ }
+ int n = PyList_Size( domains );
+ for(int var=0 ;var<n; ++var) {
+ /* iterate of domains which should contains
+ list of values
+ domains[0] contains possible values for var[0]...
+ */
+ int i, nval;
+ ovalues = PyList_GetItem( domains, var );
+ if (!PyList_Check(ovalues)) {
+ throw RqlError();
+ }
+ nval = PyList_Size( ovalues );
+
+ /* It's a bit cumbersome to construct an IntSet, but
+ it's the only way to reduce an integer domain to
+ a discrete set
+ */
+ int* vals = new int[nval];
+ for(i=0;i<nval;++i) {
+ //refcount ok, borrowed ref
+ vals[i] = PyInt_AsLong( PyList_GetItem( ovalues, i ) );
+ if (vals[i]<0) {
+ /* we don't have negative values and PyInt_AsLong returns
+ -1 if the object is not an Int */
+ delete [] vals;
+ throw RqlError();
+ }
+ }
+ IntSet gvalues(vals,nval);
+ dom(SELF, variables[var], gvalues);
+ delete [] vals;
+ }
+ }
+
+ /* Dispatch method from Node to specific node type */
+ void add_constraints( PyObject* desc, BoolVar& var )
+ {
+ long type;
+
+ if (!PyList_Check(desc)) {
+ throw RqlError();
+ }
+ /* the first element of each list (node) is
+ a symbolic Int from _AND, _OR, _EQ, _EQV
+ */
+ type = PyInt_AsLong( PyList_GetItem( desc, 0 ) );
+
+ switch(type) {
+ case _AND:
+ add_and( desc, var );
+ break;
+ case _OR:
+ add_or( desc, var );
+ break;
+ case _EQ:
+ add_equality( desc, var );
+ break;
+ case _EQV:
+ add_equivalence( desc, var );
+ break;
+ default:
+ throw RqlError();
+ }
+ }
+
+ /* retrieve an int from a list, throw error if int is <0 */
+ long get_uint( PyObject* lst, int index ) {
+ PyObject* val;
+ val = PyList_GetItem(lst, index);
+ if (val<0) {
+ throw RqlError();
+ }
+ return PyInt_AsLong(val);
+ }
+
+ /* post gecode condition for Var == Value
+ we can't use domain restriction since this
+ condition can be part of an OR clause
+
+ so we post (var == value) <=> expr_value
+ */
+ void add_equality( PyObject* desc, BoolVar& expr_value ) {
+ long variable, value;
+
+ variable = get_uint( desc, 1 );
+ value = get_uint( desc, 2 );
+ if (variable==1) {
+ debug("RQL:%ld == %ld ***\n", variable, value);
+ } else {
+ debug("RQL:%ld == %ld\n", variable, value);
+ }
+ rel(SELF, variables[variable], IRT_EQ, value, expr_value);
+ }
+
+ /* post gecode condition for Var[i] == Var[j] ... == Var[k]
+
+ there's no operator for assigning chained equality to boolean
+
+ so we post for 1<=i<=N (var[0] == var[i]) <=> bool[i]
+ and bool[1] & ... & bool[N] <=> expr_value
+ that means if all vars are equal expr_value is true
+ if all vars are different from var[0] expr_value is false
+ if some are equals and some false, the constraint is unsatisfiable
+ */
+ void add_equivalence( PyObject* desc, BoolVar& expr_value ) {
+ int len = PyList_Size(desc);
+ int var0 = get_uint( desc, 1 );
+ BoolVarArray terms(SELF, len-2,0,1);
+ debug("RQL:EQV(%d",var0);
+ for (int i=1;i<len-1;++i) {
+ int var1 = get_uint(desc, i+1);
+ debug(",%d",var1);
+ rel(SELF, variables[var0], IRT_EQ, variables[var1], terms[i-1] );
+ }
+ debug(")\n");
+#if GE_VERSION<PM_VERSION(2,0,0)
+ BoolVarArgs terms_args(terms);
+ bool_and(SELF, terms_args, expr_value);
+#else
+ rel(SELF, BOT_AND, terms, expr_value);
+#endif
+ }
+
+ /* simple and relation between nodes */
+ void add_and( PyObject* desc, BoolVar& var ) {
+ int len = PyList_Size(desc);
+ BoolVarArray terms(SELF, len-1,0,1);
+
+ debug("RQL:AND(\n");
+ for(int i=0;i<len-1;++i) {
+ PyObject* expr = PyList_GetItem(desc, i+1);
+ add_constraints( expr, terms[i] );
+ }
+ debug("RQL:)\n");
+#if GE_VERSION<PM_VERSION(2,0,0)
+ BoolVarArgs terms_args(terms);
+ bool_and(SELF, terms_args, var);
+#else
+ rel(SELF, BOT_AND, terms, var);
+#endif
+ }
+
+ /* simple or relation between nodes */
+ void add_or( PyObject* desc, BoolVar& var ) {
+ int len = PyList_Size(desc);
+ BoolVarArray terms(SELF, len-1,0,1);
+
+ debug("RQL:OR(\n");
+ for(int i=0;i<len-1;++i) {
+ PyObject* expr = PyList_GetItem(desc, i+1);
+ add_constraints( expr, terms[i] );
+ }
+ debug("RQL:)\n");
+#if GE_VERSION<PM_VERSION(2,0,0)
+ BoolVarArgs terms_args(terms);
+ bool_or(SELF, terms_args, var);
+#else
+ rel(SELF, BOT_OR, terms, var);
+#endif
+
+ }
+
+ template <template<class> class Engine>
+ static void run( RqlContext& pb, Search::Stop* stop )
+ {
+ double t0 = 0;
+ int i = pb.solutions;
+ Timer t;
+ RqlSolver* s = new RqlSolver( pb );
+ t.start();
+ unsigned int n_p = 0;
+ unsigned int n_b = 0;
+ if (s->status() != SS_FAILED) {
+ n_p = s->propagators();
+#if GE_VERSION<PM_VERSION(3,2,0)
+ n_b = s->branchings();
+#else
+ n_b = s->branchers();
+#endif
+ }
+#if GE_VERSION<PM_VERSION(2,0,0)
+ Engine<RqlSolver> e(s);
+#else
+ Search::Options opts;
+ //opts.c_d = pb.c_d;
+ //opts.a_d = pb.a_d;
+ opts.stop = stop;
+ Engine<RqlSolver> e(s, opts);
+#endif
+ delete s;
+ do {
+ RqlSolver* ex = e.next();
+ if (ex == NULL)
+ break;
+
+ ex->add_new_solution(pb);
+
+ delete ex;
+ t0 = t0 + t.stop();
+ } while (--i != 0 && (pb.time<0 || t0 < pb.time));
+ Search::Statistics stat = e.statistics();
+ if (pb.verbosity) {
+ cout << endl;
+ cout << "Initial" << endl
+ << "\tpropagators: " << n_p << endl
+ << "\tbranchings: " << n_b << endl
+ << endl
+ << "Summary" << endl
+ << "\truntime: " << t.stop() << endl
+ << "\tsolutions: " << abs(static_cast<int>(pb.solutions) - i) << endl
+ << "\tpropagations: " << stat.propagate << endl
+ << "\tfailures: " << stat.fail << endl
+#if GE_VERSION < PM_VERSION(3,0,0)
+ << "\tclones: " << stat.clone << endl
+ << "\tcommits: " << stat.commit << endl
+#else
+ << "\tdepth: " << stat.depth << endl
+ << "\tnode: " << stat.node << endl
+#endif
+#if GE_VERSION < PM_VERSION(4,2,0)
+ << "\tpeak memory: "
+ << static_cast<int>((stat.memory+1023) / 1024) << " KB"
+ << endl
+#endif
+ ;
+ }
+ }
+
+ /* We append each solutions to `sols` as a
+ tuple `t` of the values assigned to each var
+ that is t[i] = solution for var[i]
+ */
+ virtual void add_new_solution(RqlContext& pb) {
+ PyObject *tuple, *ival;
+
+ tuple = PyTuple_New( pb.nvars );
+
+ for(int i=0;i<pb.nvars;++i) {
+ ival = PyInt_FromLong( variables[i].val() );
+ PyTuple_SetItem( tuple, i, ival );
+ }
+ PyList_Append( pb.sols, tuple );
+ }
+
+ /* another function need by gecode kernel */
+ virtual Space* copy(bool share) {
+ return new RqlSolver(share, *this);
+ }
+
+};
+
+class FailTimeStop : public Search::Stop {
+private:
+ Search::TimeStop *ts;
+ Search::FailStop *fs;
+public:
+ FailTimeStop(int fails, int time):ts(0L),fs(0L) {
+ if (time>=0)
+ ts = new Search::TimeStop(time);
+ if (fails>=0) {
+ fs = new Search::FailStop(fails);
+ }
+ }
+#if GE_VERSION < PM_VERSION(3,1,0)
+ bool stop(const Search::Statistics& s) {
+ int sigs = PyErr_CheckSignals();
+ bool fs_stop = false;
+ bool ts_stop = false;
+ if (fs) {
+ fs_stop = fs->stop(s);
+ }
+ if (ts) {
+ ts_stop = ts->stop(s);
+ }
+ return sigs || fs_stop || ts_stop;
+ }
+#else
+ /* from gecode 3.1.0 */
+ bool stop(const Search::Statistics& s, const Search::Options &o) {
+ int sigs = PyErr_CheckSignals();
+ bool fs_stop = false;
+ bool ts_stop = false;
+ if (fs) {
+ fs_stop = fs->stop(s,o);
+ }
+ if (ts) {
+ ts_stop = ts->stop(s,o);
+ }
+ return sigs || fs_stop || ts_stop;
+ }
+#endif
+
+ /// Create appropriate stop-object
+ static Search::Stop* create(int fails, int time) {
+ return new FailTimeStop(fails, time);
+ }
+};
+
+static void _solve( RqlContext& ctx )
+{
+ Search::Stop *stop = FailTimeStop::create(ctx.fails, ctx.time);
+
+ RqlSolver::run<DFS>( ctx, stop );
+}
+
+
+static PyObject *
+rql_solve(PyObject *self, PyObject *args)
+{
+ PyObject* sols = 0L;
+ PyObject* constraints;
+ PyObject* domains;
+ long nvars, nvalues;
+ if (!PyArg_ParseTuple(args, "OiO", &domains, &nvalues, &constraints))
+ return NULL;
+ sols = PyList_New(0);
+ try {
+ if (!PyList_Check(domains)) {
+ throw RqlError();
+ }
+ nvars = PyList_Size(domains);
+ RqlContext ctx(nvars, domains, nvalues, constraints, sols );
+ _solve( ctx );
+ } catch(RqlError& e) {
+ Py_DECREF(sols);
+ PyErr_SetString(PyExc_RuntimeError, "Error parsing constraints");
+ return NULL;
+ };
+ return sols;
+}
+
+static PyMethodDef SolveRqlMethods[] = {
+ {"solve", rql_solve, METH_VARARGS,
+ "Solve RQL variable types problem."},
+ {NULL, NULL, 0, NULL} /* Sentinel */
+};
+
+
+PyMODINIT_FUNC
+initrql_solve(void)
+{
+ PyObject* m;
+ m = Py_InitModule("rql_solve", SolveRqlMethods);
+ if (m == NULL)
+ return;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/interfaces.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,81 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of rql.
+#
+# rql 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.
+#
+# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
+"""Interfaces used by the RQL package.
+
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.common.interface import Interface
+
+class ISchema(Interface):
+ """RQL expects some base types to exists: String, Float, Int, Boolean, Date
+ and a base relation : is
+ """
+
+ def has_entity(self, etype):
+ """Return true if the given type is defined in the schema.
+ """
+
+ def has_relation(self, rtype):
+ """Return true if the given relation's type is defined in the schema.
+ """
+
+ def entities(self, schema=None):
+ """Return the list of possible types.
+
+ If schema is not None, return a list of schemas instead of types.
+ """
+
+ def relations(self, schema=None):
+ """Return the list of possible relations.
+
+ If schema is not None, return a list of schemas instead of relation's
+ types.
+ """
+
+ def relation_schema(self, rtype):
+ """Return the relation schema for the given relation type.
+ """
+
+
+class IRelationSchema(Interface):
+ """Interface for Relation schema (a relation is a named oriented link
+ between two entities).
+ """
+
+ def associations(self):
+ """Return a list of (fromtype, [totypes]) defining between which types
+ this relation may exists.
+ """
+
+ def subjects(self):
+ """Return a list of types which can be subject of this relation.
+ """
+
+ def objects(self):
+ """Return a list of types which can be object of this relation.
+ """
+
+class IEntitySchema(Interface):
+ """Interface for Entity schema."""
+
+ def is_final(self):
+ """Return true if the entity is a final entity (ie cannot be used
+ as subject of a relation).
+ """
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/nodes.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,1123 @@
+# copyright 2004-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of rql.
+#
+# rql 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.
+#
+# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
+"""RQL syntax tree nodes.
+
+This module defines all the nodes we can find in a RQL Syntax tree, except
+root nodes, defined in the `stmts` module.
+"""
+
+__docformat__ = "restructuredtext en"
+
+import sys
+from itertools import chain
+from decimal import Decimal
+from datetime import datetime, date, time, timedelta
+from time import localtime
+
+from logilab.database import DYNAMIC_RTYPE
+
+from rql import CoercionError, RQLException
+from rql.base import BaseNode, Node, BinaryNode, LeafNode
+from rql.utils import (function_description, quote, uquote, common_parent,
+ VisitableMixIn)
+
+CONSTANT_TYPES = frozenset((None, 'Date', 'Datetime', 'Boolean', 'Float', 'Int',
+ 'String', 'Substitute', 'etype'))
+
+
+ETYPE_PYOBJ_MAP = { bool: 'Boolean',
+ int: 'Int',
+ float: 'Float',
+ Decimal: 'Decimal',
+ str: 'String',
+ datetime: 'Datetime',
+ date: 'Date',
+ time: 'Time',
+ timedelta: 'Interval',
+ }
+if sys.version_info < (3,):
+ ETYPE_PYOBJ_MAP[long] = 'Int'
+ ETYPE_PYOBJ_MAP[unicode] = 'String'
+
+KEYWORD_MAP = {'NOW' : datetime.now,
+ 'TODAY': date.today}
+
+def etype_from_pyobj(value):
+ """guess yams type from python value"""
+ # note:
+ # * Password is not selectable so no problem
+ # * use type(value) and not value.__class__ since C instances may have no
+ # __class__ attribute
+ return ETYPE_PYOBJ_MAP[type(value)]
+
+def variable_ref(var):
+ """get a VariableRef"""
+ if isinstance(var, Variable):
+ return VariableRef(var, noautoref=1)
+ assert isinstance(var, VariableRef)
+ return var
+
+def variable_refs(node):
+ for vref in node.iget_nodes(VariableRef):
+ if isinstance(vref.variable, Variable):
+ yield vref
+
+
+class OperatorExpressionMixin(object):
+
+ def initargs(self, stmt):
+ """return list of arguments to give to __init__ to clone this node"""
+ return (self.operator,)
+
+ def is_equivalent(self, other):
+ if not Node.is_equivalent(self, other):
+ return False
+ return self.operator == other.operator
+
+ def get_description(self, mainindex, tr):
+ """if there is a variable in the math expr used as rhs of a relation,
+ return the name of this relation, else return the type of the math
+ expression
+ """
+ try:
+ return tr(self.get_type())
+ except CoercionError:
+ for vref in self.iget_nodes(VariableRef):
+ vtype = vref.get_description(mainindex, tr)
+ if vtype != 'Any':
+ return tr(vtype)
+
+
+class HSMixin(object):
+ """mixin class for classes which may be the lhs or rhs of an expression"""
+ __slots__ = ()
+
+ def relation(self):
+ """return the parent relation where self occurs or None"""
+ try:
+ return self.parent.relation()
+ except AttributeError:
+ return None
+
+ def get_description(self, mainindex, tr):
+ mytype = self.get_type()
+ if mytype != 'Any':
+ return tr(mytype)
+ return 'Any'
+
+
+# rql st edition utilities ####################################################
+
+def make_relation(var, rel, rhsargs, rhsclass, operator='='):
+ """build an relation equivalent to '<var> rel = <cst>'"""
+ cmpop = Comparison(operator)
+ cmpop.append(rhsclass(*rhsargs))
+ relation = Relation(rel)
+ if hasattr(var, 'variable'):
+ var = var.variable
+ relation.append(VariableRef(var))
+ relation.append(cmpop)
+ return relation
+
+def make_constant_restriction(var, rtype, value, ctype, operator='='):
+ if ctype is None:
+ ctype = etype_from_pyobj(value)
+ if isinstance(value, (set, frozenset, tuple, list, dict)):
+ if len(value) > 1:
+ rel = make_relation(var, rtype, ('IN',), Function, operator)
+ infunc = rel.children[1].children[0]
+ for atype in sorted(value):
+ infunc.append(Constant(atype, ctype))
+ return rel
+ value = next(iter(value))
+ return make_relation(var, rtype, (value, ctype), Constant, operator)
+
+
+class EditableMixIn(object):
+ """mixin class to add edition functionalities to some nodes, eg root nodes
+ (statement) and Exists nodes
+ """
+ __slots__ = ()
+
+ @property
+ def undo_manager(self):
+ return self.root.undo_manager
+
+ @property
+ def should_register_op(self):
+ root = self.root
+ # root is None during parsing
+ return root is not None and root.should_register_op
+
+ def remove_node(self, node, undefine=False):
+ """remove the given node from the tree
+
+ USE THIS METHOD INSTEAD OF .remove to get correct variable references
+ handling
+ """
+ # unregister variable references in the removed subtree
+ parent = node.parent
+ stmt = parent.stmt
+ for varref in node.iget_nodes(VariableRef):
+ varref.unregister_reference()
+ if undefine and not varref.variable.stinfo['references']:
+ stmt.undefine_variable(varref.variable)
+ # remove return actually removed node and its parent
+ node, parent, index = parent.remove(node)
+ if self.should_register_op:
+ from rql.undo import RemoveNodeOperation
+ self.undo_manager.add_operation(RemoveNodeOperation(node, parent, stmt, index))
+
+ def add_restriction(self, relation):
+ """add a restriction relation"""
+ r = self.where
+ if r is not None:
+ newnode = And(r, relation)
+ self.set_where(newnode)
+ if self.should_register_op:
+ from rql.undo import ReplaceNodeOperation
+ self.undo_manager.add_operation(ReplaceNodeOperation(r, newnode))
+ else:
+ self.set_where(relation)
+ if self.should_register_op:
+ from rql.undo import AddNodeOperation
+ self.undo_manager.add_operation(AddNodeOperation(relation))
+ return relation
+
+ def add_constant_restriction(self, var, rtype, value, ctype,
+ operator='='):
+ """builds a restriction node to express a constant restriction:
+
+ variable rtype = value
+ """
+ restr = make_constant_restriction(var, rtype, value, ctype, operator)
+ return self.add_restriction(restr)
+
+ def add_relation(self, lhsvar, rtype, rhsvar):
+ """builds a restriction node to express '<var> eid <eid>'"""
+ return self.add_restriction(make_relation(lhsvar, rtype, (rhsvar,),
+ VariableRef))
+
+ def add_eid_restriction(self, var, eid, c_type='Int'):
+ """builds a restriction node to express '<var> eid <eid>'"""
+ assert c_type in ('Int', 'Substitute'), "Error got c_type=%r in eid restriction" % c_type
+ return self.add_constant_restriction(var, 'eid', eid, c_type)
+
+ def add_type_restriction(self, var, etype):
+ """builds a restriction node to express : variable is etype"""
+ typerel = var.stinfo.get('typerel', None)
+ if typerel:
+ if typerel.r_type == 'is':
+ istarget = typerel.children[1].children[0]
+ if isinstance(istarget, Constant):
+ etypes = (istarget.value,)
+ else: # Function (IN)
+ etypes = [et.value for et in istarget.children]
+ if etype not in etypes:
+ raise RQLException('%r not in %r' % (etype, etypes))
+ if len(etypes) > 1:
+ # iterate a copy of children because it's modified inplace
+ for child in istarget.children[:]:
+ if child.value != etype:
+ typerel.stmt.remove_node(child)
+ return typerel
+ else:
+ assert typerel.r_type == 'is_instance_of'
+ typerel.stmt.remove_node(typerel)
+ return self.add_constant_restriction(var, 'is', etype, 'etype')
+
+
+# base RQL nodes ##############################################################
+
+class SubQuery(BaseNode):
+ """WITH clause"""
+ __slots__ = ('aliases', 'query')
+ def __init__(self, aliases=None, query=None):
+ if aliases is not None:
+ self.set_aliases(aliases)
+ if query is not None:
+ self.set_query(query)
+
+ def set_aliases(self, aliases):
+ self.aliases = aliases
+ for node in aliases:
+ node.parent = self
+
+ def set_query(self, node):
+ self.query = node
+ node.parent = self
+
+ def copy(self, stmt):
+ return SubQuery([v.copy(stmt) for v in self.aliases], self.query.copy())
+
+ @property
+ def children(self):
+ return self.aliases + [self.query]
+
+ def as_string(self, encoding=None, kwargs=None):
+ return '%s BEING (%s)' % (','.join(v.name for v in self.aliases),
+ self.query.as_string(encoding, kwargs))
+ def __repr__(self):
+ return '%s BEING (%s)' % (','.join(repr(v) for v in self.aliases),
+ repr(self.query))
+
+class And(BinaryNode):
+ """a logical AND node (binary)"""
+ __slots__ = ()
+
+ def as_string(self, encoding=None, kwargs=None):
+ """return the tree as an encoded rql string"""
+ return '%s, %s' % (self.children[0].as_string(encoding, kwargs),
+ self.children[1].as_string(encoding, kwargs))
+ def __repr__(self):
+ return '%s AND %s' % (repr(self.children[0]), repr(self.children[1]))
+
+ def ored(self, traverse_scope=False, _fromnode=None):
+ return self.parent.ored(traverse_scope, _fromnode or self)
+
+ def neged(self, traverse_scope=False, _fromnode=None):
+ return self.parent.neged(traverse_scope, _fromnode or self)
+
+
+class Or(BinaryNode):
+ """a logical OR node (binary)"""
+ __slots__ = ()
+
+ def as_string(self, encoding=None, kwargs=None):
+ return '(%s) OR (%s)' % (self.children[0].as_string(encoding, kwargs),
+ self.children[1].as_string(encoding, kwargs))
+
+ def __repr__(self):
+ return '%s OR %s' % (repr(self.children[0]), repr(self.children[1]))
+
+ def ored(self, traverse_scope=False, _fromnode=None):
+ return self
+
+ def neged(self, traverse_scope=False, _fromnode=None):
+ return self.parent.neged(traverse_scope, _fromnode or self)
+
+
+class Not(Node):
+ """a logical NOT node (unary)"""
+ __slots__ = ()
+ def __init__(self, expr=None):
+ Node.__init__(self)
+ if expr is not None:
+ self.append(expr)
+
+ def as_string(self, encoding=None, kwargs=None):
+ if isinstance(self.children[0], (Exists, Relation)):
+ return 'NOT %s' % self.children[0].as_string(encoding, kwargs)
+ return 'NOT (%s)' % self.children[0].as_string(encoding, kwargs)
+
+ def __repr__(self, encoding=None, kwargs=None):
+ return 'NOT (%s)' % repr(self.children[0])
+
+ def ored(self, traverse_scope=False, _fromnode=None):
+ # XXX consider traverse_scope ?
+ return self.parent.ored(traverse_scope, _fromnode or self)
+
+ def neged(self, traverse_scope=False, _fromnode=None, strict=False):
+ return self
+
+ def remove(self, child):
+ return self.parent.remove(self)
+
+# def parent_scope_property(attr):
+# def _get_parent_attr(self, attr=attr):
+# return getattr(self.parent.scope, attr)
+# return property(_get_parent_attr)
+# # editable compatibility
+# for method in ('remove_node', 'add_restriction', 'add_constant_restriction',
+# 'add_relation', 'add_eid_restriction', 'add_type_restriction'):
+# setattr(Not, method, parent_scope_property(method))
+
+
+class Exists(EditableMixIn, BaseNode):
+ """EXISTS sub query"""
+ __slots__ = ('query',)
+
+ def __init__(self, restriction=None):
+ if restriction is not None:
+ self.set_where(restriction)
+ else:
+ self.query = None
+
+ def copy(self, stmt):
+ new = self.query.copy(stmt)
+ return Exists(new)
+
+ @property
+ def children(self):
+ return (self.query,)
+
+ def append(self, node):
+ assert self.query is None
+ self.query = node
+ node.parent = self
+
+ def is_equivalent(self, other):
+ raise NotImplementedError
+
+ def as_string(self, encoding=None, kwargs=None):
+ content = self.query and self.query.as_string(encoding, kwargs)
+ return 'EXISTS(%s)' % content
+
+ def __repr__(self):
+ return 'EXISTS(%s)' % repr(self.query)
+
+ def set_where(self, node):
+ self.query = node
+ node.parent = self
+
+ @property
+ def where(self):
+ return self.query
+
+ def replace(self, oldnode, newnode):
+ assert oldnode is self.query
+ self.query = newnode
+ newnode.parent = self
+ return oldnode, self, None
+
+ def remove(self, child):
+ return self.parent.remove(self)
+
+ @property
+ def scope(self):
+ return self
+
+ def ored(self, traverse_scope=False, _fromnode=None):
+ if not traverse_scope:
+ if _fromnode is not None: # stop here
+ return False
+ return self.parent.ored(traverse_scope, self)
+ return self.parent.ored(traverse_scope, _fromnode)
+
+ def neged(self, traverse_scope=False, _fromnode=None, strict=False):
+ if not traverse_scope:
+ if _fromnode is not None: # stop here
+ return False
+ return self.parent.neged(self)
+ elif strict:
+ return isinstance(self.parent, Not)
+ return self.parent.neged(traverse_scope, _fromnode)
+
+
+class Relation(Node):
+ """a RQL relation"""
+ __slots__ = ('r_type', 'optional',
+ '_q_sqltable', '_q_needcast') # XXX cubicweb specific
+
+ def __init__(self, r_type, optional=None):
+ Node.__init__(self)
+ self.r_type = r_type.encode()
+ self.optional = None
+ self.set_optional(optional)
+
+ def initargs(self, stmt):
+ """return list of arguments to give to __init__ to clone this node"""
+ return self.r_type, self.optional
+
+ def is_equivalent(self, other):
+ if not Node.is_equivalent(self, other):
+ return False
+ if self.r_type != other.r_type:
+ return False
+ return True
+
+ def as_string(self, encoding=None, kwargs=None):
+ """return the tree as an encoded rql string"""
+ try:
+ lhs = self.children[0].as_string(encoding, kwargs)
+ if self.optional in ('left', 'both'):
+ lhs += '?'
+ rhs = self.children[1].as_string(encoding, kwargs)
+ if self.optional in ('right', 'both'):
+ rhs += '?'
+ except IndexError:
+ return repr(self) # not fully built relation
+ return '%s %s %s' % (lhs, self.r_type, rhs)
+
+ def __repr__(self):
+ if self.optional:
+ rtype = '%s[%s]' % (self.r_type, self.optional)
+ else:
+ rtype = self.r_type
+ try:
+ return 'Relation(%r %s %r)' % (self.children[0], rtype,
+ self.children[1])
+ except IndexError:
+ return 'Relation(%s)' % self.r_type
+
+ def set_optional(self, optional):
+ assert optional in (None, 'left', 'right')
+ if optional is not None:
+ if self.optional and self.optional != optional:
+ self.optional = 'both'
+ else:
+ self.optional = optional
+
+ def relation(self):
+ """return the parent relation where self occurs or None"""
+ return self
+
+ def ored(self, traverse_scope=False, _fromnode=None):
+ return self.parent.ored(traverse_scope, _fromnode or self)
+
+ def neged(self, traverse_scope=False, _fromnode=None, strict=False):
+ if strict:
+ return isinstance(self.parent, Not)
+ return self.parent.neged(traverse_scope, _fromnode or self)
+
+ def is_types_restriction(self):
+ if self.r_type not in ('is', 'is_instance_of'):
+ return False
+ rhs = self.children[1]
+ if isinstance(rhs, Comparison):
+ rhs = rhs.children[0]
+ # else: relation used in SET OR DELETE selection
+ return ((isinstance(rhs, Constant) and rhs.type == 'etype')
+ or (isinstance(rhs, Function) and rhs.name == 'IN'))
+
+ def operator(self):
+ """return the operator of the relation <, <=, =, >=, > and LIKE
+
+ (relations used in SET, INSERT and DELETE definitions don't have
+ an operator as rhs)
+ """
+ rhs = self.children[1]
+ if isinstance(rhs, Comparison):
+ return rhs.operator
+ return '='
+
+ def get_parts(self):
+ """return the left hand side and the right hand side of this relation
+ """
+ lhs = self.children[0]
+ rhs = self.children[1]
+ return lhs, rhs
+
+ def get_variable_parts(self):
+ """return the left hand side and the right hand side of this relation,
+ ignoring comparison
+ """
+ lhs = self.children[0]
+ rhs = self.children[1].children[0]
+ return lhs, rhs
+
+ def change_optional(self, value):
+ root = self.root
+ if root is not None and root.should_register_op and value != self.optional:
+ from rql.undo import SetOptionalOperation
+ root.undo_manager.add_operation(SetOptionalOperation(self, self.optional))
+ self.set_optional(value)
+
+
+CMP_OPERATORS = frozenset(('=', '!=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE', 'REGEXP'))
+
+class Comparison(HSMixin, Node):
+ """handle comparisons:
+
+ <, <=, =, >=, > LIKE and ILIKE operators have a unique children.
+ """
+ __slots__ = ('operator', 'optional')
+
+ def __init__(self, operator, value=None, optional=None):
+ Node.__init__(self)
+ if operator == '~=':
+ operator = 'ILIKE'
+ assert operator in CMP_OPERATORS, operator
+ self.operator = operator.encode()
+ self.optional = optional
+ if value is not None:
+ self.append(value)
+
+ def initargs(self, stmt):
+ """return list of arguments to give to __init__ to clone this node"""
+ return (self.operator, None, self.optional)
+
+ def set_optional(self, left, right):
+ if left and right:
+ self.optional = 'both'
+ elif left:
+ self.optional = 'left'
+ elif right:
+ self.optional = 'right'
+
+ def is_equivalent(self, other):
+ if not Node.is_equivalent(self, other):
+ return False
+ return self.operator == other.operator
+
+ def as_string(self, encoding=None, kwargs=None):
+ """return the tree as an encoded rql string"""
+ if len(self.children) == 0:
+ return self.operator
+ if len(self.children) == 2:
+ lhsopt = rhsopt = ''
+ if self.optional in ('left', 'both'):
+ lhsopt = '?'
+ if self.optional in ('right', 'both'):
+ rhsopt = '?'
+ return '%s%s %s %s%s' % (self.children[0].as_string(encoding, kwargs),
+ lhsopt, self.operator.encode(),
+ self.children[1].as_string(encoding, kwargs), rhsopt)
+ if self.operator == '=':
+ return self.children[0].as_string(encoding, kwargs)
+ return '%s %s' % (self.operator.encode(),
+ self.children[0].as_string(encoding, kwargs))
+
+ def __repr__(self):
+ return '%s %s' % (self.operator, ', '.join(repr(c) for c in self.children))
+
+
+class MathExpression(OperatorExpressionMixin, HSMixin, BinaryNode):
+ """Mathematical Operators"""
+ __slots__ = ('operator',)
+
+ def __init__(self, operator, lhs=None, rhs=None):
+ BinaryNode.__init__(self, lhs, rhs)
+ self.operator = operator.encode()
+
+ def as_string(self, encoding=None, kwargs=None):
+ """return the tree as an encoded rql string"""
+ return '(%s %s %s)' % (self.children[0].as_string(encoding, kwargs),
+ self.operator.encode(),
+ self.children[1].as_string(encoding, kwargs))
+
+ def __repr__(self):
+ return '(%r %s %r)' % (self.children[0], self.operator,
+ self.children[1])
+
+ def get_type(self, solution=None, kwargs=None):
+ """return the type of object returned by this function if known
+
+ solution is an optional variable/etype mapping
+ """
+ lhstype = self.children[0].get_type(solution, kwargs)
+ rhstype = self.children[1].get_type(solution, kwargs)
+ key = (self.operator, lhstype, rhstype)
+ try:
+ return {('-', 'Date', 'Datetime'): 'Interval',
+ ('-', 'Datetime', 'Datetime'): 'Interval',
+ ('-', 'Date', 'Date'): 'Interval',
+ ('-', 'Date', 'Time'): 'Datetime',
+ ('+', 'Date', 'Time'): 'Datetime',
+ ('-', 'Datetime', 'Time'): 'Datetime',
+ ('+', 'Datetime', 'Time'): 'Datetime',
+ }[key]
+ except KeyError:
+ if lhstype == rhstype:
+ return rhstype
+ if sorted((lhstype, rhstype)) == ['Float', 'Int']:
+ return 'Float'
+ raise CoercionError(key)
+
+
+class UnaryExpression(OperatorExpressionMixin, Node):
+ """Unary Operators"""
+ __slots__ = ('operator',)
+
+ def __init__(self, operator, child=None):
+ Node.__init__(self)
+ self.operator = operator.encode()
+ if child is not None:
+ self.append(child)
+
+ def as_string(self, encoding=None, kwargs=None):
+ """return the tree as an encoded rql string"""
+ return '%s%s' % (self.operator.encode(),
+ self.children[0].as_string(encoding, kwargs))
+
+ def __repr__(self):
+ return '%s%r' % (self.operator, self.children[0])
+
+ def get_type(self, solution=None, kwargs=None):
+ """return the type of object returned by this expression if known
+
+ solution is an optional variable/etype mapping
+ """
+ return self.children[0].get_type(solution, kwargs)
+
+
+class Function(HSMixin, Node):
+ """Class used to deal with aggregat functions (sum, min, max, count, avg)
+ and latter upper(), lower() and other RQL transformations functions
+ """
+ __slots__ = ('name',)
+
+ def __init__(self, name):
+ Node.__init__(self)
+ self.name = name.strip().upper().encode()
+
+ def initargs(self, stmt):
+ """return list of arguments to give to __init__ to clone this node"""
+ return (self.name,)
+
+ def is_equivalent(self, other):
+ if not Node.is_equivalent(self, other):
+ return False
+ return self.name == other.name
+
+ def as_string(self, encoding=None, kwargs=None):
+ """return the tree as an encoded rql string"""
+ return '%s(%s)' % (self.name, ', '.join(c.as_string(encoding, kwargs)
+ for c in self.children))
+
+ def __repr__(self):
+ return '%s(%s)' % (self.name, ', '.join(repr(c) for c in self.children))
+
+ def get_type(self, solution=None, kwargs=None):
+ """return the type of object returned by this function if known
+
+ solution is an optional variable/etype mapping
+ """
+ func_descr = self.descr()
+ rtype = func_descr.rql_return_type(self)
+ if rtype is None:
+ # XXX support one variable ref child
+ try:
+ rtype = solution and solution.get(self.children[0].name)
+ except AttributeError:
+ pass
+ return rtype or 'Any'
+
+ def get_description(self, mainindex, tr):
+ return self.descr().st_description(self, mainindex, tr)
+
+ def descr(self):
+ """return the type of object returned by this function if known"""
+ return function_description(self.name)
+
+
+class Constant(HSMixin, LeafNode):
+ """String, Int, TRUE, FALSE, TODAY, NULL..."""
+ __slots__ = ('value', 'type', 'uid', 'uidtype')
+
+ def __init__(self, value, c_type, _uid=False, _uidtype=None):
+ assert c_type in CONSTANT_TYPES, "Error got c_type="+repr(c_type)
+ LeafNode.__init__(self) # don't care about Node attributes
+ self.value = value
+ self.type = c_type
+ # updated by the annotator/analyzer if necessary
+ self.uid = _uid
+ self.uidtype = _uidtype
+
+ def initargs(self, stmt):
+ """return list of arguments to give to __init__ to clone this node"""
+ return (self.value, self.type, self.uid, self.uidtype)
+
+ def is_equivalent(self, other):
+ if not LeafNode.is_equivalent(self, other):
+ return False
+ return self.type == other.type and self.value == other.value
+
+ def as_string(self, encoding=None, kwargs=None):
+ """return the tree as an encoded rql string (an unicode string is
+ returned if encoding is None)
+ """
+ if self.type is None:
+ return 'NULL'
+ if self.type in ('etype', 'Date', 'Datetime', 'Int', 'Float'):
+ return str(self.value)
+ if self.type == 'Boolean':
+ return self.value and 'TRUE' or 'FALSE'
+ if self.type == 'Substitute':
+ # XXX could get some type information from self.root().schema()
+ # and linked relation
+ if kwargs is not None:
+ value = kwargs.get(self.value, '???')
+ if isinstance(value, unicode):
+ if encoding:
+ value = quote(value.encode(encoding))
+ else:
+ value = uquote(value)
+ elif isinstance(value, str):
+ value = quote(value)
+ else:
+ value = repr(value)
+ return value
+ return '%%(%s)s' % self.value
+ if isinstance(self.value, unicode):
+ if encoding is not None:
+ return quote(self.value.encode(encoding))
+ return uquote(self.value)
+ return repr(self.value)
+
+ def __repr__(self):
+ return self.as_string('utf8')
+
+ def eval(self, kwargs):
+ if self.type == 'Substitute':
+ return kwargs[self.value]
+ if self.type in ('Date', 'Datetime'): # TODAY, NOW
+ return KEYWORD_MAP[self.value]()
+ return self.value
+
+ def get_type(self, solution=None, kwargs=None):
+ if self.uid:
+ return self.uidtype
+ if self.type == 'Substitute':
+ if kwargs is not None:
+ return etype_from_pyobj(self.eval(kwargs))
+ return 'String'
+ return self.type
+
+
+class VariableRef(HSMixin, LeafNode):
+ """a reference to a variable in the syntax tree"""
+ __slots__ = ('variable', 'name')
+
+ def __init__(self, variable, noautoref=None):
+ LeafNode.__init__(self) # don't care about Node attributes
+ self.variable = variable
+ self.name = variable.name
+ if noautoref is None:
+ self.register_reference()
+
+ def initargs(self, stmt):
+ """return list of arguments to give to __init__ to clone this node"""
+ var = self.variable
+ if isinstance(var, ColumnAlias):
+ newvar = stmt.get_variable(self.name, var.colnum)
+ else:
+ newvar = stmt.get_variable(self.name)
+ newvar.init_copy(var)
+ return (newvar,)
+
+ def is_equivalent(self, other):
+ if not LeafNode.is_equivalent(self, other):
+ return False
+ return self.name == other.name
+
+ def as_string(self, encoding=None, kwargs=None):
+ """return the tree as an encoded rql string"""
+ return self.name
+
+ def __repr__(self):
+ return 'VarRef(%r)' % self.variable
+
+ def __cmp__(self, other):
+ return not self.is_equivalent(other)
+
+ def register_reference(self):
+ self.variable.register_reference(self)
+
+ def unregister_reference(self):
+ self.variable.unregister_reference(self)
+
+ def get_type(self, solution=None, kwargs=None):
+ return self.variable.get_type(solution, kwargs)
+
+ def get_description(self, mainindex, tr):
+ return self.variable.get_description(mainindex, tr)
+
+ def root_selection_index(self):
+ """return the index of this variable reference *in the root selection*
+ if it's selected, else None
+ """
+ myidx = self.variable.selected_index()
+ if myidx is None:
+ return None
+ stmt = self.stmt
+ union = stmt.parent
+ if union.parent is None:
+ return myidx
+ # first .parent is the SubQuery node, we want the Select node
+ parentselect = union.parent.parent
+ for ca in parentselect.aliases.itervalues():
+ if ca.query is union and ca.colnum == myidx:
+ caidx = ca.selected_index()
+ if caidx is None:
+ return None
+ return parentselect.selection[caidx].root_selection_index()
+
+
+class SortTerm(Node):
+ """a sort term bind a variable to the boolean <asc>
+ if <asc> ascendant sort
+ else descendant sort
+ """
+ __slots__ = ('asc',)
+
+ def __init__(self, variable, asc=1, copy=None):
+ Node.__init__(self)
+ self.asc = asc
+ if copy is None:
+ self.append(variable)
+
+ def initargs(self, stmt):
+ """return list of arguments to give to __init__ to clone this node"""
+ return (None, self.asc, True)
+
+ def is_equivalent(self, other):
+ if not Node.is_equivalent(self, other):
+ return False
+ return self.asc == other.asc
+
+ def as_string(self, encoding=None, kwargs=None):
+ if self.asc:
+ return '%s' % self.term
+ return '%s DESC' % self.term
+
+ def __repr__(self):
+ if self.asc:
+ return '%r ASC' % self.term
+ return '%r DESC' % self.term
+
+ @property
+ def term(self):
+ return self.children[0]
+
+
+
+###############################################################################
+
+class Referenceable(VisitableMixIn):
+ __slots__ = ('name', 'stinfo', 'stmt')
+
+ def __init__(self, name):
+ self.name = name.strip().encode()
+ # used to collect some global information about the syntax tree
+ self.stinfo = {
+ # link to VariableReference objects in the syntax tree
+ 'references': set(),
+ }
+ # reference to the selection
+ self.stmt = None
+
+ @property
+ def schema(self):
+ return self.stmt.root.schema
+
+ def init_copy(self, old):
+ # should copy variable's possibletypes on copy
+ if not self.stinfo.get('possibletypes'):
+ self.stinfo['possibletypes'] = old.stinfo.get('possibletypes')
+
+ def as_string(self, encoding=None, kwargs=None):
+ """return the tree as an encoded rql string"""
+ return self.name
+
+ def register_reference(self, vref):
+ """add a reference to this variable"""
+ self.stinfo['references'].add(vref)
+
+ def unregister_reference(self, vref):
+ """remove a reference to this variable"""
+ try:
+ self.stinfo['references'].remove(vref)
+ except KeyError:
+ # this may occur on hairy undoing
+ pass
+
+ def references(self):
+ """return all references on this variable"""
+ return tuple(self.stinfo['references'])
+
+ def prepare_annotation(self):
+ self.stinfo.update({
+ 'scope': None,
+ # relations where this variable is used on the lhs/rhs
+ 'relations': set(),
+ 'rhsrelations': set(),
+ # selection indexes if any
+ 'selected': set(),
+ # type restriction (e.g. "is" / "is_instance_of") where this
+ # variable is used on the lhs
+ 'typerel': None,
+ # uid relations (e.g. "eid") where this variable is used on the lhs
+ 'uidrel': None,
+ # if this variable is an attribute variable (ie final entity), link
+ # to the (prefered) attribute owner variable
+ 'attrvar': None,
+ # constant node linked to an uid variable if any
+ 'constnode': None,
+ })
+ # remove optional st infos
+ for key in ('optrelations', 'blocsimplification', 'ftirels'):
+ self.stinfo.pop(key, None)
+
+ def _set_scope(self, key, scopenode):
+ if scopenode is self.stmt or self.stinfo[key] is None:
+ self.stinfo[key] = scopenode
+ elif not (self.stinfo[key] is self.stmt or scopenode is self.stinfo[key]):
+ self.stinfo[key] = common_parent(self.stinfo[key], scopenode).scope
+
+ def set_scope(self, scopenode):
+ self._set_scope('scope', scopenode)
+ def get_scope(self):
+ return self.stinfo['scope']
+ scope = property(get_scope, set_scope)
+
+ def add_optional_relation(self, relation):
+ try:
+ self.stinfo['optrelations'].add(relation)
+ except KeyError:
+ self.stinfo['optrelations'] = set((relation,))
+
+ def get_type(self, solution=None, kwargs=None):
+ """return entity type of this object, 'Any' if not found"""
+ if solution:
+ return solution[self.name]
+ if self.stinfo['typerel']:
+ rhs = self.stinfo['typerel'].children[1].children[0]
+ if isinstance(rhs, Constant):
+ return str(rhs.value)
+ schema = self.schema
+ if schema is not None:
+ for rel in self.stinfo['rhsrelations']:
+ try:
+ lhstype = rel.children[0].get_type(solution, kwargs)
+ return schema.eschema(lhstype).destination(rel.r_type)
+ except: # CoertionError, AssertionError :(
+ pass
+ return 'Any'
+
+ def get_description(self, mainindex, tr, none_allowed=False):
+ """return :
+ * the name of a relation where this variable is used as lhs,
+ * the entity type of this object if specified by a 'is' relation,
+ * 'Any' if nothing nicer has been found...
+
+ give priority to relation name
+ """
+ if mainindex is not None:
+ if mainindex in self.stinfo['selected']:
+ return ', '.join(sorted(
+ tr(etype) for etype in self.stinfo['possibletypes']))
+ rtype = frtype = None
+ schema = self.schema
+ for rel in self.stinfo['relations']:
+ if schema is not None:
+ rschema = schema.rschema(rel.r_type)
+ if rschema.final:
+ if self.name == rel.children[0].name:
+ # ignore final relation where this variable is used as subject
+ continue
+ # final relation where this variable is used as object
+ frtype = rel.r_type
+ rtype = rel.r_type
+ lhs, rhs = rel.get_variable_parts()
+ # use getattr, may not be a variable ref (rewritten, constant...)
+ rhsvar = getattr(rhs, 'variable', None)
+ if mainindex is not None:
+ # relation to the main variable, stop searching
+ lhsvar = getattr(lhs, 'variable', None)
+ context = None
+ if lhsvar is not None and mainindex in lhsvar.stinfo['selected']:
+ if len(lhsvar.stinfo['possibletypes']) == 1:
+ context = next(iter(lhsvar.stinfo['possibletypes']))
+ return tr(rtype, context=context)
+ if rhsvar is not None and mainindex in rhsvar.stinfo['selected']:
+ if len(rhsvar.stinfo['possibletypes']) == 1:
+ context = next(iter(rhsvar.stinfo['possibletypes']))
+ if schema is not None and rschema.symmetric:
+ return tr(rtype, context=context)
+ return tr(rtype + '_object', context=context)
+ if rhsvar is self:
+ rtype += '_object'
+ if frtype is not None:
+ return tr(frtype)
+ if mainindex is None and rtype is not None:
+ return tr(rtype)
+ if none_allowed:
+ return None
+ return ', '.join(sorted(
+ tr(etype) for etype in self.stinfo['possibletypes']))
+
+ def selected_index(self):
+ """return the index of this variable in the selection if it's selected,
+ else None
+ """
+ if not self.stinfo['selected']:
+ return None
+ return next(iter(self.stinfo['selected']))
+
+ def main_relation(self):
+ """Return the relation where this variable is used in the rhs.
+
+ It is useful for cases where this variable is final and we are
+ looking for the entity to which it belongs.
+ """
+ for ref in self.references():
+ rel = ref.relation()
+ if rel is None:
+ continue
+ if rel.r_type != 'is' and self.name != rel.children[0].name:
+ return rel
+ return None
+
+ def valuable_references(self):
+ """return the number of "valuable" references :
+ references is in selection or in a non type (is) relations
+ """
+ stinfo = self.stinfo
+ return len(stinfo['selected']) + len(stinfo['relations'])
+
+
+class ColumnAlias(Referenceable):
+ __slots__ = ('colnum', 'query',
+ '_q_sql', '_q_sqltable') # XXX cubicweb specific
+ def __init__(self, alias, colnum, query=None):
+ super(ColumnAlias, self).__init__(alias)
+ self.colnum = int(colnum)
+ self.query = query
+
+ def __repr__(self):
+ return 'alias %s' % self.name
+
+ def get_type(self, solution=None, kwargs=None):
+ """return entity type of this object, 'Any' if not found"""
+ vtype = super(ColumnAlias, self).get_type(solution, kwargs)
+ if vtype == 'Any':
+ for select in self.query.children:
+ vtype = select.selection[self.colnum].get_type(solution, kwargs)
+ if vtype != 'Any':
+ return vtype
+ return vtype
+
+ def get_description(self, mainindex, tr):
+ """return entity type of this object, 'Any' if not found"""
+ vtype = super(ColumnAlias, self).get_description(mainindex, tr,
+ none_allowed=True)
+ if vtype is None:
+ vtypes = set()
+ for select in self.query.children:
+ vtype = select.selection[self.colnum].get_description(mainindex, tr)
+ if vtype is not None:
+ vtypes.add(vtype)
+ if vtypes:
+ return ', '.join(sorted(vtype for vtype in vtypes))
+ return vtype
+
+
+class Variable(Referenceable):
+ """
+ a variable definition, should not be directly added to the syntax tree (use
+ VariableRef instead)
+
+ collects information about a variable use in a syntax tree
+ """
+ __slots__ = ('_q_invariant', '_q_sql', '_q_sqltable') # XXX ginco specific
+
+ def __repr__(self):
+ return '%s' % self.name
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/parser.g Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,362 @@
+"""yapps input grammar for RQL.
+
+:organization: Logilab
+:copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+
+
+Select statement grammar
+------------------------
+
+query = <squery> | <union>
+
+union = (<squery>) UNION (<squery>) [UNION (<squery>)]*
+
+squery = Any <selection>
+ [GROUPBY <variables>]
+ [ORDERBY <sortterms>]
+ [LIMIT <nb> OFFSET <nb>]
+ [WHERE <restriction>]
+ [HAVING <aggregat restriction>]
+ [WITH <subquery> [,<subquery]*]
+
+subquery = <variables> BEING (<query>)
+
+variables = <variable> [, <variable>]*
+
+
+Abbreviations in this code
+--------------------------
+
+rules:
+* rel -> relation
+* decl -> declaration
+* expr -> expression
+* restr -> restriction
+* var -> variable
+* func -> function
+* const -> constant
+* cmp -> comparison
+
+variables:
+* R -> syntax tree root
+* S -> select node
+* P -> parent node
+
+"""
+%%
+
+parser Hercule:
+
+ ignore: r'\s+'
+ # C-like comments
+ ignore: r'/\*(?:[^*]|\*(?!/))*\*/'
+
+ token DELETE: r'(?i)DELETE'
+ token SET: r'(?i)SET'
+ token INSERT: r'(?i)INSERT'
+ token UNION: r'(?i)UNION'
+ token DISTINCT: r'(?i)DISTINCT'
+ token WITH: r'(?i)WITH'
+ token WHERE: r'(?i)WHERE'
+ token BEING: r'(?i)BEING'
+ token OR: r'(?i)OR'
+ token AND: r'(?i)AND'
+ token NOT: r'(?i)NOT'
+ token GROUPBY: r'(?i)GROUPBY'
+ token HAVING: r'(?i)HAVING'
+ token ORDERBY: r'(?i)ORDERBY'
+ token SORT_ASC: r'(?i)ASC'
+ token SORT_DESC: r'(?i)DESC'
+ token LIMIT: r'(?i)LIMIT'
+ token OFFSET: r'(?i)OFFSET'
+ token DATE: r'(?i)TODAY'
+ token DATETIME: r'(?i)NOW'
+ token TRUE: r'(?i)TRUE'
+ token FALSE: r'(?i)FALSE'
+ token NULL: r'(?i)NULL'
+ token EXISTS: r'(?i)EXISTS'
+ token CMP_OP: r'(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP'
+ token ADD_OP: r'\+|-|\||#'
+ token MUL_OP: r'\*|/|%|&'
+ token POW_OP: r'\^|>>|<<'
+ token UNARY_OP: r'-|~'
+ token FUNCTION: r'[A-Za-z_]+\s*(?=\()'
+ token R_TYPE: r'[a-z_][a-z0-9_]*'
+ token E_TYPE: r'[A-Z][A-Za-z0-9]*[a-z]+[A-Z0-9]*'
+ token VARIABLE: r'[A-Z][A-Z0-9_]*'
+ token COLALIAS: r'[A-Z][A-Z0-9_]*\.\d+'
+ token QMARK: r'\?'
+
+ token STRING: r"'([^\'\\]|\\.)*'|\"([^\\\"\\]|\\.)*\""
+ token FLOAT: r'-?\d+\.\d*'
+ token INT: r'-?\d+'
+ token SUBSTITUTE: r'%\([A-Za-z_0-9]+\)s'
+
+
+#// Grammar entry ###############################################################
+
+
+rule goal: DELETE _delete<<Delete()>> ';' {{ return _delete }}
+
+ | INSERT _insert<<Insert()>> ';' {{ return _insert }}
+
+ | SET update<<Set()>> ';' {{ return update }}
+
+ | union<<Union()>> ';' {{ return union }}
+
+#// Deletion ###################################################################
+
+rule _delete<<R>>: decl_rels<<R>> where<<R>> having<<R>> {{ return R }}
+
+ | decl_vars<<R>> where<<R>> having<<R>> {{ return R }}
+
+
+#// Insertion ##################################################################
+
+rule _insert<<R>>: decl_vars<<R>> insert_rels<<R>> {{ return R }}
+
+
+rule insert_rels<<R>>: ":" decl_rels<<R>> where<<R>> having<<R>> {{ return R }}
+
+ |
+
+
+#// Update #####################################################################
+
+rule update<<R>>: decl_rels<<R>> where<<R>> having<<R>> {{ return R }}
+
+
+#// Selection ##################################################################
+
+rule union<<R>>: select<<Select()>> {{ R.append(select); return R }}
+
+ | r"\(" select<<Select()>> r"\)" {{ R.append(select) }}
+ ( UNION
+ r"\(" select<<Select()>> r"\)" {{ R.append(select) }}
+ )* {{ return R }}
+
+rule select<<S>>: DISTINCT select_<<S>> {{ S.distinct = True ; return S }}
+ | select_<<S>> {{ return S }}
+
+rule select_<<S>>: E_TYPE selection<<S>>
+ groupby<<S>>
+ orderby<<S>>
+ limit_offset<<S>>
+ where<<S>>
+ having<<S>>
+ with_<<S>> {{ S.set_statement_type(E_TYPE); return S }}
+
+rule selection<<S>>: expr_add<<S>> {{ S.append_selected(expr_add) }}
+ ( ',' expr_add<<S>> {{ S.append_selected(expr_add) }}
+ )*
+
+
+
+#// other clauses (groupby, orderby, with, having) ##############################
+
+rule groupby<<S>>: GROUPBY {{ nodes = [] }}
+ expr_add<<S>> {{ nodes.append(expr_add) }}
+ ( ',' expr_add<<S>> {{ nodes.append(expr_add) }}
+ )* {{ S.set_groupby(nodes); return True }}
+ |
+
+rule having<<S>>: HAVING logical_expr<<S>> {{ S.set_having([logical_expr]) }}
+ |
+
+rule orderby<<S>>: ORDERBY {{ nodes = [] }}
+ sort_term<<S>> {{ nodes.append(sort_term) }}
+ ( ',' sort_term<<S>> {{ nodes.append(sort_term) }}
+ )* {{ S.set_orderby(nodes); return True }}
+ |
+
+rule with_<<S>>: WITH {{ nodes = [] }}
+ subquery<<S>> {{ nodes.append(subquery) }}
+ ( ',' subquery<<S>> {{ nodes.append(subquery) }}
+ )* {{ S.set_with(nodes) }}
+ |
+
+rule subquery<<S>>: variables<<S>> {{ node = SubQuery() ; node.set_aliases(variables) }}
+ BEING r"\(" union<<Union()>> r"\)" {{ node.set_query(union); return node }}
+
+
+rule sort_term<<S>>: expr_add<<S>> sort_meth {{ return SortTerm(expr_add, sort_meth) }}
+
+
+rule sort_meth: SORT_DESC {{ return 0 }}
+
+ | SORT_ASC {{ return 1 }}
+
+ | {{ return 1 # default to SORT_ASC }}
+
+
+#// Limit and offset ############################################################
+
+rule limit_offset<<R>> : limit<<R>> offset<<R>> {{ return limit or offset }}
+
+rule limit<<R>> : LIMIT INT {{ R.set_limit(int(INT)); return True }}
+ |
+
+rule offset<<R>> : OFFSET INT {{ R.set_offset(int(INT)); return True }}
+ |
+
+
+#// Restriction statements ######################################################
+
+rule where<<S>>: WHERE restriction<<S>> {{ S.set_where(restriction) }}
+ |
+
+rule restriction<<S>>: rels_or<<S>> {{ node = rels_or }}
+ ( ',' rels_or<<S>> {{ node = And(node, rels_or) }}
+ )* {{ return node }}
+
+rule rels_or<<S>>: rels_and<<S>> {{ node = rels_and }}
+ ( OR rels_and<<S>> {{ node = Or(node, rels_and) }}
+ )* {{ return node }}
+
+rule rels_and<<S>>: rels_not<<S>> {{ node = rels_not }}
+ ( AND rels_not<<S>> {{ node = And(node, rels_not) }}
+ )* {{ return node }}
+
+rule rels_not<<S>>: NOT rel<<S>> {{ return Not(rel) }}
+ | rel<<S>> {{ return rel }}
+
+rule rel<<S>>: rel_base<<S>> {{ return rel_base }}
+ | r"\(" restriction<<S>> r"\)" {{ return restriction }}
+
+
+rule rel_base<<S>>: var<<S>> opt_left<<S>> rtype {{ rtype.append(var) ; rtype.set_optional(opt_left) }}
+ expr<<S>> opt_right<<S>> {{ rtype.append(expr) ; rtype.set_optional(opt_right) ; return rtype }}
+ | EXISTS r"\(" restriction<<S>> r"\)" {{ return Exists(restriction) }}
+
+rule rtype: R_TYPE {{ return Relation(R_TYPE) }}
+
+rule opt_left<<S>>: QMARK {{ return 'left' }}
+ |
+rule opt_right<<S>>: QMARK {{ return 'right' }}
+ |
+
+#// restriction expressions ####################################################
+
+rule logical_expr<<S>>: exprs_or<<S>> {{ node = exprs_or }}
+ ( ',' exprs_or<<S>> {{ node = And(node, exprs_or) }}
+ )* {{ return node }}
+
+rule exprs_or<<S>>: exprs_and<<S>> {{ node = exprs_and }}
+ ( OR exprs_and<<S>> {{ node = Or(node, exprs_and) }}
+ )* {{ return node }}
+
+rule exprs_and<<S>>: exprs_not<<S>> {{ node = exprs_not }}
+ ( AND exprs_not<<S>> {{ node = And(node, exprs_not) }}
+ )* {{ return node }}
+
+rule exprs_not<<S>>: NOT balanced_expr<<S>> {{ return Not(balanced_expr) }}
+ | balanced_expr<<S>> {{ return balanced_expr }}
+
+#// XXX ambiguity, expr_add may also have '(' as first token. Hence
+#// put "(" logical_expr<<S>> ")" rule first. We can then parse:
+#//
+#// Any T2 WHERE T1 relation T2 HAVING (1 < COUNT(T1));
+#//
+#// but not
+#//
+#// Any T2 WHERE T1 relation T2 HAVING (1+2) < COUNT(T1);
+rule balanced_expr<<S>>: r"\(" logical_expr<<S>> r"\)" {{ return logical_expr }}
+ | expr_add<<S>> opt_left<<S>>
+ expr_op<<S>> opt_right<<S>> {{ expr_op.insert(0, expr_add); expr_op.set_optional(opt_left, opt_right); return expr_op }}
+
+# // cant use expr<<S>> without introducing some ambiguities
+rule expr_op<<S>>: CMP_OP expr_add<<S>> {{ return Comparison(CMP_OP.upper(), expr_add) }}
+ | in_expr<<S>> {{ return Comparison('=', in_expr) }}
+
+
+#// common statements ###########################################################
+
+rule variables<<S>>: {{ vars = [] }}
+ var<<S>> {{ vars.append(var) }}
+ ( ',' var<<S>> {{ vars.append(var) }}
+ )* {{ return vars }}
+
+rule decl_vars<<R>>: E_TYPE var<<R>> ( {{ R.add_main_variable(E_TYPE, var) }}
+ ',' E_TYPE var<<R>>)* {{ R.add_main_variable(E_TYPE, var) }}
+
+
+rule decl_rels<<R>>: simple_rel<<R>> ( {{ R.add_main_relation(simple_rel) }}
+ ',' simple_rel<<R>>)* {{ R.add_main_relation(simple_rel) }}
+
+
+rule simple_rel<<R>>: var<<R>> R_TYPE {{ e = Relation(R_TYPE) ; e.append(var) }}
+ expr_add<<R>> {{ e.append(Comparison('=', expr_add)) ; return e }}
+
+
+rule expr<<S>>: CMP_OP expr_add<<S>> {{ return Comparison(CMP_OP.upper(), expr_add) }}
+
+ | expr_add<<S>> {{ return Comparison('=', expr_add) }}
+
+
+rule expr_add<<S>>: expr_mul<<S>> {{ node = expr_mul }}
+ ( ADD_OP expr_mul<<S>> {{ node = MathExpression( ADD_OP, node, expr_mul ) }}
+ )* {{ return node }}
+ | UNARY_OP expr_mul<<S>> {{ node = UnaryExpression( UNARY_OP, expr_mul ) }}
+ ( ADD_OP expr_mul<<S>> {{ node = MathExpression( ADD_OP, node, expr_mul ) }}
+ )* {{ return node }}
+
+
+rule expr_mul<<S>>: expr_pow<<S>> {{ node = expr_pow }}
+ ( MUL_OP expr_pow<<S>> {{ node = MathExpression( MUL_OP, node, expr_pow) }}
+ )* {{ return node }}
+
+rule expr_pow<<S>>: expr_base<<S>> {{ node = expr_base }}
+ ( POW_OP expr_base<<S>> {{ node = MathExpression( MUL_OP, node, expr_base) }}
+ )* {{ return node }}
+
+
+rule expr_base<<S>>: const {{ return const }}
+ | var<<S>> {{ return var }}
+ | etype<<S>> {{ return etype }}
+ | func<<S>> {{ return func }}
+ | r"\(" expr_add<<S>> r"\)" {{ return expr_add }}
+
+
+rule func<<S>>: FUNCTION r"\(" {{ F = Function(FUNCTION) }}
+ ( expr_add<<S>> ( {{ F.append(expr_add) }}
+ ',' expr_add<<S>>
+ )* {{ F.append(expr_add) }}
+ )?
+ r"\)" {{ return F }}
+
+rule in_expr<<S>>: 'IN' r"\(" {{ F = Function('IN') }}
+ ( expr_add<<S>> ( {{ F.append(expr_add) }}
+ ',' expr_add<<S>>
+ )* {{ F.append(expr_add) }}
+ )?
+ r"\)" {{ return F }}
+
+
+rule var<<S>>: VARIABLE {{ return VariableRef(S.get_variable(VARIABLE)) }}
+
+rule etype<<S>>: E_TYPE {{ return S.get_etype(E_TYPE) }}
+
+
+rule const: NULL {{ return Constant(None, None) }}
+ | DATE {{ return Constant(DATE.upper(), 'Date') }}
+ | DATETIME {{ return Constant(DATETIME.upper(), 'Datetime') }}
+ | TRUE {{ return Constant(True, 'Boolean') }}
+ | FALSE {{ return Constant(False, 'Boolean') }}
+ | FLOAT {{ return Constant(float(FLOAT), 'Float') }}
+ | INT {{ return Constant(int(INT), 'Int') }}
+ | STRING {{ return Constant(unquote(STRING), 'String') }}
+ | SUBSTITUTE {{ return Constant(SUBSTITUTE[2:-2], 'Substitute') }}
+%%
+
+from warnings import warn
+from rql.stmts import Union, Select, Delete, Insert, Set
+from rql.nodes import *
+
+
+def unquote(string):
+ """Remove quotes from a string."""
+ if string.startswith('"'):
+ return string[1:-1].replace('\\\\', '\\').replace('\\"', '"')
+ elif string.startswith("'"):
+ return string[1:-1].replace('\\\\', '\\').replace("\\'", "'")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/parser.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,708 @@
+"""yapps input grammar for RQL.
+
+:organization: Logilab
+:copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+
+
+Select statement grammar
+------------------------
+
+query = <squery> | <union>
+
+union = (<squery>) UNION (<squery>) [UNION (<squery>)]*
+
+squery = Any <selection>
+ [GROUPBY <variables>]
+ [ORDERBY <sortterms>]
+ [LIMIT <nb> OFFSET <nb>]
+ [WHERE <restriction>]
+ [HAVING <aggregat restriction>]
+ [WITH <subquery> [,<subquery]*]
+
+subquery = <variables> BEING (<query>)
+
+variables = <variable> [, <variable>]*
+
+
+Abbreviations in this code
+--------------------------
+
+rules:
+* rel -> relation
+* decl -> declaration
+* expr -> expression
+* restr -> restriction
+* var -> variable
+* func -> function
+* const -> constant
+* cmp -> comparison
+
+variables:
+* R -> syntax tree root
+* S -> select node
+* P -> parent node
+
+"""
+
+# Begin -- grammar generated by Yapps
+from __future__ import print_function
+import sys, re
+from yapps import runtime
+
+class HerculeScanner(runtime.Scanner):
+ patterns = [
+ ("'IN'", re.compile('IN')),
+ ("','", re.compile(',')),
+ ('r"\\)"', re.compile('\\)')),
+ ('r"\\("', re.compile('\\(')),
+ ('":"', re.compile(':')),
+ ("';'", re.compile(';')),
+ ('\\s+', re.compile('\\s+')),
+ ('/\\*(?:[^*]|\\*(?!/))*\\*/', re.compile('/\\*(?:[^*]|\\*(?!/))*\\*/')),
+ ('DELETE', re.compile('(?i)DELETE')),
+ ('SET', re.compile('(?i)SET')),
+ ('INSERT', re.compile('(?i)INSERT')),
+ ('UNION', re.compile('(?i)UNION')),
+ ('DISTINCT', re.compile('(?i)DISTINCT')),
+ ('WITH', re.compile('(?i)WITH')),
+ ('WHERE', re.compile('(?i)WHERE')),
+ ('BEING', re.compile('(?i)BEING')),
+ ('OR', re.compile('(?i)OR')),
+ ('AND', re.compile('(?i)AND')),
+ ('NOT', re.compile('(?i)NOT')),
+ ('GROUPBY', re.compile('(?i)GROUPBY')),
+ ('HAVING', re.compile('(?i)HAVING')),
+ ('ORDERBY', re.compile('(?i)ORDERBY')),
+ ('SORT_ASC', re.compile('(?i)ASC')),
+ ('SORT_DESC', re.compile('(?i)DESC')),
+ ('LIMIT', re.compile('(?i)LIMIT')),
+ ('OFFSET', re.compile('(?i)OFFSET')),
+ ('DATE', re.compile('(?i)TODAY')),
+ ('DATETIME', re.compile('(?i)NOW')),
+ ('TRUE', re.compile('(?i)TRUE')),
+ ('FALSE', re.compile('(?i)FALSE')),
+ ('NULL', re.compile('(?i)NULL')),
+ ('EXISTS', re.compile('(?i)EXISTS')),
+ ('CMP_OP', re.compile('(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP')),
+ ('ADD_OP', re.compile('\\+|-|\\||#')),
+ ('MUL_OP', re.compile('\\*|/|%|&')),
+ ('POW_OP', re.compile('\\^|>>|<<')),
+ ('UNARY_OP', re.compile('-|~')),
+ ('FUNCTION', re.compile('[A-Za-z_]+\\s*(?=\\()')),
+ ('R_TYPE', re.compile('[a-z_][a-z0-9_]*')),
+ ('E_TYPE', re.compile('[A-Z][A-Za-z0-9]*[a-z]+[A-Z0-9]*')),
+ ('VARIABLE', re.compile('[A-Z][A-Z0-9_]*')),
+ ('COLALIAS', re.compile('[A-Z][A-Z0-9_]*\\.\\d+')),
+ ('QMARK', re.compile('\\?')),
+ ('STRING', re.compile('\'([^\\\'\\\\]|\\\\.)*\'|\\"([^\\\\\\"\\\\]|\\\\.)*\\"')),
+ ('FLOAT', re.compile('-?\\d+\\.\\d*')),
+ ('INT', re.compile('-?\\d+')),
+ ('SUBSTITUTE', re.compile('%\\([A-Za-z_0-9]+\\)s')),
+ ]
+ def __init__(self, str,*args,**kw):
+ runtime.Scanner.__init__(self,None,{'\\s+':None,'/\\*(?:[^*]|\\*(?!/))*\\*/':None,},str,*args,**kw)
+
+class Hercule(runtime.Parser):
+ Context = runtime.Context
+ def goal(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'goal', [])
+ _token = self._peek('DELETE', 'INSERT', 'SET', 'r"\\("', 'DISTINCT', 'E_TYPE', context=_context)
+ if _token == 'DELETE':
+ DELETE = self._scan('DELETE', context=_context)
+ _delete = self._delete(Delete(), _context)
+ self._scan("';'", context=_context)
+ return _delete
+ elif _token == 'INSERT':
+ INSERT = self._scan('INSERT', context=_context)
+ _insert = self._insert(Insert(), _context)
+ self._scan("';'", context=_context)
+ return _insert
+ elif _token == 'SET':
+ SET = self._scan('SET', context=_context)
+ update = self.update(Set(), _context)
+ self._scan("';'", context=_context)
+ return update
+ else: # in ['r"\\("', 'DISTINCT', 'E_TYPE']
+ union = self.union(Union(), _context)
+ self._scan("';'", context=_context)
+ return union
+
+ def _delete(self, R, _parent=None):
+ _context = self.Context(_parent, self._scanner, '_delete', [R])
+ _token = self._peek('E_TYPE', 'VARIABLE', context=_context)
+ if _token == 'VARIABLE':
+ decl_rels = self.decl_rels(R, _context)
+ where = self.where(R, _context)
+ having = self.having(R, _context)
+ return R
+ else: # == 'E_TYPE'
+ decl_vars = self.decl_vars(R, _context)
+ where = self.where(R, _context)
+ having = self.having(R, _context)
+ return R
+
+ def _insert(self, R, _parent=None):
+ _context = self.Context(_parent, self._scanner, '_insert', [R])
+ decl_vars = self.decl_vars(R, _context)
+ insert_rels = self.insert_rels(R, _context)
+ return R
+
+ def insert_rels(self, R, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'insert_rels', [R])
+ _token = self._peek('":"', "';'", context=_context)
+ if _token == '":"':
+ self._scan('":"', context=_context)
+ decl_rels = self.decl_rels(R, _context)
+ where = self.where(R, _context)
+ having = self.having(R, _context)
+ return R
+ else: # == "';'"
+ pass
+
+ def update(self, R, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'update', [R])
+ decl_rels = self.decl_rels(R, _context)
+ where = self.where(R, _context)
+ having = self.having(R, _context)
+ return R
+
+ def union(self, R, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'union', [R])
+ _token = self._peek('r"\\("', 'DISTINCT', 'E_TYPE', context=_context)
+ if _token != 'r"\\("':
+ select = self.select(Select(), _context)
+ R.append(select); return R
+ else: # == 'r"\\("'
+ self._scan('r"\\("', context=_context)
+ select = self.select(Select(), _context)
+ self._scan('r"\\)"', context=_context)
+ R.append(select)
+ while self._peek('UNION', "';'", 'r"\\)"', context=_context) == 'UNION':
+ UNION = self._scan('UNION', context=_context)
+ self._scan('r"\\("', context=_context)
+ select = self.select(Select(), _context)
+ self._scan('r"\\)"', context=_context)
+ R.append(select)
+ return R
+
+ def select(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'select', [S])
+ _token = self._peek('DISTINCT', 'E_TYPE', context=_context)
+ if _token == 'DISTINCT':
+ DISTINCT = self._scan('DISTINCT', context=_context)
+ select_ = self.select_(S, _context)
+ S.distinct = True ; return S
+ else: # == 'E_TYPE'
+ select_ = self.select_(S, _context)
+ return S
+
+ def select_(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'select_', [S])
+ E_TYPE = self._scan('E_TYPE', context=_context)
+ selection = self.selection(S, _context)
+ groupby = self.groupby(S, _context)
+ orderby = self.orderby(S, _context)
+ limit_offset = self.limit_offset(S, _context)
+ where = self.where(S, _context)
+ having = self.having(S, _context)
+ with_ = self.with_(S, _context)
+ S.set_statement_type(E_TYPE); return S
+
+ def selection(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'selection', [S])
+ expr_add = self.expr_add(S, _context)
+ S.append_selected(expr_add)
+ while self._peek("','", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
+ self._scan("','", context=_context)
+ expr_add = self.expr_add(S, _context)
+ S.append_selected(expr_add)
+
+ def groupby(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'groupby', [S])
+ _token = self._peek('GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
+ if _token == 'GROUPBY':
+ GROUPBY = self._scan('GROUPBY', context=_context)
+ nodes = []
+ expr_add = self.expr_add(S, _context)
+ nodes.append(expr_add)
+ while self._peek("','", 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
+ self._scan("','", context=_context)
+ expr_add = self.expr_add(S, _context)
+ nodes.append(expr_add)
+ S.set_groupby(nodes); return True
+ else:
+ pass
+
+ def having(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'having', [S])
+ _token = self._peek('HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
+ if _token == 'HAVING':
+ HAVING = self._scan('HAVING', context=_context)
+ logical_expr = self.logical_expr(S, _context)
+ S.set_having([logical_expr])
+ else: # in ['WITH', "';'", 'r"\\)"']
+ pass
+
+ def orderby(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'orderby', [S])
+ _token = self._peek('ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
+ if _token == 'ORDERBY':
+ ORDERBY = self._scan('ORDERBY', context=_context)
+ nodes = []
+ sort_term = self.sort_term(S, _context)
+ nodes.append(sort_term)
+ while self._peek("','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
+ self._scan("','", context=_context)
+ sort_term = self.sort_term(S, _context)
+ nodes.append(sort_term)
+ S.set_orderby(nodes); return True
+ else:
+ pass
+
+ def with_(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'with_', [S])
+ _token = self._peek('WITH', 'r"\\)"', "';'", context=_context)
+ if _token == 'WITH':
+ WITH = self._scan('WITH', context=_context)
+ nodes = []
+ subquery = self.subquery(S, _context)
+ nodes.append(subquery)
+ while self._peek("','", 'r"\\)"', "';'", context=_context) == "','":
+ self._scan("','", context=_context)
+ subquery = self.subquery(S, _context)
+ nodes.append(subquery)
+ S.set_with(nodes)
+ else: # in ['r"\\)"', "';'"]
+ pass
+
+ def subquery(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'subquery', [S])
+ variables = self.variables(S, _context)
+ node = SubQuery() ; node.set_aliases(variables)
+ BEING = self._scan('BEING', context=_context)
+ self._scan('r"\\("', context=_context)
+ union = self.union(Union(), _context)
+ self._scan('r"\\)"', context=_context)
+ node.set_query(union); return node
+
+ def sort_term(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'sort_term', [S])
+ expr_add = self.expr_add(S, _context)
+ sort_meth = self.sort_meth(_context)
+ return SortTerm(expr_add, sort_meth)
+
+ def sort_meth(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'sort_meth', [])
+ _token = self._peek('SORT_DESC', 'SORT_ASC', "','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
+ if _token == 'SORT_DESC':
+ SORT_DESC = self._scan('SORT_DESC', context=_context)
+ return 0
+ elif _token == 'SORT_ASC':
+ SORT_ASC = self._scan('SORT_ASC', context=_context)
+ return 1
+ else:
+ return 1 # default to SORT_ASC
+
+ def limit_offset(self, R, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'limit_offset', [R])
+ limit = self.limit(R, _context)
+ offset = self.offset(R, _context)
+ return limit or offset
+
+ def limit(self, R, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'limit', [R])
+ _token = self._peek('LIMIT', 'OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
+ if _token == 'LIMIT':
+ LIMIT = self._scan('LIMIT', context=_context)
+ INT = self._scan('INT', context=_context)
+ R.set_limit(int(INT)); return True
+ else: # in ['OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"']
+ pass
+
+ def offset(self, R, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'offset', [R])
+ _token = self._peek('OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
+ if _token == 'OFFSET':
+ OFFSET = self._scan('OFFSET', context=_context)
+ INT = self._scan('INT', context=_context)
+ R.set_offset(int(INT)); return True
+ else: # in ['WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"']
+ pass
+
+ def where(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'where', [S])
+ _token = self._peek('WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
+ if _token == 'WHERE':
+ WHERE = self._scan('WHERE', context=_context)
+ restriction = self.restriction(S, _context)
+ S.set_where(restriction)
+ else: # in ['HAVING', 'WITH', "';'", 'r"\\)"']
+ pass
+
+ def restriction(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'restriction', [S])
+ rels_or = self.rels_or(S, _context)
+ node = rels_or
+ while self._peek("','", 'r"\\)"', 'HAVING', 'WITH', "';'", context=_context) == "','":
+ self._scan("','", context=_context)
+ rels_or = self.rels_or(S, _context)
+ node = And(node, rels_or)
+ return node
+
+ def rels_or(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'rels_or', [S])
+ rels_and = self.rels_and(S, _context)
+ node = rels_and
+ while self._peek('OR', "','", 'r"\\)"', 'HAVING', 'WITH', "';'", context=_context) == 'OR':
+ OR = self._scan('OR', context=_context)
+ rels_and = self.rels_and(S, _context)
+ node = Or(node, rels_and)
+ return node
+
+ def rels_and(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'rels_and', [S])
+ rels_not = self.rels_not(S, _context)
+ node = rels_not
+ while self._peek('AND', 'OR', "','", 'r"\\)"', 'HAVING', 'WITH', "';'", context=_context) == 'AND':
+ AND = self._scan('AND', context=_context)
+ rels_not = self.rels_not(S, _context)
+ node = And(node, rels_not)
+ return node
+
+ def rels_not(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'rels_not', [S])
+ _token = self._peek('NOT', 'r"\\("', 'EXISTS', 'VARIABLE', context=_context)
+ if _token == 'NOT':
+ NOT = self._scan('NOT', context=_context)
+ rel = self.rel(S, _context)
+ return Not(rel)
+ else: # in ['r"\\("', 'EXISTS', 'VARIABLE']
+ rel = self.rel(S, _context)
+ return rel
+
+ def rel(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'rel', [S])
+ _token = self._peek('r"\\("', 'EXISTS', 'VARIABLE', context=_context)
+ if _token != 'r"\\("':
+ rel_base = self.rel_base(S, _context)
+ return rel_base
+ else: # == 'r"\\("'
+ self._scan('r"\\("', context=_context)
+ restriction = self.restriction(S, _context)
+ self._scan('r"\\)"', context=_context)
+ return restriction
+
+ def rel_base(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'rel_base', [S])
+ _token = self._peek('EXISTS', 'VARIABLE', context=_context)
+ if _token == 'VARIABLE':
+ var = self.var(S, _context)
+ opt_left = self.opt_left(S, _context)
+ rtype = self.rtype(_context)
+ rtype.append(var) ; rtype.set_optional(opt_left)
+ expr = self.expr(S, _context)
+ opt_right = self.opt_right(S, _context)
+ rtype.append(expr) ; rtype.set_optional(opt_right) ; return rtype
+ else: # == 'EXISTS'
+ EXISTS = self._scan('EXISTS', context=_context)
+ self._scan('r"\\("', context=_context)
+ restriction = self.restriction(S, _context)
+ self._scan('r"\\)"', context=_context)
+ return Exists(restriction)
+
+ def rtype(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'rtype', [])
+ R_TYPE = self._scan('R_TYPE', context=_context)
+ return Relation(R_TYPE)
+
+ def opt_left(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'opt_left', [S])
+ _token = self._peek('QMARK', 'R_TYPE', 'CMP_OP', "'IN'", context=_context)
+ if _token == 'QMARK':
+ QMARK = self._scan('QMARK', context=_context)
+ return 'left'
+ else: # in ['R_TYPE', 'CMP_OP', "'IN'"]
+ pass
+
+ def opt_right(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'opt_right', [S])
+ _token = self._peek('QMARK', 'AND', 'OR', "','", 'r"\\)"', 'WITH', "';'", 'HAVING', context=_context)
+ if _token == 'QMARK':
+ QMARK = self._scan('QMARK', context=_context)
+ return 'right'
+ else: # in ['AND', 'OR', "','", 'r"\\)"', 'WITH', "';'", 'HAVING']
+ pass
+
+ def logical_expr(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'logical_expr', [S])
+ exprs_or = self.exprs_or(S, _context)
+ node = exprs_or
+ while self._peek("','", 'r"\\)"', 'WITH', "';'", context=_context) == "','":
+ self._scan("','", context=_context)
+ exprs_or = self.exprs_or(S, _context)
+ node = And(node, exprs_or)
+ return node
+
+ def exprs_or(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'exprs_or', [S])
+ exprs_and = self.exprs_and(S, _context)
+ node = exprs_and
+ while self._peek('OR', "','", 'r"\\)"', 'WITH', "';'", context=_context) == 'OR':
+ OR = self._scan('OR', context=_context)
+ exprs_and = self.exprs_and(S, _context)
+ node = Or(node, exprs_and)
+ return node
+
+ def exprs_and(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'exprs_and', [S])
+ exprs_not = self.exprs_not(S, _context)
+ node = exprs_not
+ while self._peek('AND', 'OR', "','", 'r"\\)"', 'WITH', "';'", context=_context) == 'AND':
+ AND = self._scan('AND', context=_context)
+ exprs_not = self.exprs_not(S, _context)
+ node = And(node, exprs_not)
+ return node
+
+ def exprs_not(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'exprs_not', [S])
+ _token = self._peek('NOT', 'r"\\("', 'UNARY_OP', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+ if _token == 'NOT':
+ NOT = self._scan('NOT', context=_context)
+ balanced_expr = self.balanced_expr(S, _context)
+ return Not(balanced_expr)
+ else:
+ balanced_expr = self.balanced_expr(S, _context)
+ return balanced_expr
+
+ def balanced_expr(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'balanced_expr', [S])
+ _token = self._peek('r"\\("', 'UNARY_OP', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+ if _token == 'r"\\("':
+ self._scan('r"\\("', context=_context)
+ logical_expr = self.logical_expr(S, _context)
+ self._scan('r"\\)"', context=_context)
+ return logical_expr
+ elif 1:
+ expr_add = self.expr_add(S, _context)
+ opt_left = self.opt_left(S, _context)
+ expr_op = self.expr_op(S, _context)
+ opt_right = self.opt_right(S, _context)
+ expr_op.insert(0, expr_add); expr_op.set_optional(opt_left, opt_right); return expr_op
+ else:
+ raise runtime.SyntaxError(_token[0], 'Could not match balanced_expr')
+
+ def expr_op(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'expr_op', [S])
+ _token = self._peek('CMP_OP', "'IN'", context=_context)
+ if _token == 'CMP_OP':
+ CMP_OP = self._scan('CMP_OP', context=_context)
+ expr_add = self.expr_add(S, _context)
+ return Comparison(CMP_OP.upper(), expr_add)
+ else: # == "'IN'"
+ in_expr = self.in_expr(S, _context)
+ return Comparison('=', in_expr)
+
+ def variables(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'variables', [S])
+ vars = []
+ var = self.var(S, _context)
+ vars.append(var)
+ while self._peek("','", 'BEING', context=_context) == "','":
+ self._scan("','", context=_context)
+ var = self.var(S, _context)
+ vars.append(var)
+ return vars
+
+ def decl_vars(self, R, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'decl_vars', [R])
+ E_TYPE = self._scan('E_TYPE', context=_context)
+ var = self.var(R, _context)
+ while self._peek("','", 'R_TYPE', 'QMARK', 'WHERE', '":"', 'CMP_OP', 'HAVING', "'IN'", "';'", 'POW_OP', 'BEING', 'WITH', 'MUL_OP', 'r"\\)"', 'ADD_OP', 'SORT_DESC', 'SORT_ASC', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'AND', 'OR', context=_context) == "','":
+ R.add_main_variable(E_TYPE, var)
+ self._scan("','", context=_context)
+ E_TYPE = self._scan('E_TYPE', context=_context)
+ var = self.var(R, _context)
+ R.add_main_variable(E_TYPE, var)
+
+ def decl_rels(self, R, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'decl_rels', [R])
+ simple_rel = self.simple_rel(R, _context)
+ while self._peek("','", 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
+ R.add_main_relation(simple_rel)
+ self._scan("','", context=_context)
+ simple_rel = self.simple_rel(R, _context)
+ R.add_main_relation(simple_rel)
+
+ def simple_rel(self, R, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'simple_rel', [R])
+ var = self.var(R, _context)
+ R_TYPE = self._scan('R_TYPE', context=_context)
+ e = Relation(R_TYPE) ; e.append(var)
+ expr_add = self.expr_add(R, _context)
+ e.append(Comparison('=', expr_add)) ; return e
+
+ def expr(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'expr', [S])
+ _token = self._peek('CMP_OP', 'UNARY_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+ if _token == 'CMP_OP':
+ CMP_OP = self._scan('CMP_OP', context=_context)
+ expr_add = self.expr_add(S, _context)
+ return Comparison(CMP_OP.upper(), expr_add)
+ else:
+ expr_add = self.expr_add(S, _context)
+ return Comparison('=', expr_add)
+
+ def expr_add(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'expr_add', [S])
+ _token = self._peek('UNARY_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+ if _token != 'UNARY_OP':
+ expr_mul = self.expr_mul(S, _context)
+ node = expr_mul
+ while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'ADD_OP':
+ ADD_OP = self._scan('ADD_OP', context=_context)
+ expr_mul = self.expr_mul(S, _context)
+ node = MathExpression( ADD_OP, node, expr_mul )
+ return node
+ else: # == 'UNARY_OP'
+ UNARY_OP = self._scan('UNARY_OP', context=_context)
+ expr_mul = self.expr_mul(S, _context)
+ node = UnaryExpression( UNARY_OP, expr_mul )
+ while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'ADD_OP':
+ ADD_OP = self._scan('ADD_OP', context=_context)
+ expr_mul = self.expr_mul(S, _context)
+ node = MathExpression( ADD_OP, node, expr_mul )
+ return node
+
+ def expr_mul(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'expr_mul', [S])
+ expr_pow = self.expr_pow(S, _context)
+ node = expr_pow
+ while self._peek('MUL_OP', 'ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'MUL_OP':
+ MUL_OP = self._scan('MUL_OP', context=_context)
+ expr_pow = self.expr_pow(S, _context)
+ node = MathExpression( MUL_OP, node, expr_pow)
+ return node
+
+ def expr_pow(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'expr_pow', [S])
+ expr_base = self.expr_base(S, _context)
+ node = expr_base
+ while self._peek('POW_OP', 'MUL_OP', 'ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'POW_OP':
+ POW_OP = self._scan('POW_OP', context=_context)
+ expr_base = self.expr_base(S, _context)
+ node = MathExpression( MUL_OP, node, expr_base)
+ return node
+
+ def expr_base(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'expr_base', [S])
+ _token = self._peek('r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+ if _token not in ['r"\\("', 'VARIABLE', 'E_TYPE', 'FUNCTION']:
+ const = self.const(_context)
+ return const
+ elif _token == 'VARIABLE':
+ var = self.var(S, _context)
+ return var
+ elif _token == 'E_TYPE':
+ etype = self.etype(S, _context)
+ return etype
+ elif _token == 'FUNCTION':
+ func = self.func(S, _context)
+ return func
+ else: # == 'r"\\("'
+ self._scan('r"\\("', context=_context)
+ expr_add = self.expr_add(S, _context)
+ self._scan('r"\\)"', context=_context)
+ return expr_add
+
+ def func(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'func', [S])
+ FUNCTION = self._scan('FUNCTION', context=_context)
+ self._scan('r"\\("', context=_context)
+ F = Function(FUNCTION)
+ if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
+ expr_add = self.expr_add(S, _context)
+ while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == "','":
+ F.append(expr_add)
+ self._scan("','", context=_context)
+ expr_add = self.expr_add(S, _context)
+ F.append(expr_add)
+ self._scan('r"\\)"', context=_context)
+ return F
+
+ def in_expr(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'in_expr', [S])
+ self._scan("'IN'", context=_context)
+ self._scan('r"\\("', context=_context)
+ F = Function('IN')
+ if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
+ expr_add = self.expr_add(S, _context)
+ while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == "','":
+ F.append(expr_add)
+ self._scan("','", context=_context)
+ expr_add = self.expr_add(S, _context)
+ F.append(expr_add)
+ self._scan('r"\\)"', context=_context)
+ return F
+
+ def var(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'var', [S])
+ VARIABLE = self._scan('VARIABLE', context=_context)
+ return VariableRef(S.get_variable(VARIABLE))
+
+ def etype(self, S, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'etype', [S])
+ E_TYPE = self._scan('E_TYPE', context=_context)
+ return S.get_etype(E_TYPE)
+
+ def const(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, 'const', [])
+ _token = self._peek('NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', context=_context)
+ if _token == 'NULL':
+ NULL = self._scan('NULL', context=_context)
+ return Constant(None, None)
+ elif _token == 'DATE':
+ DATE = self._scan('DATE', context=_context)
+ return Constant(DATE.upper(), 'Date')
+ elif _token == 'DATETIME':
+ DATETIME = self._scan('DATETIME', context=_context)
+ return Constant(DATETIME.upper(), 'Datetime')
+ elif _token == 'TRUE':
+ TRUE = self._scan('TRUE', context=_context)
+ return Constant(True, 'Boolean')
+ elif _token == 'FALSE':
+ FALSE = self._scan('FALSE', context=_context)
+ return Constant(False, 'Boolean')
+ elif _token == 'FLOAT':
+ FLOAT = self._scan('FLOAT', context=_context)
+ return Constant(float(FLOAT), 'Float')
+ elif _token == 'INT':
+ INT = self._scan('INT', context=_context)
+ return Constant(int(INT), 'Int')
+ elif _token == 'STRING':
+ STRING = self._scan('STRING', context=_context)
+ return Constant(unquote(STRING), 'String')
+ else: # == 'SUBSTITUTE'
+ SUBSTITUTE = self._scan('SUBSTITUTE', context=_context)
+ return Constant(SUBSTITUTE[2:-2], 'Substitute')
+
+
+def parse(rule, text):
+ P = Hercule(HerculeScanner(text))
+ return runtime.wrap_error_reporter(P, rule)
+
+# End -- grammar generated by Yapps
+
+
+
+from warnings import warn
+from rql.stmts import Union, Select, Delete, Insert, Set
+from rql.nodes import *
+
+
+def unquote(string):
+ """Remove quotes from a string."""
+ if string.startswith('"'):
+ return string[1:-1].replace('\\\\', '\\').replace('\\"', '"')
+ elif string.startswith("'"):
+ return string[1:-1].replace('\\\\', '\\').replace("\\'", "'")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/parser_main.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,39 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of rql.
+#
+# rql 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.
+#
+# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
+"""Main parser command.
+
+"""
+__docformat__ = "restructuredtext en"
+
+if __name__ == '__main__':
+ from sys import argv
+
+ parser = Hercule(HerculeScanner(argv[1]))
+ e_types = {}
+ # parse the RQL string
+ try:
+ tree = parser.goal(e_types)
+ print '-'*80
+ print tree
+ print '-'*80
+ print repr(tree)
+ print e_types
+ except SyntaxError, ex:
+ # try to get error message from yapps
+ from yapps.runtime import print_error
+ print_error(ex, parser._scanner)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/pygments_ext.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+"""
+ pygments.lexers.rql
+ ~~~~~~~~~~~~~~~~~~~
+
+ Lexer for RQL the relation query language
+
+ http://www.logilab.org/project/rql
+"""
+
+import re
+
+from pygments.lexer import RegexLexer, _mapping
+from pygments.token import Punctuation, \
+ Text, Comment, Operator, Keyword, Name, String, Number
+
+__all__ = ['RqlLexer']
+
+class RqlLexer(RegexLexer):
+ """
+ Lexer for Relation Query Language.
+ """
+
+ name = 'RQL'
+ aliases = ['rql']
+ filenames = ['*.rql']
+ mimetypes = ['text/x-rql']
+
+ flags = re.IGNORECASE
+ tokens = {
+ 'root': [
+ (r'\s+', Text),
+ (r'(DELETE|SET|INSERT|UNION|DISTINCT|WITH|WHERE|BEING|OR'
+ r'|AND|NOT|GROUPBY|HAVING|ORDERBY|ASC|DESC|LIMIT|OFFSET'
+ r'|TODAY|NOW|TRUE|FALSE|NULL|EXISTS)\b', Keyword),
+ (r'[+*/<>=%-]', Operator),
+ (r'(Any|is|instance_of)\b', Name.Builtin),
+ (r'[0-9]+', Number.Integer),
+ (r'[A-Z_][A-Z0-9_]*\??', Name),
+ (r"'(''|[^'])*'", String.Single),
+ (r'"(""|[^"])*"', String.Single),
+ (r'[;:()\[\],\.]', Punctuation)
+ ],
+ }
+
+_mapping.LEXERS['RqlLexer'] = ('rql.pygments_ext', 'RQL', ('rql',), ('*.rql',), ('text/x-rql',))
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/rqlgen.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,214 @@
+# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of rql.
+#
+# rql 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.
+#
+# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
+"""Generation of RQL strings.
+
+"""
+__docformat__ = "restructuredtext en"
+
+from six.moves import range
+
+NOT = 1
+
+class RQLGenerator(object):
+ """
+ Helper class to generate RQL strings.
+ """
+
+ def select(self, etype, nupplets=(), groups=(), sorts=()) :
+ """
+ Return a RQL selection query.
+
+ :Parameters:
+ * `etype`: the desired entity type (can be 'Any')
+
+ * `nupplets`: a list of 4-uples (subject, relation, object, not).
+ <subject> and <object> may be a string representing a variable
+ or a constant. The special variable X represents the searched set
+ of entities.
+ <relation> is the statement axis.
+ <not> is a boolean indicating it should be a negative statement
+ (0 -> positive statement, 1 -> negative statement). You may omit
+ this parameter, it default to 0.
+
+ * `groups`: a list of variables to use in groups
+
+ * `sorts`: a list of sort term. A sort term is a string designing a
+ variable and optionnaly the sort order ('ASC' or 'DESC'). If the
+ sort order is omitted default to 'ASC'
+
+ Example:
+
+ >>> s = RQLGenerator()
+ >>> s.select('Any', (('X', 'eid', 14),) )
+ 'Any X\\nWHERE X eid 14'
+ >>> s.select('Person',
+ ... ( ('X','work_for','S'), ('S','name','"Logilab"'),
+ ... ('X','firstname','F'), ('X','surname','S') ),
+ ... sorts=('F ASC', 'S DESC')
+ ... )
+ 'Person X\\nWHERE X work_for S , S name "Logilab" , X firstname F , X surname S\\nSORTBY F ASC, S DESC'
+ """
+ result = [etype + ' X']
+ if nupplets:
+ result.append(self.where(nupplets))
+ if groups:
+ result.append(self.groupby(groups))
+ if sorts:
+ result.append(self.sortby(sorts))
+ return '\n'.join(result)
+
+
+ def where(self, nupplets):
+ """Return a WHERE statement.
+
+ :Parameters:
+ * `nupplets`: a list of 4-uples (subject, relation, object, not)
+ <subject> and <object> maybe a string designing a variable or a
+ constant. The special variable X represents the searched set of
+ entities
+ <relation> is the statement axis
+ <not> is a boolean indicating it should be a negative statement
+ (0 -> positive statement, 1 -> negative statement). You may omit
+ this parameter, it default to 0.
+
+ Example:
+
+ >>> s = RQLGenerator()
+ >>> s.where( (('X', 'eid', 14),) )
+ 'WHERE X eid 14'
+ >>> s.where( ( ('X','work_for','S'), ('S','name','"Logilab"'),
+ ... ('X','firstname','F'), ('X','surname','S') ) )
+ 'WHERE X work_for S , S name "Logilab" , X firstname F , X surname S'
+ """
+ result = ['WHERE']
+ total = len(nupplets)
+ for i in range(total):
+ nupplet = nupplets[i]
+ if len(nupplet) == 4 and nupplet[3] == NOT:
+ result.append('NOT')
+ result.append(str(nupplet[0]))
+ result.append(str(nupplet[1]))
+ result.append(str(nupplet[2]))
+ if i < total - 1:
+ result.append(',')
+ return ' '.join(result)
+
+
+ def groupby(self, groups):
+ """Return a GROUPBY statement.
+
+ :param groups: a list of variables to use in groups
+
+ Example:
+
+ >>> s = RQLGenerator()
+ >>> s.groupby(('F', 'S'))
+ 'GROUPBY F, S'
+ """
+ return 'GROUPBY %s' % ', '.join(groups)
+
+
+ def sortby(self, sorts):
+ """Return a SORTBY statement.
+
+ :param sorts: a list of sort term. A sort term is a string designing a
+ variable and optionnaly the sort order ('ASC' or 'DESC'). If the
+ sort order is omitted default to 'ASC'
+
+
+ Example:
+
+ >>> s = RQLGenerator()
+ >>> s.sortby(('F ASC', 'S DESC'))
+ 'SORTBY F ASC, S DESC'
+ """
+ return 'SORTBY %s' % ', '.join(sorts)
+
+
+ def insert(self, etype, attributes):
+ """Return an INSERT statement.
+
+ :Parameters:
+ * `etype`: the entity type to insert
+ * `attributes`: a list of tuples (attr_name, attr_value)
+
+ Example:
+
+ >>> s = RQLGenerator()
+ >>> s.insert('Person', (('firstname', "Clark"), ('lastname', "Kent")))
+ 'INSERT Person X: X firstname "Clark", X lastname "Kent"'
+ """
+ restrictions = ['X %s "%s"' % (attr_name, attr_value)
+ for attr_name, attr_value in attributes] # .items()]
+ return 'INSERT %s X: %s' % (etype, ', '.join(restrictions))
+
+
+ def delete(self, etype, attributes):
+ """Return a DELETE statement.
+
+ :Parameters:
+ * `etype`: the entity type to delete
+ * `attributes`: a list of tuples (attr_name, attr_value)
+
+ Example:
+
+ >>> s = RQLGenerator()
+ >>> s.delete('Person', (('firstname', "Clark"), ('lastname', "Kent")))
+ 'DELETE Person X where X firstname "Clark", X lastname "Kent"'
+ """
+ restrictions = ['X %s "%s"' % (attr_name, attr_value)
+ for attr_name, attr_value in attributes] # .items()]
+ return 'DELETE %s X where %s' % (etype, ', '.join(restrictions))
+
+
+ def update(self, etype, old_descr, new_descr):
+ """Return a SET statement.
+
+ :Parameters:
+ * `etype`: the entity type to update
+ * `old_descr`: a list of tuples (attr_name, attr_value)
+ that identifies the entity to update
+ * `new_descr`: a list of tuples (attr_name, attr_value)
+ that defines the attributes to update
+
+ Example:
+
+ >>> s = RQLGenerator()
+ >>> s.update('Person', (('firstname', "Clark"), ('lastname', "Kent")),
+ ... (('job', "superhero"), ('nickname', "superman")))
+ 'SET X job "superhero", X nickname "superman" WHERE X is "Person", X firstname "Clark", X lastname "Kent"'
+ """
+ old_restrictions = ['X is "%s"' % etype]
+ old_restrictions += ['X %s "%s"' % (attr_name, attr_value)
+ for attr_name, attr_value in old_descr]
+ new_restrictions = ['X %s "%s"' % (attr_name, attr_value)
+ for attr_name, attr_value in new_descr]
+ return 'SET %s WHERE %s' % (', '.join(new_restrictions),
+ ', '.join(old_restrictions))
+
+RQLGENERATOR = RQLGenerator()
+
+def _test():
+ """
+ Launch doctest
+ """
+ import doctest, sys
+ return doctest.testmod(sys.modules[__name__])
+
+if __name__ == "__main__":
+ _test()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rql/stcheck.py Thu May 28 01:13:28 2015 +0200
@@ -0,0 +1,714 @@
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of rql.
+#
+# rql 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.
+#
+# rql 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 rql. If not, see <http://www.gnu.org/licenses/>.
+"""RQL Syntax tree annotator"""
+
+__docformat__ = "restructuredtext en"
+
+from itertools import chain
+from logilab.common.graph import has_path
+from logilab.database import UnknownFunction
+
+from rql._exceptions import BadRQLQuery
+from rql.utils import function_description
+from rql.nodes import (Relation, VariableRef, Constant, Not, Exists, Function,
+ And, Variable, Comparison, variable_refs, make_relation)
+from rql.stmts import Union
+
+
+def _var_graphid(subvarname, trmap, select):
+ try:
+ return trmap[subvarname]
+ except KeyError:
+ return subvarname + str(id(select))
+
+def bloc_simplification(variable, term):
+ try:
+ variable.stinfo['blocsimplification'].add(term)
+ except KeyError:
+ variable.stinfo['blocsimplification'] = set((term,))
+
+
+class GoTo(Exception):
+ """Exception used to control the visit of the tree."""
+ def __init__(self, node):
+ self.node = node
+
+VAR_SELECTED = 1
+VAR_HAS_TYPE_REL = 2
+VAR_HAS_UID_REL = 4
+VAR_HAS_REL = 8
+
+class STCheckState(object):
+ def __init__(self):
+ self.errors = []
+ self.under_not = []
+ self.var_info = {}
+
+ def error(self, msg):
+ self.errors.append(msg)
+
+ def add_var_info(self, var, vi):
+ try:
+ self.var_info[var] |= vi
+ except KeyError:
+ self.var_info[var] = vi
+
+class RQLSTChecker(object):
+ """Check a RQL syntax tree for errors not detected on parsing.
+
+ Some simple rewriting of the tree may be done too:
+ * if a OR is used on a symmetric relation
+ * IN function with a single child
+
+ use assertions for internal error but specific `BadRQLQuery` exception for
+ errors due to a bad rql input
+ """
+
+ def __init__(self, schema, special_relations=None, backend=None):
+ self.schema = schema
+ self.special_relations = special_relations or {}
+ self.backend = backend
+
+ def check(self, node):
+ state = STCheckState()
+ self._visit(node, state)
+ if state.errors:
+ raise BadRQLQuery('%s\n** %s' % (node, '\n** '.join(state.errors)))
+ #if node.TYPE == 'select' and \
+ # not node.defined_vars and not node.get_restriction():
+ # result = []
+ # for term in node.selected_terms():
+ # result.append(term.eval(kwargs))
+
+ def _visit(self, node, state):
+ try:
+ node.accept(self, state)
+ except GoTo as ex:
+ self._visit(ex.node, state)
+ else:
+ for c in node.children:
+ self._visit(c, state)
+ node.leave(self, state)
+
+ def _visit_selectedterm(self, node, state):
+ for i, term in enumerate(node.selection):
+ # selected terms are not included by the default visit,
+ # accept manually each of them
+ self._visit(term, state)
+
+ def _check_selected(self, term, termtype, state):
+ """check that variables referenced in the given term are selected"""
+ for vref in variable_refs(term):
+ # no stinfo yet, use references
+ for ovref in vref.variable.references():
+ rel = ovref.relation()
+ if rel is not None:
+ break
+ else:
+ msg = 'variable %s used in %s is not referenced by any relation'
+ state.error(msg % (vref.name, termtype))
+
+ # statement nodes #########################################################
+
+ def visit_union(self, node, state):
+ nbselected = len(node.children[0].selection)
+ for select in node.children[1:]:
+ if not len(select.selection) == nbselected:
+ state.error('when using union, all subqueries should have '
+ 'the same number of selected terms')
+ def leave_union(self, node, state):
+ pass
+
+ def visit_select(self, node, state):
+ node.vargraph = {} # graph representing links between variable
+ node.aggregated = set()
+ self._visit_selectedterm(node, state)
+
+ def leave_select(self, node, state):
+ selected = node.selection
+ # check selected variable are used in restriction
+ if node.where is not None or len(selected) > 1:
+ for term in selected:
+ self._check_selected(term, 'selection', state)
+ for vref in term.iget_nodes(VariableRef):
+ state.add_var_info(vref.variable, VAR_SELECTED)
+ for var in node.defined_vars.values():
+ vinfo = state.var_info.get(var, 0)
+ if not (vinfo & VAR_HAS_REL) and (vinfo & VAR_HAS_TYPE_REL) \
+ and not (vinfo & VAR_SELECTED):
+ raise BadRQLQuery('unbound variable %s (%s)' % (var.name, selected))
+ if node.groupby:
+ # check that selected variables are used in groups
+ for var in node.selection:
+ if isinstance(var, VariableRef) and not var in node.groupby:
+ state.error('variable %s should be grouped' % var)
+ for group in node.groupby:
+ self._check_selected(group, 'group', state)
+ if node.distinct and node.orderby:
+ # check that variables referenced in the given term are reachable from
+ # a selected variable with only ?1 cardinality selected
+ selectidx = frozenset(vref.name for term in selected
+ for vref in term.get_nodes(VariableRef))
+ schema = self.schema
+ for sortterm in node.orderby:
+ for vref in sortterm.term.get_nodes(VariableRef):
+ if vref.name in selectidx:
+ continue
+ for vname in selectidx:
+ try:
+ if self.has_unique_value_path(node, vname, vref.name):
+ break
+ except KeyError:
+ continue # unlinked variable (usually from a subquery)
+ else:
+ msg = ('can\'t sort on variable %s which is linked to a'
+ ' variable in the selection but may have different'
+ ' values for a resulting row')
+ state.error(msg % vref.name)
+
+ def has_unique_value_path(self, select, fromvar, tovar):
+ graph = select.vargraph
+ path = has_path(graph, fromvar, tovar)
+ if path is None:
+ return False
+ for var in path:
+ try:
+ rtype = graph[(fromvar, var)]
+ cardidx = 0
+ except KeyError:
+ rtype = graph[(var, fromvar)]
+ cardidx = 1
+ rschema = self.schema.rschema(rtype)
+ for rdef in rschema.rdefs.values():
+ # XXX aggregats handling needs much probably some enhancements...
+ if not (var in select.aggregated
+ or (rdef.cardinality[cardidx] in '?1' and
+ (var == tovar or not rschema.final))):
+ return False
+ fromvar = var
+ return True
+
+
+ def visit_insert(self, insert, state):
+ self._visit_selectedterm(insert, state)
+ def leave_insert(self, node, state):
+ pass
+
+ def visit_delete(self, delete, state):
+ self._visit_selectedterm(delete, state)
+ def leave_delete(self, node, state):
+ pass
+
+ def visit_set(self, update, state):
+ self._visit_selectedterm(update, state)
+ def leave_set(self, node, state):
+ pass
+
+ # tree nodes ##############################################################
+
+ def visit_exists(self, node, state):
+ pass
+ def leave_exists(self, node, state):
+ pass
+
+ def visit_subquery(self, node, state):
+ pass
+
+ def leave_subquery(self, node, state):
+ # copy graph information we're interested in
+ pgraph = node.parent.vargraph
+ for select in node.query.children:
+ # map subquery variable names to outer query variable names
+ trmap = {}
+ for i, vref in enumerate(node.aliases):
+ try:
+ subvref = select.selection[i]
+ except IndexError:
+ state.error('subquery "%s" has only %s selected terms, needs %s'
+ % (select, len(select.selection), len(node.aliases)))
+ continue
+ if isinstance(subvref, VariableRef):
+ trmap[subvref.name] = vref.name
+ elif (isinstance(subvref, Function) and subvref.descr().aggregat
+ and len(subvref.children) == 1
+ and isinstance(subvref.children[0], VariableRef)):
+ # XXX ok for MIN, MAX, but what about COUNT, AVG...
+ trmap[subvref.children[0].name] = vref.name
+ node.parent.aggregated.add(vref.name)
+ for key, val in select.vargraph.items():
+ if isinstance(key, tuple):
+ key = (_var_graphid(key[0], trmap, select),
+ _var_graphid(key[1], trmap, select))
+ pgraph[key] = val
+ else:
+ values = pgraph.setdefault(_var_graphid(key, trmap, select), [])
+ values += [_var_graphid(v, trmap, select) for v in val]
+
+ def visit_sortterm(self, sortterm, state):
+ term = sortterm.term
+ if isinstance(term, Constant):
+ for select in sortterm.root.children:
+ if len(select.selection) < term.value:
+ state.error('order column out of bound %s' % term.value)
+ else:
+ stmt = term.stmt
+ for tvref in variable_refs(term):
+ for vref in tvref.variable.references():
+ if vref.relation() or vref in stmt.selection:
+ break
+ else:
+ msg = 'sort variable %s is not referenced any where else'
+ state.error(msg % tvref.name)
+
+ def leave_sortterm(self, node, state):
+ pass
+
+ def visit_and(self, et, state):
+ pass #assert len(et.children) == 2, len(et.children)
+ def leave_and(self, node, state):
+ pass
+
+ def visit_or(self, ou, state):
+ #assert len(ou.children) == 2, len(ou.children)
+ # simplify Ored expression of a symmetric relation
+ r1, r2 = ou.children[0], ou.children[1]
+ try:
+ r1type = r1.r_type
+ r2type = r2.r_type
+ except AttributeError:
+ return # can't be
+ if r1type == r2type and self.schema.rschema(r1type).symmetric:
+ lhs1, rhs1 = r1.get_variable_parts()
+ lhs2, rhs2 = r2.get_variable_parts()
+ try:
+ if (lhs1.variable is rhs2.variable and
+ rhs1.variable is lhs2.variable):
+ ou.parent.replace(ou, r1)
+ for vref in r2.get_nodes(VariableRef):
+ vref.unregister_reference()
+ raise GoTo(r1)
+ except AttributeError:
+ pass
+ def leave_or(self, node, state):
+ pass
+
+ def visit_not(self, not_, state):
+ state.under_not.append(True)
+ def leave_not(self, not_, state):
+ state.under_not.pop()
+ # NOT normalization
+ child = not_.children[0]
+ if self._should_wrap_by_exists(child):
+ not_.replace(child, Exists(child))
+
+ def _should_wrap_by_exists(self, child):
+ if isinstance(child, Exists):
+ return False
+ if not isinstance(child, Relation):
+ return True
+ if child.r_type == 'identity':
+ return False
+ rschema = self.schema.rschema(child.r_type)
+ if rschema.final:
+ return False
+ # XXX no exists for `inlined` relation (allow IS NULL optimization)
+ # unless the lhs variable is only referenced from this neged relation,
+ # in which case it's *not* in the statement's scope, hence EXISTS should
+ # be added anyway
+ if rschema.inlined:
+ references = child.children[0].variable.references()
+ valuable = 0
+ for vref in references:
+ rel = vref.relation()
+ if rel is None or not rel.is_types_restriction():
+ if valuable:
+ return False
+ valuable = 1
+ return True
+ return not child.is_types_restriction()
+
+ def visit_relation(self, relation, state):
+ if relation.optional and state.under_not:
+ state.error("can't use optional relation under NOT (%s)"
+ % relation.as_string())
+ lhsvar = relation.children[0].variable
+ if relation.is_types_restriction():
+ if relation.optional:
+ state.error('can\'t use optional relation on "%s"'
+ % relation.as_string())
+ if state.var_info.get(lhsvar, 0) & VAR_HAS_TYPE_REL:
+ state.error('can only one type restriction per variable (use '
+ 'IN for %s if desired)' % lhsvar.name)
+ else:
+ state.add_var_info(lhsvar, VAR_HAS_TYPE_REL)
+ # special case "C is NULL"
+ # if relation.children[1].operator == 'IS':
+ # lhs, rhs = relation.children
+ # #assert isinstance(lhs, VariableRef), lhs
+ # #assert isinstance(rhs.children[0], Constant)
+ # #assert rhs.operator == 'IS', rhs.operator
+ # #assert rhs.children[0].type == None
+ else:
+ state.add_var_info(lhsvar, VAR_HAS_REL)
+ rtype = relation.r_type
+ try:
+ rschema = self.schema.rschema(rtype)
+ except KeyError:
+ state.error('unknown relation `%s`' % rtype)
+ else:
+ if rschema.final and relation.optional not in (None, 'right'):
+ state.error("optional may only be set on the rhs on final relation `%s`"
+ % relation.r_type)
+ if self.special_relations.get(rtype) == 'uid' and relation.operator() == '=':
+ if state.var_info.get(lhsvar, 0) & VAR_HAS_UID_REL:
+ state.error('can only one uid restriction per variable '
+ '(use IN for %s if desired)' % lhsvar.name)
+ else:
+ state.add_var_info(lhsvar, VAR_HAS_UID_REL)
+
+ for vref in relation.children[1].get_nodes(VariableRef):
+ state.add_var_info(vref.variable, VAR_HAS_REL)
+ try:
+ vargraph = relation.stmt.vargraph
+ rhsvarname = relation.children[1].children[0].variable.name
+ except AttributeError:
+ pass
+ else:
+ vargraph.setdefault(lhsvar.name, []).append(rhsvarname)
+ vargraph.setdefault(rhsvarname, []).append(lhsvar.name)
+ vargraph[(lhsvar.name, rhsvarname)] = relation.r_type
+
+ def leave_relation(self, relation, state):
+ pass
+ #assert isinstance(lhs, VariableRef), '%s: %s' % (lhs.__class__,
+ # relation)
+
+ def visit_comparison(self, comparison, state):
+ pass #assert len(comparison.children) in (1,2), len(comparison.children)
+ def leave_comparison(self, node, state):
+ pass
+
+ def visit_mathexpression(self, mathexpr, state):
+ pass #assert len(mathexpr.children) == 2, len(mathexpr.children)
+ def leave_mathexpression(self, node, state):
+ pass
+ def visit_unaryexpression(self, unaryexpr, state):
+ pass #assert len(unaryexpr.children) == 2, len(unaryexpr.children)
+ def leave_unaryexpression(self, node, state):
+ pass
+
+ def visit_function(self, function, state):
+ try:
+ funcdescr = function_description(function.name)
+ except UnknownFunction:
+ state.error('unknown function "%s"' % function.name)
+ else:
+ try:
+ funcdescr.check_nbargs(len(function.children))
+ except BadRQLQuery as ex:
+ state.error(str(ex))
+ if self.backend is not None:
+ try:
+ funcdescr.st_check_backend(self.backend, function)
+ except BadRQLQuery as ex:
+ state.error(str(ex))
+ if funcdescr.aggregat:
+ if isinstance(function.children[0], Function) and \
+ function.children[0].descr().aggregat:
+ state.error('can\'t nest aggregat functions')
+ if funcdescr.name == 'IN':
+ #assert function.parent.operator == '='
+ if len(function.children) == 1:
+ function.parent.append(function.children[0])
+ function.parent.remove(function)
+ #else:
+ # assert len(function.children) >= 1
+
+ def leave_function(self, node, state):
+ pass
+
+ def visit_variableref(self, variableref, state):
+ #assert len(variableref.children)==0
+ #assert not variableref.parent is variableref
+## try:
+## assert variableref.variable in variableref.root().defined_vars.values(), \
+## (variableref.root(), variableref.variable, variableref.root().defined_vars)
+## except AttributeError:
+## raise Exception((variableref.root(), variableref.variable))
+ pass
+
+ def leave_variableref(self, node, state):
+ pass
+
+ def visit_constant(self, constant, state):
+ if constant.type != 'etype':
+ return
+ if constant.value not in self.schema:
+ state.error('unknown entity type %s' % constant.value)
+ if (isinstance(constant.parent, Function) and
+ constant.parent.name == 'CAST'):
+ return
+ rel = constant.relation()
+ if rel is not None and rel.r_type in ('is', 'is_instance_of'):
+ return
+ state.error('Entity types can only be used inside a CAST() '
+ 'or with "is" relation')
+
+ def leave_constant(self, node, state):
+ pass
+
+
+class RQLSTAnnotator(object):
+ """Annotate RQL syntax tree to ease further code generation from it.
+
+ If an optional variable is shared among multiple scopes, it's rewritten to
+ use identity relation.
+ """
+
+ def __init__(self, schema, special_relations=None):
+ self.schema = schema
+ self.special_relations = special_relations or {}
+
+ def annotate(self, node):
+ #assert not node.annotated
+ node.accept(self)
+ node.annotated = True
+
+ def _visit_stmt(self, node):
+ for var in node.defined_vars.values():
+ var.prepare_annotation()
+ for i, term in enumerate(node.selection):
+ for func in term.iget_nodes(Function):
+ if func.descr().aggregat:
+ node.has_aggregat = True
+ break
+ # register the selection column index
+ for vref in term.get_nodes(VariableRef):
+ vref.variable.stinfo['selected'].add(i)
+ vref.variable.set_scope(node)
+ if node.where is not None:
+ node.where.accept(self, node)
+
+ visit_insert = visit_delete = visit_set = _visit_stmt
+
+ def visit_union(self, node):
+ for select in node.children:
+ self.visit_select(select)
+
+ def visit_select(self, node):
+ for var in node.aliases.values():
+ var.prepare_annotation()
+ if node.with_ is not None:
+ for subquery in node.with_:
+ self.visit_union(subquery.query)
+ subquery.query.schema = node.root.schema
+ node.has_aggregat = False
+ self._visit_stmt(node)
+ if node.having:
+ # if there is a having clause, bloc simplification of variables used in GROUPBY
+ for term in node.groupby:
+ for vref in term.get_nodes(VariableRef):
+ bloc_simplification(vref.variable, term)
+ try:
+ vargraph = node.vargraph
+ except AttributeError:
+ vargraph = None
+ # XXX node.having is a list of size 1
+ assert len(node.having) == 1
+ for term in node.having[0].get_nodes(Comparison):
+ lhsvariables = set(vref.variable for vref in term.children[0].get_nodes(VariableRef))
+ rhsvariables = set(vref.variable for vref in term.children[1].get_nodes(VariableRef))
+ for var in lhsvariables | rhsvariables:
+ var.stinfo.setdefault('having', []).append(term)
+ if vargraph is not None:
+ for v1 in lhsvariables:
+ v1 = v1.name
+ for v2 in rhsvariables:
+ v2 = v2.name
+ if v1 != v2:
+ vargraph.setdefault(v1, []).append(v2)
+ vargraph.setdefault(v2, []).append(v1)
+ if term.optional in ('left', 'both'):
+ for var in lhsvariables:
+ if var.stinfo['attrvar'] is not None:
+ optcomps = var.stinfo['attrvar'].stinfo.setdefault('optcomparisons', set())
+ optcomps.add(term)
+ if