--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/sources/rql2sql.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,1703 @@
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+"""RQL to SQL generator for native sources.
+
+
+SQL queries optimization
+~~~~~~~~~~~~~~~~~~~~~~~~
+1. CWUser X WHERE X in_group G, G name 'users':
+
+ CWUser is the only subject entity type for the in_group relation,
+ which allow us to do ::
+
+ SELECT eid_from FROM in_group, CWGroup
+ WHERE in_group.eid_to = CWGroup.eid_from
+ AND CWGroup.name = 'users'
+
+
+2. Any X WHERE X nonfinal1 Y, Y nonfinal2 Z
+
+ -> direct join between nonfinal1 and nonfinal2, whatever X,Y, Z (unless
+ inlined...)
+
+ NOT IMPLEMENTED (and quite hard to implement)
+
+Potential optimization information is collected by the querier, sql generation
+is done according to this information
+
+cross RDMS note : read `Comparison of different SQL implementations`_
+by Troels Arvin. Features SQL ISO Standard, PG, mysql, Oracle, MS SQL, DB2
+and Informix.
+
+.. _Comparison of different SQL implementations: http://www.troels.arvin.dk/db/rdbms
+"""
+
+__docformat__ = "restructuredtext en"
+
+import threading
+
+from six import PY2
+from six.moves import range
+
+from logilab.database import FunctionDescr, SQL_FUNCTIONS_REGISTRY
+
+from rql import BadRQLQuery, CoercionError
+from rql.utils import common_parent
+from rql.stmts import Union, Select
+from rql.nodes import (VariableRef, Constant, Function, Variable, Or,
+ Not, Comparison, ColumnAlias, Relation, SubQuery)
+
+from cubicweb import QueryError
+from cubicweb.rqlrewrite import cleanup_solutions
+from cubicweb.server.sqlutils import SQL_PREFIX
+
+ColumnAlias._q_invariant = False # avoid to check for ColumnAlias / Variable
+
+FunctionDescr.source_execute = None
+
+def default_update_cb_stack(self, stack):
+ stack.append(self.source_execute)
+FunctionDescr.update_cb_stack = default_update_cb_stack
+
+get_func_descr = SQL_FUNCTIONS_REGISTRY.get_function
+
+LENGTH = get_func_descr('LENGTH')
+def length_source_execute(source, session, value):
+ return len(value.getvalue())
+LENGTH.source_execute = length_source_execute
+
+def _new_var(select, varname):
+ newvar = select.get_variable(varname)
+ if not 'relations' in newvar.stinfo:
+ # not yet initialized
+ newvar.prepare_annotation()
+ newvar.stinfo['scope'] = select
+ newvar._q_invariant = False
+ select.selection.append(VariableRef(newvar))
+ return newvar
+
+def _fill_to_wrap_rel(var, newselect, towrap, schema):
+ for rel in var.stinfo['relations'] - var.stinfo['rhsrelations']:
+ rschema = schema.rschema(rel.r_type)
+ if rschema.inlined:
+ towrap.add( (var, rel) )
+ for vref in rel.children[1].iget_nodes(VariableRef):
+ newivar = _new_var(newselect, vref.name)
+ _fill_to_wrap_rel(vref.variable, newselect, towrap, schema)
+ elif rschema.final:
+ towrap.add( (var, rel) )
+ for vref in rel.children[1].iget_nodes(VariableRef):
+ newivar = _new_var(newselect, vref.name)
+ newivar.stinfo['attrvar'] = (var, rel.r_type)
+
+def rewrite_unstable_outer_join(select, solutions, unstable, schema):
+ """if some optional variables are unstable, they should be selected in a
+ subquery. This function check this and rewrite the rql syntax tree if
+ necessary (in place). Return a boolean telling if the tree has been modified
+ """
+ modified = False
+ for varname in tuple(unstable):
+ var = select.defined_vars[varname]
+ if not var.stinfo.get('optrelations'):
+ continue
+ unstable.remove(varname)
+ newselect = Select()
+ myunion = Union()
+ myunion.append(newselect)
+ # extract aliases / selection
+ newvar = _new_var(newselect, var.name)
+ newselect.selection = [VariableRef(newvar)]
+ towrap_rels = set()
+ _fill_to_wrap_rel(var, newselect, towrap_rels, schema)
+ # extract relations
+ for var, rel in towrap_rels:
+ newrel = rel.copy(newselect)
+ newselect.add_restriction(newrel)
+ select.remove_node(rel)
+ var.stinfo['relations'].remove(rel)
+ newvar.stinfo['relations'].add(newrel)
+ if rel.optional in ('left', 'both'):
+ newvar.add_optional_relation(newrel)
+ for vref in newrel.children[1].iget_nodes(VariableRef):
+ var = vref.variable
+ var.stinfo['relations'].add(newrel)
+ var.stinfo['rhsrelations'].add(newrel)
+ if rel.optional in ('right', 'both'):
+ var.add_optional_relation(newrel)
+ if not select.where and not modified:
+ # oops, generated the same thing as the original select....
+ # restore original query, else we'll indefinitly loop
+ for var, rel in towrap_rels:
+ select.add_restriction(rel)
+ continue
+ modified = True
+ # extract subquery solutions
+ mysolutions = [sol.copy() for sol in solutions]
+ cleanup_solutions(newselect, mysolutions)
+ newselect.set_possible_types(mysolutions)
+ # full sub-query
+ aliases = [VariableRef(select.get_variable(avar.name, i))
+ for i, avar in enumerate(newselect.selection)]
+ select.add_subquery(SubQuery(aliases, myunion), check=False)
+ return modified
+
+def _new_solutions(rqlst, solutions):
+ """first filter out subqueries variables from solutions"""
+ newsolutions = []
+ for origsol in solutions:
+ asol = {}
+ for vname in rqlst.defined_vars:
+ asol[vname] = origsol[vname]
+ if not asol in newsolutions:
+ newsolutions.append(asol)
+ return newsolutions
+
+def remove_unused_solutions(rqlst, solutions, varmap, schema):
+ """cleanup solutions: remove solutions where invariant variables are taking
+ different types
+ """
+ newsols = _new_solutions(rqlst, solutions)
+ existssols = {}
+ unstable = set()
+ invariants = {}
+ for vname, var in rqlst.defined_vars.items():
+ vtype = newsols[0][vname]
+ if var._q_invariant or vname in varmap:
+ # remove invariant variable from solutions to remove duplicates
+ # later, then reinserting a type for the variable even later
+ for sol in newsols:
+ invariants.setdefault(id(sol), {})[vname] = sol.pop(vname)
+ elif var.scope is not rqlst:
+ # move apart variables which are in a EXISTS scope and are variating
+ try:
+ thisexistssols, thisexistsvars = existssols[var.scope]
+ except KeyError:
+ thisexistssols = [newsols[0]]
+ thisexistsvars = set()
+ existssols[var.scope] = thisexistssols, thisexistsvars
+ for i in range(len(newsols)-1, 0, -1):
+ if vtype != newsols[i][vname]:
+ thisexistssols.append(newsols.pop(i))
+ thisexistsvars.add(vname)
+ else:
+ # remember unstable variables
+ for i in range(1, len(newsols)):
+ if vtype != newsols[i][vname]:
+ unstable.add(vname)
+ if invariants:
+ # filter out duplicates
+ newsols_ = []
+ for sol in newsols:
+ if not sol in newsols_:
+ newsols_.append(sol)
+ newsols = newsols_
+ # reinsert solutions for invariants
+ for sol in newsols:
+ for invvar, vartype in invariants[id(sol)].items():
+ sol[invvar] = vartype
+ for sol in existssols:
+ try:
+ for invvar, vartype in invariants[id(sol)].items():
+ sol[invvar] = vartype
+ except KeyError:
+ continue
+ if len(newsols) > 1:
+ if rewrite_unstable_outer_join(rqlst, newsols, unstable, schema):
+ # remove variables extracted to subqueries from solutions
+ newsols = _new_solutions(rqlst, newsols)
+ return newsols, existssols, unstable
+
+def relation_info(relation):
+ lhs, rhs = relation.get_variable_parts()
+ try:
+ lhs = lhs.variable
+ lhsconst = lhs.stinfo['constnode']
+ except AttributeError:
+ lhsconst = lhs
+ lhs = None
+ except KeyError:
+ lhsconst = None # ColumnAlias
+ try:
+ rhs = rhs.variable
+ rhsconst = rhs.stinfo['constnode']
+ except AttributeError:
+ rhsconst = rhs
+ rhs = None
+ except KeyError:
+ rhsconst = None # ColumnAlias
+ return lhs, lhsconst, rhs, rhsconst
+
+def sort_term_selection(sorts, rqlst, groups):
+ # XXX beurk
+ if isinstance(rqlst, list):
+ def append(term):
+ rqlst.append(term)
+ selectionidx = set(str(term) for term in rqlst)
+ else:
+ def append(term):
+ rqlst.selection.append(term.copy(rqlst))
+ selectionidx = set(str(term) for term in rqlst.selection)
+
+ for sortterm in sorts:
+ term = sortterm.term
+ if not isinstance(term, Constant) and not str(term) in selectionidx:
+ selectionidx.add(str(term))
+ append(term)
+ if groups:
+ for vref in term.iget_nodes(VariableRef):
+ if not any(vref.is_equivalent(g) for g in groups):
+ groups.append(vref)
+
+def fix_selection_and_group(rqlst, needwrap, selectsortterms,
+ sorts, groups, having):
+ if selectsortterms and sorts:
+ sort_term_selection(sorts, rqlst, not needwrap and groups)
+ groupvrefs = [vref for term in groups for vref in term.iget_nodes(VariableRef)]
+ if sorts and groups:
+ # when a query is grouped, ensure sort terms are grouped as well
+ for sortterm in sorts:
+ term = sortterm.term
+ if not (isinstance(term, Constant) or \
+ (isinstance(term, Function) and
+ get_func_descr(term.name).aggregat)):
+ for vref in term.iget_nodes(VariableRef):
+ if not any(vref.is_equivalent(group) for group in groupvrefs):
+ groups.append(vref)
+ groupvrefs.append(vref)
+ if needwrap and (groups or having):
+ selectedidx = set(vref.name for term in rqlst.selection
+ for vref in term.get_nodes(VariableRef))
+ if groups:
+ for vref in groupvrefs:
+ if vref.name not in selectedidx:
+ selectedidx.add(vref.name)
+ rqlst.selection.append(vref)
+ if having:
+ for term in having:
+ for vref in term.iget_nodes(VariableRef):
+ if vref.name not in selectedidx:
+ selectedidx.add(vref.name)
+ rqlst.selection.append(vref)
+
+def iter_mapped_var_sels(stmt, variable):
+ # variable is a Variable or ColumnAlias node mapped to a source side
+ # callback
+ if not (len(variable.stinfo['rhsrelations']) <= 1 and # < 1 on column alias
+ variable.stinfo['selected']):
+ raise QueryError("can't use %s as a restriction variable"
+ % variable.name)
+ for selectidx in variable.stinfo['selected']:
+ vrefs = stmt.selection[selectidx].get_nodes(VariableRef)
+ if len(vrefs) != 1:
+ raise QueryError()
+ yield selectidx, vrefs[0]
+
+def update_source_cb_stack(state, stmt, node, stack):
+ while True:
+ node = node.parent
+ if node is stmt:
+ break
+ if not isinstance(node, Function):
+ raise QueryError()
+ funcd = get_func_descr(node.name)
+ if funcd.source_execute is None:
+ raise QueryError('%s can not be called on mapped attribute'
+ % node.name)
+ state.source_cb_funcs.add(node)
+ funcd.update_cb_stack(stack)
+
+
+# IGenerator implementation for RQL->SQL #######################################
+
+class StateInfo(object):
+ """this class stores data accumulated during the RQL syntax tree visit
+ for later SQL generation.
+
+ Attributes related to OUTER JOIN handling
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ * `outer_chains`, list of list of strings. Each list represent a tables
+ that have to be outer joined together.
+
+ * `outer_tables`, dictionary used as index of tables used in outer join ::
+
+ 'table alias': (outertype, [conditions], [chain])
+
+ where:
+
+ * `outertype` is one of None, 'LEFT', 'RIGHT', 'FULL'
+ * `conditions` is a list of join conditions (string)
+ * `chain` is a list of table alias (the *outer chain*) in which the key
+ alias appears
+
+ * `outer_pending` is a dictionary containing some conditions that will have
+ to be added to the outer join when the table will be turned into an
+ outerjoin ::
+
+ 'table alias': [conditions]
+ """
+ def __init__(self, select, existssols, unstablevars):
+ self.existssols = existssols
+ self.unstablevars = unstablevars
+ self.subtables = {}
+ self.needs_source_cb = None
+ self.subquery_source_cb = None
+ self.source_cb_funcs = set()
+ self.scopes = {select: 0}
+ self.scope_nodes = []
+
+ def reset(self, solution):
+ """reset some visit variables"""
+ self.solution = solution
+ self.count = 0
+ self.done = set()
+ self.tables = self.subtables.copy()
+ self.actual_tables = [[]]
+ for _, tsql in self.tables.values():
+ self.actual_tables[-1].append(tsql)
+ self.outer_chains = []
+ self.outer_tables = {}
+ self.outer_pending = {}
+ self.duplicate_switches = []
+ self.aliases = {}
+ self.restrictions = []
+ self._restr_stack = []
+ self.ignore_varmap = False
+ self._needs_source_cb = {}
+
+ def merge_source_cbs(self, needs_source_cb):
+ if self.needs_source_cb is None:
+ self.needs_source_cb = needs_source_cb
+ elif needs_source_cb != self.needs_source_cb:
+ raise QueryError('query fetch some source mapped attribute, some not')
+
+ def finalize_source_cbs(self):
+ if self.subquery_source_cb is not None:
+ self.needs_source_cb.update(self.subquery_source_cb)
+
+ def add_restriction(self, restr):
+ if restr:
+ self.restrictions.append(restr)
+
+ def iter_exists_sols(self, exists):
+ if not exists in self.existssols:
+ yield 1
+ return
+ thisexistssols, thisexistsvars = self.existssols[exists]
+ notdone_outside_vars = set()
+ # when iterating other solutions inner to an EXISTS subquery, we should
+ # reset variables which have this exists node as scope at each iteration
+ for var in exists.stmt.defined_vars.values():
+ if var.scope is exists:
+ thisexistsvars.add(var.name)
+ elif var.name not in self.done:
+ notdone_outside_vars.add(var)
+ origsol = self.solution
+ origtables = self.tables
+ done = self.done
+ for thisexistssol in thisexistssols:
+ for vname in self.unstablevars:
+ if thisexistssol[vname] != origsol[vname] and vname in thisexistsvars:
+ break
+ else:
+ self.tables = origtables.copy()
+ self.solution = thisexistssol
+ yield 1
+ # cleanup self.done from stuff specific to exists
+ for var in thisexistsvars:
+ if var in done:
+ done.remove(var)
+ for var in list(notdone_outside_vars):
+ if var.name in done and var._q_sqltable in self.tables:
+ origtables[var._q_sqltable] = self.tables[var._q_sqltable]
+ notdone_outside_vars.remove(var)
+ for rel in exists.iget_nodes(Relation):
+ if rel in done:
+ done.remove(rel)
+ self.solution = origsol
+ self.tables = origtables
+
+ def push_scope(self, scope_node):
+ self.scope_nodes.append(scope_node)
+ self.scopes[scope_node] = len(self.actual_tables)
+ self.actual_tables.append([])
+ self._restr_stack.append(self.restrictions)
+ self.restrictions = []
+
+ def pop_scope(self):
+ del self.scopes[self.scope_nodes[-1]]
+ self.scope_nodes.pop()
+ restrictions = self.restrictions
+ self.restrictions = self._restr_stack.pop()
+ scope = len(self.actual_tables) - 1
+ # check if we have some outer chain for this scope
+ matching_chains = []
+ for chain in self.outer_chains:
+ for tablealias in chain:
+ if self.tables[tablealias][0] < scope:
+ # chain belongs to outer scope
+ break
+ else:
+ # chain match current scope
+ matching_chains.append(chain)
+ # call to `tables_sql` will pop actual_tables
+ tables = self.tables_sql(matching_chains)
+ # cleanup outer join related structure for tables in matching chains
+ for chain in matching_chains:
+ self.outer_chains.remove(chain)
+ for alias in chain:
+ del self.outer_tables[alias]
+ return restrictions, tables
+
+ # tables handling #########################################################
+
+ def add_table(self, table, key=None, scope=-1):
+ if key is None:
+ key = table
+ if key in self.tables:
+ return
+ if scope < 0:
+ scope = len(self.actual_tables) + scope
+ self.tables[key] = (scope, table)
+ self.actual_tables[scope].append(table)
+
+ def alias_and_add_table(self, tablename, scope=-1):
+ alias = '%s%s' % (tablename, self.count)
+ self.count += 1
+ self.add_table('%s AS %s' % (tablename, alias), alias, scope)
+ return alias
+
+ def relation_table(self, relation):
+ """return the table alias used by the given relation"""
+ if relation in self.done:
+ return relation._q_sqltable
+ rid = 'rel_%s%s' % (relation.r_type, self.count)
+ # relation's table is belonging to the root scope if it is the principal
+ # table of one of its variable and that variable belong's to parent
+ # scope
+ for varref in relation.iget_nodes(VariableRef):
+ var = varref.variable
+ # XXX may have a principal without being invariant for this generation,
+ # not sure this is a pb or not
+ if var.stinfo.get('principal') is relation and var.scope is var.stmt:
+ scope = 0
+ break
+ else:
+ scope = -1
+ self.count += 1
+ self.add_table('%s_relation AS %s' % (relation.r_type, rid), rid, scope=scope)
+ relation._q_sqltable = rid
+ self.done.add(relation)
+ return rid
+
+ def fti_table(self, relation, fti_table):
+ """return the table alias used by the given has_text relation,
+ `fti_table` being the table name for the plain text index
+ """
+ if relation in self.done:
+ try:
+ return relation._q_sqltable
+ except AttributeError:
+ pass
+ self.done.add(relation)
+ scope = self.scopes[relation.scope]
+ alias = self.alias_and_add_table(fti_table, scope=scope)
+ relation._q_sqltable = alias
+ return alias
+
+ # outer join handling ######################################################
+
+ def mark_as_used_in_outer_join(self, tablealias, addpending=True):
+ """Mark table of given alias as used in outer join. This must be called
+ after `outer_tables[tablealias]` has been initialized.
+ """
+ # remove a table from actual_table because it's used in an outer join
+ # chain
+ scope, tabledef = self.tables[tablealias]
+ self.actual_tables[scope].remove(tabledef)
+ # check if there are some pending outer join condition for this table
+ if addpending:
+ try:
+ pending_conditions = self.outer_pending.pop(tablealias)
+ except KeyError:
+ pass
+ else:
+ self.outer_tables[tablealias][1].extend(pending_conditions)
+ else:
+ assert not tablealias in self.outer_pending
+
+ def add_outer_join_condition(self, tablealias, condition):
+ try:
+ outer, conditions, chain = self.outer_tables[tablealias]
+ conditions.append(condition)
+ except KeyError:
+ self.outer_pending.setdefault(tablealias, []).append(condition)
+
+ def replace_tables_by_outer_join(self, leftalias, rightalias,
+ outertype, condition):
+ """tell we need <leftalias> <outertype> JOIN <rightalias> ON <condition>
+ """
+ assert leftalias != rightalias, leftalias
+ outer_tables = self.outer_tables
+ louter, lconditions, lchain = outer_tables.get(leftalias,
+ (None, None, None))
+ router, rconditions, rchain = outer_tables.get(rightalias,
+ (None, None, None))
+ if lchain is None and rchain is None:
+ # create a new outer chaine
+ chain = [leftalias, rightalias]
+ outer_tables[leftalias] = (None, [], chain)
+ outer_tables[rightalias] = (outertype, [condition], chain)
+ self.outer_chains.append(chain)
+ self.mark_as_used_in_outer_join(leftalias, addpending=False)
+ self.mark_as_used_in_outer_join(rightalias)
+ elif lchain is None:
+ # [A > B > C] + [D > A] -> [D > A > B > C]
+ if rightalias == rchain[0]:
+ outer_tables[leftalias] = (None, [], rchain)
+ conditions = outer_tables[rightalias][1] + [condition]
+ outer_tables[rightalias] = (outertype, conditions, rchain)
+ rchain.insert(0, leftalias)
+ else:
+ # [A > B > C] + [D > B] -> [A > B > C < D]
+ if outertype == 'LEFT':
+ outertype = 'RIGHT'
+ outer_tables[leftalias] = (outertype, [condition], rchain)
+ rchain.append(leftalias)
+ self.mark_as_used_in_outer_join(leftalias)
+ elif rchain is None:
+ # [A > B > C] + [B > D] -> [A > B > C > D]
+ outer_tables[rightalias] = (outertype, [condition], lchain)
+ lchain.append(rightalias)
+ self.mark_as_used_in_outer_join(rightalias)
+ elif lchain is rchain:
+ # already in the same chain, simply check compatibility and append
+ # the condition if it's ok
+ lidx = lchain.index(leftalias)
+ ridx = lchain.index(rightalias)
+ if (outertype == 'FULL' and router != 'FULL') \
+ or (lidx < ridx and router != 'LEFT') \
+ or (ridx < lidx and louter != 'RIGHT'):
+ raise BadRQLQuery()
+ # merge conditions
+ if lidx < ridx:
+ rconditions.append(condition)
+ else:
+ lconditions.append(condition)
+ elif louter is None:
+ # merge chains
+ self.outer_chains.remove(lchain)
+ rchain += lchain
+ self.mark_as_used_in_outer_join(leftalias)
+ for alias, (aouter, aconditions, achain) in outer_tables.items():
+ if achain is lchain:
+ outer_tables[alias] = (aouter, aconditions, rchain)
+ else:
+ raise BadRQLQuery()
+
+ # sql generation helpers ###################################################
+
+ def tables_sql(self, outer_chains=None):
+ """generate SQL for FROM clause"""
+ # sort for test predictability
+ tables = sorted(self.actual_tables.pop())
+ # process outer joins
+ if outer_chains is None:
+ assert not self.actual_tables, self.actual_tables
+ assert not self.outer_pending
+ outer_chains = self.outer_chains
+ for chain in sorted(outer_chains):
+ tablealias = chain[0]
+ outertype, conditions, _ = self.outer_tables[tablealias]
+ assert _ is chain, (chain, _)
+ assert outertype is None, (chain, self.outer_chains)
+ assert not conditions, (chain, self.outer_chains)
+ assert len(chain) > 1
+ tabledef = self.tables[tablealias][1]
+ outerjoin = [tabledef]
+ for tablealias in chain[1:]:
+ outertype, conditions, _ = self.outer_tables[tablealias]
+ assert _ is chain, (chain, self.outer_chains)
+ assert outertype in ('LEFT', 'RIGHT', 'FULL'), (
+ tablealias, outertype, conditions)
+ assert isinstance(conditions, (list)), (
+ tablealias, outertype, conditions)
+ tabledef = self.tables[tablealias][1]
+ outerjoin.append('%s OUTER JOIN %s ON (%s)' % (
+ outertype, tabledef, ' AND '.join(conditions)))
+ tables.append(' '.join(outerjoin))
+ return ', '.join(tables)
+
+
+def extract_fake_having_terms(having):
+ """RQL's HAVING may be used to contains stuff that should go in the WHERE
+ clause of the SQL query, due to RQL grammar limitation. Split them...
+
+ Return a list nodes that can be ANDed with query's WHERE clause. Having
+ subtrees updated in place.
+ """
+ fakehaving = []
+ for subtree in having:
+ ors, tocheck = set(), []
+ for compnode in subtree.get_nodes(Comparison):
+ for fnode in compnode.get_nodes(Function):
+ if fnode.descr().aggregat:
+ p = compnode.parent
+ oor = None
+ while not isinstance(p, Select):
+ if isinstance(p, (Or, Not)):
+ oor = p
+ p = p.parent
+ if oor is not None:
+ ors.add(oor)
+ break
+ else:
+ tocheck.append(compnode)
+ # tocheck hold a set of comparison not implying an aggregat function
+ # put them in fakehaving if they don't share an Or node as ancestor
+ # with another comparison containing an aggregat function
+ for compnode in tocheck:
+ parents = set()
+ p = compnode.parent
+ oor = None
+ while not isinstance(p, Select):
+ if p in ors or p is None: # p is None for nodes already in fakehaving
+ break
+ if isinstance(p, (Or, Not)):
+ oor = p
+ p = p.parent
+ else:
+ node = oor or compnode
+ fakehaving.append(node)
+ node.parent.remove(node)
+ return fakehaving
+
+
+class SQLGenerator(object):
+ """
+ generation of SQL from the fully expanded RQL syntax tree
+ SQL is designed to be used with a CubicWeb SQL schema
+
+ Groups and sort are not handled here since they should not be handled at
+ this level (see cubicweb.server.querier)
+
+ we should not have errors here!
+
+ WARNING: a CubicWebSQLGenerator instance is not thread safe, but generate is
+ protected by a lock
+ """
+
+ def __init__(self, schema, dbhelper, attrmap=None):
+ self.schema = schema
+ self.dbhelper = dbhelper
+ self.dbencoding = dbhelper.dbencoding
+ self.keyword_map = {'NOW' : self.dbhelper.sql_current_timestamp,
+ 'TODAY': self.dbhelper.sql_current_date,
+ }
+ if not self.dbhelper.union_parentheses_support:
+ self.union_sql = self.noparen_union_sql
+ self._lock = threading.Lock()
+ if attrmap is None:
+ attrmap = {}
+ self.attr_map = attrmap
+
+ def generate(self, union, args=None, varmap=None):
+ """return SQL queries and a variable dictionary from a RQL syntax tree
+
+ :partrqls: a list of couple (rqlst, solutions)
+ :args: optional dictionary with values of substitutions used in the query
+ :varmap: optional dictionary mapping variable name to a special table
+ name, in case the query as to fetch data from temporary tables
+
+ return an sql string and a dictionary with substitutions values
+ """
+ if args is None:
+ args = {}
+ if varmap is None:
+ varmap = {}
+ self._lock.acquire()
+ self._args = args
+ self._varmap = varmap
+ self._query_attrs = {}
+ self._state = None
+ # self._not_scope_offset = 0
+ try:
+ # union query for each rqlst / solution
+ sql = self.union_sql(union)
+ # we are done
+ return sql, self._query_attrs, self._state.needs_source_cb
+ finally:
+ self._lock.release()
+
+ def union_sql(self, union, needalias=False): # pylint: disable=E0202
+ if len(union.children) == 1:
+ return self.select_sql(union.children[0], needalias)
+ sqls = ('(%s)' % self.select_sql(select, needalias)
+ for select in union.children)
+ return '\nUNION ALL\n'.join(sqls)
+
+ def noparen_union_sql(self, union, needalias=False):
+ # needed for sqlite backend which doesn't like parentheses around union
+ # query. This may cause bug in some condition (sort in one of the
+ # subquery) but will work in most case
+ #
+ # see http://www.sqlite.org/cvstrac/tktview?tn=3074
+ sqls = (self.select_sql(select, needalias)
+ for i, select in enumerate(union.children))
+ return '\nUNION ALL\n'.join(sqls)
+
+ def select_sql(self, select, needalias=False):
+ """return SQL queries and a variable dictionary from a RQL syntax tree
+
+ :select: a selection statement of the syntax tree (`rql.stmts.Select`)
+ :solution: a dictionary containing variables binding.
+ A solution's dictionary has variable's names as key and variable's
+ types as values
+ :needwrap: boolean telling if the query will be wrapped in an outer
+ query (to deal with aggregat and/or grouping)
+ """
+ if select.distinct:
+ distinct = True
+ elif self.dbhelper.fti_need_distinct:
+ distinct = getattr(select.parent, 'has_text_query', False)
+ else:
+ distinct = False
+ sorts = select.orderby
+ groups = select.groupby
+ having = select.having
+ for restr in extract_fake_having_terms(having):
+ scope = None
+ for vref in restr.get_nodes(VariableRef):
+ vscope = vref.variable.scope
+ if vscope is select:
+ continue # ignore select scope, so restriction is added to
+ # the inner most scope possible
+ if scope is None:
+ scope = vscope
+ elif vscope is not scope:
+ scope = common_parent(scope, vscope).scope
+ if scope is None:
+ scope = select
+ scope.add_restriction(restr)
+ # remember selection, it may be changed and have to be restored
+ origselection = select.selection[:]
+ # check if the query will have union subquery, if it need sort term
+ # selection (union or distinct query) and wrapping (union with groups)
+ needwrap = False
+ sols = select.solutions
+ selectsortterms = distinct
+ if len(sols) > 1:
+ # remove invariant from solutions
+ sols, existssols, unstable = remove_unused_solutions(
+ select, sols, self._varmap, self.schema)
+ if len(sols) > 1:
+ # if there is still more than one solution, a UNION will be
+ # generated and so sort terms have to be selected
+ selectsortterms = True
+ # and if select is using group by or aggregat, a wrapping
+ # query will be necessary
+ if groups or select.has_aggregat:
+ select.select_only_variables()
+ needwrap = True
+ else:
+ existssols, unstable = {}, ()
+ state = StateInfo(select, existssols, unstable)
+ if self._state is not None:
+ # state from a previous unioned select
+ state.merge_source_cbs(self._state.needs_source_cb)
+ # treat subqueries
+ self._subqueries_sql(select, state)
+ # generate sql for this select node
+ if needwrap:
+ outerselection = origselection[:]
+ if sorts and selectsortterms:
+ if distinct:
+ sort_term_selection(sorts, outerselection, groups)
+ fix_selection_and_group(select, needwrap, selectsortterms,
+ sorts, groups, having)
+ if needwrap:
+ fneedwrap = len(outerselection) != len(origselection)
+ else:
+ fneedwrap = len(select.selection) != len(origselection)
+ if fneedwrap:
+ needalias = True
+ self._in_wrapping_query = False
+ self._state = state
+ try:
+ sql = self._solutions_sql(select, sols, distinct,
+ needalias or needwrap)
+ # generate groups / having before wrapping query selection to get
+ # correct column aliases
+ self._in_wrapping_query = needwrap
+ if groups:
+ # no constant should be inserted in GROUP BY else the backend
+ # will interpret it as a positional index in the selection
+ groups = ','.join(vref.accept(self) for vref in groups
+ if not isinstance(vref, Constant))
+ if having:
+ # filter out constants as for GROUP BY
+ having = ' AND '.join(term.accept(self) for term in having
+ if not isinstance(term, Constant))
+ if needwrap:
+ sql = '%s FROM (%s) AS T1' % (
+ self._selection_sql(outerselection, distinct,needalias),
+ sql)
+ if groups:
+ sql += '\nGROUP BY %s' % groups
+ if having:
+ sql += '\nHAVING %s' % having
+ # sort
+ if sorts:
+ sqlsortterms = []
+ if needwrap:
+ selectidx = [str(term) for term in outerselection]
+ else:
+ selectidx = [str(term) for term in select.selection]
+ for sortterm in sorts:
+ _term = self._sortterm_sql(sortterm, selectidx)
+ if _term is not None:
+ sqlsortterms.append(_term)
+ if sqlsortterms:
+ sql = self.dbhelper.sql_add_order_by(
+ sql, sqlsortterms, origselection, fneedwrap,
+ select.limit or select.offset)
+ else:
+ sqlsortterms = None
+ state.finalize_source_cbs()
+ finally:
+ select.selection = origselection
+ # limit / offset
+ sql = self.dbhelper.sql_add_limit_offset(sql,
+ select.limit,
+ select.offset,
+ sqlsortterms)
+ return sql
+
+ def _subqueries_sql(self, select, state):
+ for i, subquery in enumerate(select.with_):
+ sql = self.union_sql(subquery.query, needalias=True)
+ tablealias = '_T%s' % i # XXX nested subqueries
+ sql = '(%s) AS %s' % (sql, tablealias)
+ state.subtables[tablealias] = (0, sql)
+ latest_state = self._state
+ for vref in subquery.aliases:
+ alias = vref.variable
+ alias._q_sqltable = tablealias
+ alias._q_sql = '%s.C%s' % (tablealias, alias.colnum)
+ try:
+ stack = latest_state.needs_source_cb[alias.colnum]
+ if state.subquery_source_cb is None:
+ state.subquery_source_cb = {}
+ for selectidx, vref in iter_mapped_var_sels(select, alias):
+ stack = stack[:]
+ update_source_cb_stack(state, select, vref, stack)
+ state.subquery_source_cb[selectidx] = stack
+ except KeyError:
+ continue
+
+ def _solutions_sql(self, select, solutions, distinct, needalias):
+ sqls = []
+ for solution in solutions:
+ self._state.reset(solution)
+ # visit restriction subtree
+ if select.where is not None:
+ self._state.add_restriction(select.where.accept(self))
+ sql = [self._selection_sql(select.selection, distinct, needalias)]
+ if self._state.restrictions:
+ sql.append('WHERE %s' % ' AND '.join(self._state.restrictions))
+ self._state.merge_source_cbs(self._state._needs_source_cb)
+ # add required tables
+ assert len(self._state.actual_tables) == 1, self._state.actual_tables
+ tables = self._state.tables_sql()
+ if tables:
+ sql.insert(1, 'FROM %s' % tables)
+ elif self._state.restrictions and self.dbhelper.needs_from_clause:
+ sql.insert(1, 'FROM (SELECT 1) AS _T')
+ sqls.append('\n'.join(sql))
+ if distinct:
+ return '\nUNION\n'.join(sqls)
+ else:
+ return '\nUNION ALL\n'.join(sqls)
+
+ def _selection_sql(self, selected, distinct, needaliasing=False):
+ clause = []
+ for term in selected:
+ sql = term.accept(self)
+ if needaliasing:
+ colalias = 'C%s' % len(clause)
+ clause.append('%s AS %s' % (sql, colalias))
+ if isinstance(term, VariableRef):
+ self._state.aliases[term.name] = colalias
+ else:
+ clause.append(sql)
+ if distinct:
+ return 'SELECT DISTINCT %s' % ', '.join(clause)
+ return 'SELECT %s' % ', '.join(clause)
+
+ def _sortterm_sql(self, sortterm, selectidx):
+ term = sortterm.term
+ try:
+ sqlterm = selectidx.index(str(term)) + 1
+ except ValueError:
+ # Constant node or non selected term
+ sqlterm = term.accept(self)
+ if sqlterm is None:
+ return None
+ if sortterm.asc:
+ return str(sqlterm)
+ else:
+ return '%s DESC' % sqlterm
+
+ def visit_and(self, et):
+ """generate SQL for a AND subtree"""
+ res = []
+ for c in et.children:
+ part = c.accept(self)
+ if part:
+ res.append(part)
+ return ' AND '.join(res)
+
+ def visit_or(self, ou):
+ """generate SQL for a OR subtree"""
+ res = []
+ for c in ou.children:
+ part = c.accept(self)
+ if part:
+ res.append('(%s)' % part)
+ if res:
+ if len(res) > 1:
+ return '(%s)' % ' OR '.join(res)
+ return res[0]
+ return ''
+
+ def visit_not(self, node):
+ csql = node.children[0].accept(self)
+ if node in self._state.done or not csql:
+ # already processed or no sql generated by children
+ return csql
+ return 'NOT (%s)' % csql
+
+ def visit_exists(self, exists):
+ """generate SQL name for a exists subquery"""
+ sqls = []
+ for dummy in self._state.iter_exists_sols(exists):
+ sql = self._visit_exists(exists)
+ if sql:
+ sqls.append(sql)
+ if not sqls:
+ return ''
+ return 'EXISTS(%s)' % ' UNION '.join(sqls)
+
+ def _visit_exists(self, exists):
+ self._state.push_scope(exists)
+ restriction = exists.children[0].accept(self)
+ restrictions, tables = self._state.pop_scope()
+ if restriction:
+ restrictions.append(restriction)
+ restriction = ' AND '.join(restrictions)
+ if not restriction:
+ if tables:
+ return 'SELECT 1 FROM %s' % tables
+ return ''
+ if not tables:
+ # XXX could leave surrounding EXISTS() in this case no?
+ sql = 'SELECT 1 WHERE %s' % restriction
+ else:
+ sql = 'SELECT 1 FROM %s WHERE %s' % (tables, restriction)
+ return sql
+
+
+ def visit_relation(self, relation):
+ """generate SQL for a relation"""
+ rtype = relation.r_type
+ # don't care of type constraint statement (i.e. relation_type = 'is')
+ if relation.is_types_restriction():
+ return ''
+ lhs, rhs = relation.get_parts()
+ rschema = self.schema.rschema(rtype)
+ if rschema.final:
+ if rtype == 'eid' and lhs.variable._q_invariant and \
+ lhs.variable.stinfo['constnode']:
+ # special case where this restriction is already generated by
+ # some other relation
+ return ''
+ # attribute relation
+ if rtype == 'has_text':
+ sql = self._visit_has_text_relation(relation)
+ else:
+ rhs_vars = rhs.get_nodes(VariableRef)
+ if rhs_vars:
+ # if variable(s) in the RHS
+ sql = self._visit_var_attr_relation(relation, rhs_vars)
+ else:
+ # no variables in the RHS
+ sql = self._visit_attribute_relation(relation)
+ elif (rtype == 'is' and isinstance(rhs.children[0], Constant)
+ and rhs.children[0].eval(self._args) is None):
+ # special case "C is NULL"
+ if lhs.name in self._varmap:
+ lhssql = self._varmap[lhs.name]
+ else:
+ lhssql = lhs.accept(self)
+ return '%s%s' % (lhssql, rhs.accept(self))
+ elif '%s.%s' % (lhs, relation.r_type) in self._varmap:
+ # relation has already been processed by a previous step
+ return ''
+ elif relation.optional:
+ # OPTIONAL relation, generate a left|right outer join
+ if rtype == 'identity' or rschema.inlined:
+ sql = self._visit_outer_join_inlined_relation(relation, rschema)
+ else:
+ sql = self._visit_outer_join_relation(relation, rschema)
+ elif rschema.inlined:
+ sql = self._visit_inlined_relation(relation)
+ else:
+ # regular (non final) relation
+ sql = self._visit_relation(relation, rschema)
+ return sql
+
+ def _visit_inlined_relation(self, relation):
+ lhsvar, _, rhsvar, rhsconst = relation_info(relation)
+ # we are sure lhsvar is not None
+ lhssql = self._inlined_var_sql(lhsvar, relation.r_type)
+ if rhsvar is None:
+ moresql = None
+ else:
+ moresql = self._extra_join_sql(relation, lhssql, rhsvar)
+ if isinstance(relation.parent, Not):
+ self._state.done.add(relation.parent)
+ if rhsvar is not None and rhsvar._q_invariant:
+ sql = '%s IS NULL' % lhssql
+ else:
+ # column != 1234 may not get back rows where column is NULL...
+ sql = '(%s IS NULL OR %s!=%s)' % (
+ lhssql, lhssql, (rhsvar or rhsconst).accept(self))
+ elif rhsconst is not None:
+ sql = '%s=%s' % (lhssql, rhsconst.accept(self))
+ elif isinstance(rhsvar, Variable) and rhsvar._q_invariant and \
+ not rhsvar.name in self._varmap:
+ # if the rhs variable is only linked to this relation, this mean we
+ # only want the relation to exists, eg NOT NULL in case of inlined
+ # relation
+ if moresql is not None:
+ return moresql
+ return '%s IS NOT NULL' % lhssql
+ else:
+ sql = '%s=%s' % (lhssql, rhsvar.accept(self))
+ if moresql is None:
+ return sql
+ return '%s AND %s' % (sql, moresql)
+
+ def _process_relation_term(self, relation, rid, termvar, termconst, relfield):
+ if termconst or not termvar._q_invariant:
+ termsql = termconst and termconst.accept(self) or termvar.accept(self)
+ yield '%s.%s=%s' % (rid, relfield, termsql)
+ elif termvar._q_invariant:
+ # if the variable is mapped, generate restriction anyway
+ if termvar.name in self._varmap:
+ termsql = termvar.accept(self)
+ yield '%s.%s=%s' % (rid, relfield, termsql)
+ extrajoin = self._extra_join_sql(relation, '%s.%s' % (rid, relfield), termvar)
+ if extrajoin is not None:
+ yield extrajoin
+
+ def _visit_relation(self, relation, rschema):
+ """generate SQL for a relation
+
+ implements optimization 1.
+ """
+ if relation.r_type == 'identity':
+ # special case "X identity Y"
+ lhs, rhs = relation.get_parts()
+ return '%s%s' % (lhs.accept(self), rhs.accept(self))
+ lhsvar, lhsconst, rhsvar, rhsconst = relation_info(relation)
+ rid = self._state.relation_table(relation)
+ sqls = []
+ sqls += self._process_relation_term(relation, rid, lhsvar, lhsconst, 'eid_from')
+ sqls += self._process_relation_term(relation, rid, rhsvar, rhsconst, 'eid_to')
+ sql = ' AND '.join(sqls)
+ return sql
+
+ def _visit_outer_join_relation(self, relation, rschema):
+ """
+ left outer join syntax (optional=='right'):
+ X relation Y?
+
+ right outer join syntax (optional=='left'):
+ X? relation Y
+
+ full outer join syntaxes (optional=='both'):
+ X? relation Y?
+
+ if relation is inlined:
+ if it's a left outer join:
+ -> X LEFT OUTER JOIN Y ON (X.relation=Y.eid)
+ elif it's a right outer join:
+ -> Y LEFT OUTER JOIN X ON (X.relation=Y.eid)
+ elif it's a full outer join:
+ -> X FULL OUTER JOIN Y ON (X.relation=Y.eid)
+ else:
+ if it's a left outer join:
+ -> X LEFT OUTER JOIN relation ON (relation.eid_from=X.eid)
+ LEFT OUTER JOIN Y ON (relation.eid_to=Y.eid)
+ elif it's a right outer join:
+ -> Y LEFT OUTER JOIN relation ON (relation.eid_to=Y.eid)
+ LEFT OUTER JOIN X ON (relation.eid_from=X.eid)
+ elif it's a full outer join:
+ -> X FULL OUTER JOIN Y ON (X.relation=Y.eid)
+ """
+ leftvar, leftconst, rightvar, rightconst = relation_info(relation)
+ assert not (leftconst and rightconst), "doesn't make sense"
+ if relation.optional == 'left':
+ leftvar, rightvar = rightvar, leftvar
+ leftconst, rightconst = rightconst, leftconst
+ joinattr, restrattr = 'eid_to', 'eid_from'
+ else:
+ joinattr, restrattr = 'eid_from', 'eid_to'
+ # search table for this variable, to use as left table of the outer join
+ leftalias = None
+ if leftvar:
+ # take care, may return None for invariant variable
+ leftalias = self._var_table(leftvar)
+ if leftalias is None:
+ if leftvar.stinfo['principal'] is not relation:
+ # use variable's principal relation
+ leftalias = leftvar.stinfo['principal']._q_sqltable
+ else:
+ # search for relation on which we should join
+ for orelation in leftvar.stinfo['relations']:
+ if (orelation is not relation and
+ not self.schema.rschema(orelation.r_type).final):
+ break
+ else:
+ for orelation in rightvar.stinfo['relations']:
+ if (orelation is not relation and
+ not self.schema.rschema(orelation.r_type).final
+ and orelation.optional):
+ break
+ else:
+ # unexpected
+ assert False, leftvar
+ leftalias = self._state.relation_table(orelation)
+ # right table of the outer join
+ rightalias = self._state.relation_table(relation)
+ # compute join condition
+ if not leftconst or (leftvar and not leftvar._q_invariant):
+ leftsql = leftvar.accept(self)
+ else:
+ leftsql = leftconst.accept(self)
+ condition = '%s.%s=%s' % (rightalias, joinattr, leftsql)
+ if rightconst:
+ condition += ' AND %s.%s=%s' % (rightalias, restrattr, rightconst.accept(self))
+ # record outer join
+ outertype = 'FULL' if relation.optional == 'both' else 'LEFT'
+ self._state.replace_tables_by_outer_join(leftalias, rightalias,
+ outertype, condition)
+ # need another join?
+ if rightconst is None:
+ # we need another outer join for the other side of the relation (e.g.
+ # for "X relation Y?" in RQL, we treated earlier the (cw_X.eid /
+ # relation.eid_from) join, now we've to do (relation.eid_to /
+ # cw_Y.eid)
+ leftalias = rightalias
+ rightsql = rightvar.accept(self) # accept before using var_table
+ rightalias = self._var_table(rightvar)
+ if rightalias is None:
+ if rightvar.stinfo['principal'] is not relation:
+ self._state.replace_tables_by_outer_join(
+ leftalias, rightvar.stinfo['principal']._q_sqltable,
+ outertype, '%s.%s=%s' % (leftalias, restrattr, rightvar.accept(self)))
+ else:
+ self._state.replace_tables_by_outer_join(
+ leftalias, rightalias, outertype,
+ '%s.%s=%s' % (leftalias, restrattr, rightvar.accept(self)))
+ # this relation will hence be expressed in FROM clause, return nothing
+ # here
+ return ''
+
+
+ def _visit_outer_join_inlined_relation(self, relation, rschema):
+ lhsvar, lhsconst, rhsvar, rhsconst = relation_info(relation)
+ assert not (lhsconst and rhsconst), "doesn't make sense"
+ attr = 'eid' if relation.r_type == 'identity' else relation.r_type
+ lhsalias = self._var_table(lhsvar)
+ rhsalias = rhsvar and self._var_table(rhsvar)
+ try:
+ lhssql = self._varmap['%s.%s' % (lhsvar.name, attr)]
+ except KeyError:
+ if lhsalias is None:
+ lhssql = lhsconst.accept(self)
+ elif attr == 'eid':
+ lhssql = lhsvar.accept(self)
+ else:
+ lhssql = '%s.%s%s' % (lhsalias, SQL_PREFIX, attr)
+ condition = '%s=%s' % (lhssql, (rhsconst or rhsvar).accept(self))
+ # this is not a typo, rhs optional variable means lhs outer join and vice-versa
+ if relation.optional == 'left':
+ lhsvar, rhsvar = rhsvar, lhsvar
+ lhsconst, rhsconst = rhsconst, lhsconst
+ lhsalias, rhsalias = rhsalias, lhsalias
+ outertype = 'LEFT'
+ elif relation.optional == 'both':
+ outertype = 'FULL'
+ else:
+ outertype = 'LEFT'
+ if rhsalias is None:
+ if rhsconst is not None:
+ # inlined relation with invariant as rhs
+ if relation.r_type != 'identity':
+ condition = '(%s OR %s IS NULL)' % (condition, lhssql)
+ if not lhsvar.stinfo.get('optrelations'):
+ return condition
+ self._state.add_outer_join_condition(lhsalias, condition)
+ return
+ if lhsalias is None:
+ if lhsconst is not None and not rhsvar.stinfo.get('optrelations'):
+ return condition
+ lhsalias = lhsvar._q_sql.split('.', 1)[0]
+ if lhsalias == rhsalias:
+ self._state.add_outer_join_condition(lhsalias, condition)
+ else:
+ self._state.replace_tables_by_outer_join(
+ lhsalias, rhsalias, outertype, condition)
+ return ''
+
+ def _visit_var_attr_relation(self, relation, rhs_vars):
+ """visit an attribute relation with variable(s) in the RHS
+
+ attribute variables are used either in the selection or for unification
+ (eg X attr1 A, Y attr2 A). In case of selection, nothing to do here.
+ """
+ ored = relation.ored()
+ for vref in rhs_vars:
+ var = vref.variable
+ if var.name in self._varmap:
+ # ensure table is added
+ self._var_info(var)
+ if isinstance(var, ColumnAlias):
+ # force sql generation whatever the computed principal
+ principal = 1
+ else:
+ principal = var.stinfo.get('principal')
+ # we've to return some sql if:
+ # 1. visited relation is ored
+ # 2. variable's principal is not this relation and not 1.
+ if ored or (principal is not None and principal is not relation
+ and not getattr(principal, 'ored', lambda : 0)()):
+ # we have to generate unification expression
+ if principal is relation:
+ # take care if ored case and principal is the relation to
+ # use the right relation in the unification term
+ _rel = [rel for rel in var.stinfo['rhsrelations']
+ if not rel is principal][0]
+ else:
+ _rel = relation
+ lhssql = self._inlined_var_sql(_rel.children[0].variable,
+ _rel.r_type)
+ try:
+ self._state.ignore_varmap = True
+ sql = lhssql + relation.children[1].accept(self)
+ finally:
+ self._state.ignore_varmap = False
+ if relation.optional == 'right':
+ leftalias = self._var_table(principal.children[0].variable)
+ rightalias = self._var_table(relation.children[0].variable)
+ self._state.replace_tables_by_outer_join(
+ leftalias, rightalias, 'LEFT', sql)
+ return ''
+ return sql
+ return ''
+
+ def _visit_attribute_relation(self, rel):
+ """generate SQL for an attribute relation"""
+ lhs, rhs = rel.get_parts()
+ rhssql = rhs.accept(self)
+ table = self._var_table(lhs.variable)
+ if table is None:
+ assert rel.r_type == 'eid'
+ lhssql = lhs.accept(self)
+ else:
+ try:
+ lhssql = self._varmap['%s.%s' % (lhs.name, rel.r_type)]
+ except KeyError:
+ mapkey = '%s.%s' % (self._state.solution[lhs.name], rel.r_type)
+ if mapkey in self.attr_map:
+ cb, sourcecb = self.attr_map[mapkey]
+ if sourcecb:
+ # callback is a source callback, we can't use this
+ # attribute in restriction
+ raise QueryError("can't use %s (%s) in restriction"
+ % (mapkey, rel.as_string()))
+ lhssql = cb(self, lhs.variable, rel)
+ elif rel.r_type == 'eid':
+ lhssql = lhs.variable._q_sql
+ else:
+ lhssql = '%s.%s%s' % (table, SQL_PREFIX, rel.r_type)
+ try:
+ if rel._q_needcast == 'TODAY':
+ sql = 'DATE(%s)%s' % (lhssql, rhssql)
+ # XXX which cast function should be used
+ #elif rel._q_needcast == 'NOW':
+ # sql = 'TIMESTAMP(%s)%s' % (lhssql, rhssql)
+ else:
+ sql = '%s%s' % (lhssql, rhssql)
+ except AttributeError:
+ sql = '%s%s' % (lhssql, rhssql)
+ if lhs.variable.stinfo.get('optrelations'):
+ self._state.add_outer_join_condition(table, sql)
+ else:
+ return sql
+
+ def _visit_has_text_relation(self, rel):
+ """generate SQL for a has_text relation"""
+ lhs, rhs = rel.get_parts()
+ const = rhs.children[0]
+ alias = self._state.fti_table(rel, self.dbhelper.fti_table)
+ jointo = lhs.accept(self)
+ restriction = ''
+ lhsvar = lhs.variable
+ me_is_principal = lhsvar.stinfo.get('principal') is rel
+ if me_is_principal:
+ if lhsvar.stinfo['typerel'] is None:
+ # the variable is using the fti table, no join needed
+ jointo = None
+ elif not lhsvar.name in self._varmap:
+ # join on entities instead of etype's table to get result for
+ # external entities on multisources configurations
+ ealias = lhsvar._q_sqltable = '_' + lhsvar.name
+ jointo = lhsvar._q_sql = '%s.eid' % ealias
+ self._state.add_table('entities AS %s' % ealias, ealias)
+ if not lhsvar._q_invariant or len(lhsvar.stinfo['possibletypes']) == 1:
+ restriction = " AND %s.type='%s'" % (ealias, self._state.solution[lhs.name])
+ else:
+ etypes = ','.join("'%s'" % etype for etype in lhsvar.stinfo['possibletypes'])
+ restriction = " AND %s.type IN (%s)" % (ealias, etypes)
+ if isinstance(rel.parent, Not):
+ self._state.done.add(rel.parent)
+ not_ = True
+ else:
+ not_ = False
+ query = const.eval(self._args)
+ return self.dbhelper.fti_restriction_sql(alias, query,
+ jointo, not_) + restriction
+
+ def visit_comparison(self, cmp):
+ """generate SQL for a comparison"""
+ optional = getattr(cmp, 'optional', None) # rql < 0.30
+ if len(cmp.children) == 2:
+ # simplified expression from HAVING clause
+ lhs, rhs = cmp.children
+ else:
+ lhs = None
+ rhs = cmp.children[0]
+ assert not optional
+ sql = None
+ operator = cmp.operator
+ if operator in ('LIKE', 'ILIKE'):
+ if operator == 'ILIKE' and not self.dbhelper.ilike_support:
+ operator = ' LIKE '
+ else:
+ operator = ' %s ' % operator
+ elif operator == 'REGEXP':
+ sql = ' %s' % self.dbhelper.sql_regexp_match_expression(rhs.accept(self))
+ elif (operator == '=' and isinstance(rhs, Constant)
+ and rhs.eval(self._args) is None):
+ if lhs is None:
+ sql = ' IS NULL'
+ else:
+ sql = '%s IS NULL' % lhs.accept(self)
+ elif isinstance(rhs, Function) and rhs.name == 'IN':
+ assert operator == '='
+ operator = ' '
+ if sql is None:
+ if lhs is None:
+ sql = '%s%s'% (operator, rhs.accept(self))
+ else:
+ sql = '%s%s%s'% (lhs.accept(self), operator, rhs.accept(self))
+ if optional is None:
+ return sql
+ leftvars = cmp.children[0].get_nodes(VariableRef)
+ assert len(leftvars) == 1
+ if leftvars[0].variable.stinfo['attrvar'] is None:
+ assert isinstance(leftvars[0].variable, ColumnAlias)
+ leftalias = leftvars[0].variable._q_sqltable
+ else:
+ leftalias = self._var_table(leftvars[0].variable.stinfo['attrvar'])
+ rightvars = cmp.children[1].get_nodes(VariableRef)
+ assert len(rightvars) == 1
+ if rightvars[0].variable.stinfo['attrvar'] is None:
+ assert isinstance(rightvars[0].variable, ColumnAlias)
+ rightalias = rightvars[0].variable._q_sqltable
+ else:
+ rightalias = self._var_table(rightvars[0].variable.stinfo['attrvar'])
+ if optional == 'right':
+ self._state.replace_tables_by_outer_join(
+ leftalias, rightalias, 'LEFT', sql)
+ elif optional == 'left':
+ self._state.replace_tables_by_outer_join(
+ rightalias, leftalias, 'LEFT', sql)
+ else:
+ self._state.replace_tables_by_outer_join(
+ leftalias, rightalias, 'FULL', sql)
+ return ''
+
+ def visit_mathexpression(self, mexpr):
+ """generate SQL for a mathematic expression"""
+ lhs, rhs = mexpr.get_parts()
+ # check for string concatenation
+ operator = mexpr.operator
+ if operator == '%':
+ operator = '%%'
+ try:
+ if mexpr.operator == '+' and mexpr.get_type(self._state.solution, self._args) == 'String':
+ return '(%s)' % self.dbhelper.sql_concat_string(lhs.accept(self),
+ rhs.accept(self))
+ except CoercionError:
+ pass
+ return '(%s %s %s)'% (lhs.accept(self), operator, rhs.accept(self))
+
+ def visit_unaryexpression(self, uexpr):
+ """generate SQL for a unary expression"""
+ return '%s%s'% (uexpr.operator, uexpr.children[0].accept(self))
+
+ def visit_function(self, func):
+ """generate SQL name for a function"""
+ if func.name == 'FTIRANK':
+ try:
+ rel = next(iter(func.children[0].variable.stinfo['ftirels']))
+ except KeyError:
+ raise BadRQLQuery("can't use FTIRANK on variable not used in an"
+ " 'has_text' relation (eg full-text search)")
+ const = rel.get_parts()[1].children[0]
+ return self.dbhelper.fti_rank_order(
+ self._state.fti_table(rel, self.dbhelper.fti_table),
+ const.eval(self._args))
+ args = [c.accept(self) for c in func.children]
+ if func in self._state.source_cb_funcs:
+ # function executed as a callback on the source
+ assert len(args) == 1
+ return args[0]
+ # func_as_sql will check function is supported by the backend
+ return self.dbhelper.func_as_sql(func.name, args)
+
+ def visit_constant(self, constant):
+ """generate SQL name for a constant"""
+ if constant.type is None:
+ return 'NULL'
+ value = constant.value
+ if constant.type == 'etype':
+ return value
+ # don't substitute int, causes pb when used as sorting column number
+ if constant.type == 'Int':
+ return str(value)
+ if constant.type in ('Date', 'Datetime'):
+ rel = constant.relation()
+ if rel is not None:
+ rel._q_needcast = value
+ return self.keyword_map[value]()
+ if constant.type == 'Substitute':
+ try:
+ # we may found constant from simplified var in varmap
+ return self._mapped_term(constant, '%%(%s)s' % value)[0]
+ except KeyError:
+ _id = value
+ if PY2 and isinstance(_id, unicode):
+ _id = _id.encode()
+ else:
+ _id = str(id(constant)).replace('-', '', 1)
+ self._query_attrs[_id] = value
+ return '%%(%s)s' % _id
+
+ def visit_variableref(self, variableref):
+ """get the sql name for a variable reference"""
+ # use accept, .variable may be a variable or a columnalias
+ return variableref.variable.accept(self)
+
+ def visit_columnalias(self, colalias):
+ """get the sql name for a subquery column alias"""
+ if colalias.name in self._varmap:
+ sql = self._varmap[colalias.name]
+ table = sql.split('.', 1)[0]
+ colalias._q_sqltable = table
+ colalias._q_sql = sql
+ self._state.add_table(table)
+ return sql
+ return colalias._q_sql
+
+ def visit_variable(self, variable):
+ """get the table name and sql string for a variable"""
+ #if contextrels is None and variable.name in self._state.done:
+ if variable.name in self._state.done:
+ if self._in_wrapping_query:
+ return 'T1.%s' % self._state.aliases[variable.name]
+ return variable._q_sql
+ self._state.done.add(variable.name)
+ vtablename = None
+ if not self._state.ignore_varmap and variable.name in self._varmap:
+ sql, vtablename = self._var_info(variable)
+ elif variable.stinfo['attrvar']:
+ # attribute variable (systematically used in rhs of final
+ # relation(s)), get table name and sql from any rhs relation
+ sql = self._linked_var_sql(variable)
+ elif variable._q_invariant:
+ # since variable is invariant, we know we won't found final relation
+ principal = variable.stinfo['principal']
+ if principal is None:
+ vtablename = '_' + variable.name
+ self._state.add_table('entities AS %s' % vtablename, vtablename)
+ sql = '%s.eid' % vtablename
+ if variable.stinfo['typerel'] is not None:
+ # add additional restriction on entities.type column
+ pts = variable.stinfo['possibletypes']
+ if len(pts) == 1:
+ etype = next(iter(variable.stinfo['possibletypes']))
+ restr = "%s.type='%s'" % (vtablename, etype)
+ else:
+ etypes = ','.join("'%s'" % et for et in pts)
+ restr = '%s.type IN (%s)' % (vtablename, etypes)
+ self._state.add_restriction(restr)
+ elif principal.r_type == 'has_text':
+ sql = '%s.%s' % (self._state.fti_table(principal,
+ self.dbhelper.fti_table),
+ self.dbhelper.fti_uid_attr)
+ elif principal in variable.stinfo['rhsrelations']:
+ if self.schema.rschema(principal.r_type).inlined:
+ sql = self._linked_var_sql(variable)
+ else:
+ sql = '%s.eid_to' % self._state.relation_table(principal)
+ else:
+ sql = '%s.eid_from' % self._state.relation_table(principal)
+ else:
+ # standard variable: get table name according to etype and use .eid
+ # attribute
+ sql, vtablename = self._var_info(variable)
+ variable._q_sqltable = vtablename
+ variable._q_sql = sql
+ return sql
+
+ # various utilities #######################################################
+
+ def _extra_join_sql(self, relation, sql, var):
+ # if rhs var is invariant, and this relation is not its principal,
+ # generate extra join
+ try:
+ if not var.stinfo['principal'] is relation:
+ op = relation.operator()
+ if op == '=':
+ # need a predicable result for tests
+ args = sorted( (sql, var.accept(self)) )
+ args.insert(1, op)
+ else:
+ args = (sql, op, var.accept(self))
+ return '%s%s%s' % tuple(args)
+ except KeyError:
+ # no principal defined, relation is necessarily the principal and
+ # so nothing to return here
+ pass
+ return None
+
+ def _temp_table_scope(self, select, table):
+ scope = 9999
+ for var, sql in self._varmap.items():
+ # skip "attribute variable" in varmap (such 'T.login')
+ if not '.' in var and table == sql.split('.', 1)[0]:
+ try:
+ scope = min(scope, self._state.scopes[select.defined_vars[var].scope])
+ except KeyError:
+ scope = 0 # XXX
+ if scope == 0:
+ break
+ return scope
+
+ def _mapped_term(self, term, key):
+ """return sql and table alias to the `term`, mapped as `key` or raise
+ KeyError when the key is not found in the varmap
+ """
+ sql = self._varmap[key]
+ tablealias = sql.split('.', 1)[0]
+ scope = self._temp_table_scope(term.stmt, tablealias)
+ self._state.add_table(tablealias, scope=scope)
+ return sql, tablealias
+
+ def _var_info(self, var):
+ try:
+ return self._mapped_term(var, var.name)
+ except KeyError:
+ scope = self._state.scopes[var.scope]
+ etype = self._state.solution[var.name]
+ # XXX this check should be moved in rql.stcheck
+ if self.schema.eschema(etype).final:
+ raise BadRQLQuery(var.stmt.root)
+ tablealias = '_' + var.name
+ sql = '%s.%seid' % (tablealias, SQL_PREFIX)
+ self._state.add_table('%s%s AS %s' % (SQL_PREFIX, etype, tablealias),
+ tablealias, scope=scope)
+ return sql, tablealias
+
+ def _inlined_var_sql(self, var, rtype):
+ try:
+ sql = self._varmap['%s.%s' % (var.name, rtype)]
+ scope = self._state.scopes[var.scope]
+ self._state.add_table(sql.split('.', 1)[0], scope=scope)
+ except KeyError:
+ # rtype may be an attribute relation when called from
+ # _visit_var_attr_relation. take care about 'eid' rtype, since in
+ # some case we may use the `entities` table, so in that case we've
+ # to properly use variable'sql
+ if rtype == 'eid':
+ sql = var.accept(self)
+ else:
+ sql = '%s.%s%s' % (self._var_table(var), SQL_PREFIX, rtype)
+ return sql
+
+ def _linked_var_sql(self, variable):
+ if not self._state.ignore_varmap:
+ try:
+ return self._varmap[variable.name]
+ except KeyError:
+ pass
+ rel = (variable.stinfo.get('principal') or
+ next(iter(variable.stinfo['rhsrelations'])))
+ linkedvar = rel.children[0].variable
+ if rel.r_type == 'eid':
+ return linkedvar.accept(self)
+ if isinstance(linkedvar, ColumnAlias):
+ raise BadRQLQuery('variable %s should be selected by the subquery'
+ % variable.name)
+ try:
+ sql = self._varmap['%s.%s' % (linkedvar.name, rel.r_type)]
+ except KeyError:
+ mapkey = '%s.%s' % (self._state.solution[linkedvar.name], rel.r_type)
+ if mapkey in self.attr_map:
+ cb, sourcecb = self.attr_map[mapkey]
+ if not sourcecb:
+ return cb(self, linkedvar, rel)
+ # attribute mapped at the source level (bfss for instance)
+ stmt = rel.stmt
+ for selectidx, vref in iter_mapped_var_sels(stmt, variable):
+ stack = [cb]
+ update_source_cb_stack(self._state, stmt, vref, stack)
+ self._state._needs_source_cb[selectidx] = stack
+ linkedvar.accept(self)
+ sql = '%s.%s%s' % (linkedvar._q_sqltable, SQL_PREFIX, rel.r_type)
+ return sql
+
+ # tables handling #########################################################
+
+ def _var_table(self, var):
+ var.accept(self)#.visit_variable(var)
+ return var._q_sqltable