Add missing relation types items in schema global variables
Some of these global variables are used, in particular, to control schema
views (web/views/schema.py).
While they are currently not properly documented (see #3360868), their
definition are currently incomplete (e.g. one could reasonably expect to find
``cw_schema`` in ``SYSTEM_RTYPES``).
Closes #3486114.
# 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 generationis done according to this informationcross RDMS note : read `Comparison of different SQL implementations`_by Troels Arvin. Features SQL ISO Standard, PG, mysql, Oracle, MS SQL, DB2and Informix... _Comparison of different SQL implementations: http://www.troels.arvin.dk/db/rdbms"""__docformat__="restructuredtext en"importthreadingfromdatetimeimportdatetime,timefromlogilab.common.dateimportutcdatetime,utctimefromlogilab.databaseimportFunctionDescr,SQL_FUNCTIONS_REGISTRYfromrqlimportBadRQLQuery,CoercionErrorfromrql.utilsimportcommon_parentfromrql.stmtsimportUnion,Selectfromrql.nodesimport(VariableRef,Constant,Function,Variable,Or,Not,Comparison,ColumnAlias,Relation,SubQuery)fromcubicwebimportQueryErrorfromcubicweb.rqlrewriteimportcleanup_solutionsfromcubicweb.server.sqlutilsimportSQL_PREFIXColumnAlias._q_invariant=False# avoid to check for ColumnAlias / VariableFunctionDescr.source_execute=Nonedefdefault_update_cb_stack(self,stack):stack.append(self.source_execute)FunctionDescr.update_cb_stack=default_update_cb_stackget_func_descr=SQL_FUNCTIONS_REGISTRY.get_functionLENGTH=get_func_descr('LENGTH')deflength_source_execute(source,session,value):returnlen(value.getvalue())LENGTH.source_execute=length_source_executedef_new_var(select,varname):newvar=select.get_variable(varname)ifnot'relations'innewvar.stinfo:# not yet initializednewvar.prepare_annotation()newvar.stinfo['scope']=selectnewvar._q_invariant=Falseselect.selection.append(VariableRef(newvar))returnnewvardef_fill_to_wrap_rel(var,newselect,towrap,schema):forrelinvar.stinfo['relations']-var.stinfo['rhsrelations']:rschema=schema.rschema(rel.r_type)ifrschema.inlined:towrap.add((var,rel))forvrefinrel.children[1].iget_nodes(VariableRef):newivar=_new_var(newselect,vref.name)_fill_to_wrap_rel(vref.variable,newselect,towrap,schema)elifrschema.final:towrap.add((var,rel))forvrefinrel.children[1].iget_nodes(VariableRef):newivar=_new_var(newselect,vref.name)newivar.stinfo['attrvar']=(var,rel.r_type)defrewrite_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=Falseforvarnameintuple(unstable):var=select.defined_vars[varname]ifnotvar.stinfo.get('optrelations'):continueunstable.remove(varname)newselect=Select()myunion=Union()myunion.append(newselect)# extract aliases / selectionnewvar=_new_var(newselect,var.name)newselect.selection=[VariableRef(newvar)]towrap_rels=set()_fill_to_wrap_rel(var,newselect,towrap_rels,schema)# extract relationsforvar,relintowrap_rels:newrel=rel.copy(newselect)newselect.add_restriction(newrel)select.remove_node(rel)var.stinfo['relations'].remove(rel)newvar.stinfo['relations'].add(newrel)ifrel.optionalin('left','both'):newvar.add_optional_relation(newrel)forvrefinnewrel.children[1].iget_nodes(VariableRef):var=vref.variablevar.stinfo['relations'].add(newrel)var.stinfo['rhsrelations'].add(newrel)ifrel.optionalin('right','both'):var.add_optional_relation(newrel)ifnotselect.whereandnotmodified:# oops, generated the same thing as the original select....# restore original query, else we'll indefinitly loopforvar,relintowrap_rels:select.add_restriction(rel)continuemodified=True# extract subquery solutionsmysolutions=[sol.copy()forsolinsolutions]cleanup_solutions(newselect,mysolutions)newselect.set_possible_types(mysolutions)# full sub-queryaliases=[VariableRef(select.get_variable(avar.name,i))fori,avarinenumerate(newselect.selection)]select.add_subquery(SubQuery(aliases,myunion),check=False)returnmodifieddef_new_solutions(rqlst,solutions):"""first filter out subqueries variables from solutions"""newsolutions=[]fororigsolinsolutions:asol={}forvnameinrqlst.defined_vars:asol[vname]=origsol[vname]ifnotasolinnewsolutions:newsolutions.append(asol)returnnewsolutionsdefremove_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={}forvname,varinrqlst.defined_vars.iteritems():vtype=newsols[0][vname]ifvar._q_invariantorvnameinvarmap:# remove invariant variable from solutions to remove duplicates# later, then reinserting a type for the variable even laterforsolinnewsols:invariants.setdefault(id(sol),{})[vname]=sol.pop(vname)elifvar.scopeisnotrqlst:# move appart variables which are in a EXISTS scope and are variatingtry:thisexistssols,thisexistsvars=existssols[var.scope]exceptKeyError:thisexistssols=[newsols[0]]thisexistsvars=set()existssols[var.scope]=thisexistssols,thisexistsvarsforiinxrange(len(newsols)-1,0,-1):ifvtype!=newsols[i][vname]:thisexistssols.append(newsols.pop(i))thisexistsvars.add(vname)else:# remember unstable variablesforiinxrange(1,len(newsols)):ifvtype!=newsols[i][vname]:unstable.add(vname)ifinvariants:# filter out duplicatesnewsols_=[]forsolinnewsols:ifnotsolinnewsols_:newsols_.append(sol)newsols=newsols_# reinsert solutions for invariantsforsolinnewsols:forinvvar,vartypeininvariants[id(sol)].iteritems():sol[invvar]=vartypeforsolinexistssols:try:forinvvar,vartypeininvariants[id(sol)].iteritems():sol[invvar]=vartypeexceptKeyError:continueiflen(newsols)>1:ifrewrite_unstable_outer_join(rqlst,newsols,unstable,schema):# remove variables extracted to subqueries from solutionsnewsols=_new_solutions(rqlst,newsols)returnnewsols,existssols,unstabledefrelation_info(relation):lhs,rhs=relation.get_variable_parts()try:lhs=lhs.variablelhsconst=lhs.stinfo['constnode']exceptAttributeError:lhsconst=lhslhs=NoneexceptKeyError:lhsconst=None# ColumnAliastry:rhs=rhs.variablerhsconst=rhs.stinfo['constnode']exceptAttributeError:rhsconst=rhsrhs=NoneexceptKeyError:rhsconst=None# ColumnAliasreturnlhs,lhsconst,rhs,rhsconstdefsort_term_selection(sorts,rqlst,groups):# XXX beurkifisinstance(rqlst,list):defappend(term):rqlst.append(term)selectionidx=set(str(term)forterminrqlst)else:defappend(term):rqlst.selection.append(term.copy(rqlst))selectionidx=set(str(term)forterminrqlst.selection)forsortterminsorts:term=sortterm.termifnotisinstance(term,Constant)andnotstr(term)inselectionidx:selectionidx.add(str(term))append(term)ifgroups:forvrefinterm.iget_nodes(VariableRef):ifnotvrefingroups:groups.append(vref)deffix_selection_and_group(rqlst,needwrap,selectsortterms,sorts,groups,having):ifselectsorttermsandsorts:sort_term_selection(sorts,rqlst,notneedwrapandgroups)groupvrefs=[vreffortermingroupsforvrefinterm.iget_nodes(VariableRef)]ifsortsandgroups:# when a query is grouped, ensure sort terms are grouped as wellforsortterminsorts:term=sortterm.termifnot(isinstance(term,Constant)or \(isinstance(term,Function)andget_func_descr(term.name).aggregat)):forvrefinterm.iget_nodes(VariableRef):ifnotvrefingroupvrefs:groups.append(vref)groupvrefs.append(vref)ifneedwrapand(groupsorhaving):selectedidx=set(vref.nameforterminrqlst.selectionforvrefinterm.get_nodes(VariableRef))ifgroups:forvrefingroupvrefs:ifvref.namenotinselectedidx:selectedidx.add(vref.name)rqlst.selection.append(vref)ifhaving:forterminhaving:forvrefinterm.iget_nodes(VariableRef):ifvref.namenotinselectedidx:selectedidx.add(vref.name)rqlst.selection.append(vref)defiter_mapped_var_sels(stmt,variable):# variable is a Variable or ColumnAlias node mapped to a source side# callbackifnot(len(variable.stinfo['rhsrelations'])<=1and# < 1 on column aliasvariable.stinfo['selected']):raiseQueryError("can't use %s as a restriction variable"%variable.name)forselectidxinvariable.stinfo['selected']:vrefs=stmt.selection[selectidx].get_nodes(VariableRef)iflen(vrefs)!=1:raiseQueryError()yieldselectidx,vrefs[0]defupdate_source_cb_stack(state,stmt,node,stack):whileTrue:node=node.parentifnodeisstmt:breakifnotisinstance(node,Function):raiseQueryError()funcd=get_func_descr(node.name)iffuncd.source_executeisNone:raiseQueryError('%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 #######################################classStateInfo(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=existssolsself.unstablevars=unstablevarsself.subtables={}self.needs_source_cb=Noneself.subquery_source_cb=Noneself.source_cb_funcs=set()self.scopes={select:0}self.scope_nodes=[]defreset(self,solution):"""reset some visit variables"""self.solution=solutionself.count=0self.done=set()self.tables=self.subtables.copy()self.actual_tables=[[]]for_,tsqlinself.tables.itervalues():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=Falseself._needs_source_cb={}defmerge_source_cbs(self,needs_source_cb):ifself.needs_source_cbisNone:self.needs_source_cb=needs_source_cbelifneeds_source_cb!=self.needs_source_cb:raiseQueryError('query fetch some source mapped attribute, some not')deffinalize_source_cbs(self):ifself.subquery_source_cbisnotNone:self.needs_source_cb.update(self.subquery_source_cb)defadd_restriction(self,restr):ifrestr:self.restrictions.append(restr)defiter_exists_sols(self,exists):ifnotexistsinself.existssols:yield1returnthisexistssols,thisexistsvars=self.existssols[exists]# when iterating other solutions inner to an EXISTS subquery, we should# reset variables which have this exists node as scope at each iterationforvarinexists.stmt.defined_vars.itervalues():ifvar.scopeisexists:thisexistsvars.add(var.name)origsol=self.solutionorigtables=self.tablesdone=self.doneforthisexistssolinthisexistssols:forvnameinself.unstablevars:ifthisexistssol[vname]!=origsol[vname]andvnameinthisexistsvars:breakelse:self.tables=origtables.copy()self.solution=thisexistssolyield1# cleanup self.done from stuff specific to existsforvarinthisexistsvars:ifvarindone:done.remove(var)forrelinexists.iget_nodes(Relation):ifrelindone:done.remove(rel)self.solution=origsolself.tables=origtablesdefpush_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=[]defpop_scope(self):delself.scopes[self.scope_nodes[-1]]self.scope_nodes.pop()restrictions=self.restrictionsself.restrictions=self._restr_stack.pop()scope=len(self.actual_tables)-1# check if we have some outer chain for this scopematching_chains=[]forchaininself.outer_chains:fortablealiasinchain:ifself.tables[tablealias][0]<scope:# chain belongs to outer scopebreakelse:# chain match current scopematching_chains.append(chain)# call to `tables_sql` will pop actual_tablestables=self.tables_sql(matching_chains)# cleanup outer join related structure for tables in matching chainsforchaininmatching_chains:self.outer_chains.remove(chain)foraliasinchain:delself.outer_tables[alias]returnrestrictions,tables# tables handling #########################################################defadd_table(self,table,key=None,scope=-1):ifkeyisNone:key=tableifkeyinself.tables:returnifscope<0:scope=len(self.actual_tables)+scopeself.tables[key]=(scope,table)self.actual_tables[scope].append(table)defalias_and_add_table(self,tablename,scope=-1):alias='%s%s'%(tablename,self.count)self.count+=1self.add_table('%s AS %s'%(tablename,alias),alias,scope)returnaliasdefrelation_table(self,relation):"""return the table alias used by the given relation"""ifrelationinself.done:returnrelation._q_sqltablerid='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# scopeforvarrefinrelation.iget_nodes(VariableRef):var=varref.variable# XXX may have a principal without being invariant for this generation,# not sure this is a pb or notifvar.stinfo.get('principal')isrelationandvar.scopeisvar.stmt:scope=0breakelse:scope=-1self.count+=1self.add_table('%s_relation AS %s'%(relation.r_type,rid),rid,scope=scope)relation._q_sqltable=ridself.done.add(relation)returnriddeffti_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 """ifrelationinself.done:try:returnrelation._q_sqltableexceptAttributeError:passself.done.add(relation)scope=self.scopes[relation.scope]alias=self.alias_and_add_table(fti_table,scope=scope)relation._q_sqltable=aliasreturnalias# outer join handling ######################################################defmark_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# chainscope,tabledef=self.tables[tablealias]self.actual_tables[scope].remove(tabledef)# check if there are some pending outer join condition for this tableifaddpending:try:pending_conditions=self.outer_pending.pop(tablealias)exceptKeyError:passelse:self.outer_tables[tablealias][1].extend(pending_conditions)else:assertnottablealiasinself.outer_pendingdefadd_outer_join_condition(self,tablealias,condition):try:outer,conditions,chain=self.outer_tables[tablealias]conditions.append(condition)exceptKeyError:self.outer_pending.setdefault(tablealias,[]).append(condition)defreplace_tables_by_outer_join(self,leftalias,rightalias,outertype,condition):"""tell we need <leftalias> <outertype> JOIN <rightalias> ON <condition> """assertleftalias!=rightalias,leftaliasouter_tables=self.outer_tableslouter,lconditions,lchain=outer_tables.get(leftalias,(None,None,None))router,rconditions,rchain=outer_tables.get(rightalias,(None,None,None))iflchainisNoneandrchainisNone:# create a new outer chainechain=[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)eliflchainisNone:# [A > B > C] + [D > A] -> [D > A > B > C]ifrightalias==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]ifoutertype=='LEFT':outertype='RIGHT'outer_tables[leftalias]=(outertype,[condition],rchain)rchain.append(leftalias)self.mark_as_used_in_outer_join(leftalias)elifrchainisNone:# [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)eliflchainisrchain:# already in the same chain, simply check compatibility and append# the condition if it's oklidx=lchain.index(leftalias)ridx=lchain.index(rightalias)if(outertype=='FULL'androuter!='FULL') \or(lidx<ridxandrouter!='LEFT') \or(ridx<lidxandlouter!='RIGHT'):raiseBadRQLQuery()# merge conditionsiflidx<ridx:rconditions.append(condition)else:lconditions.append(condition)eliflouterisNone:# merge chainsself.outer_chains.remove(lchain)rchain+=lchainself.mark_as_used_in_outer_join(leftalias)foralias,(aouter,aconditions,achain)inouter_tables.iteritems():ifachainislchain:outer_tables[alias]=(aouter,aconditions,rchain)else:raiseBadRQLQuery()# sql generation helpers ###################################################deftables_sql(self,outer_chains=None):"""generate SQL for FROM clause"""# sort for test predictabilitytables=sorted(self.actual_tables.pop())# process outer joinsifouter_chainsisNone:assertnotself.actual_tables,self.actual_tablesassertnotself.outer_pendingouter_chains=self.outer_chainsforchaininsorted(outer_chains):tablealias=chain[0]outertype,conditions,_=self.outer_tables[tablealias]assert_ischain,(chain,_)assertoutertypeisNone,(chain,self.outer_chains)assertnotconditions,(chain,self.outer_chains)assertlen(chain)>1tabledef=self.tables[tablealias][1]outerjoin=[tabledef]fortablealiasinchain[1:]:outertype,conditions,_=self.outer_tables[tablealias]assert_ischain,(chain,self.outer_chains)assertoutertypein('LEFT','RIGHT','FULL'),(tablealias,outertype,conditions)assertisinstance(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)defextract_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=[]forsubtreeinhaving:ors,tocheck=set(),[]forcompnodeinsubtree.get_nodes(Comparison):forfnodeincompnode.get_nodes(Function):iffnode.descr().aggregat:p=compnode.parentoor=Nonewhilenotisinstance(p,Select):ifisinstance(p,(Or,Not)):oor=pp=p.parentifoorisnotNone:ors.add(oor)breakelse: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 functionforcompnodeintocheck:parents=set()p=compnode.parentoor=Nonewhilenotisinstance(p,Select):ifpinorsorpisNone:# p is None for nodes already in fakehavingbreakifisinstance(p,(Or,Not)):oor=pp=p.parentelse:node=oororcompnodefakehaving.append(node)node.parent.remove(node)returnfakehavingclassSQLGenerator(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=schemaself.dbhelper=dbhelperself.dbencoding=dbhelper.dbencodingself.keyword_map={'NOW':self.dbhelper.sql_current_timestamp,'TODAY':self.dbhelper.sql_current_date,}ifnotself.dbhelper.union_parentheses_support:self.union_sql=self.noparen_union_sqlself._lock=threading.Lock()ifattrmapisNone:attrmap={}self.attr_map=attrmapdefgenerate(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 """ifargsisNone:args={}ifvarmapisNone:varmap={}self._lock.acquire()self._args=argsself._varmap=varmapself._query_attrs={}self._state=None# self._not_scope_offset = 0try:# union query for each rqlst / solutionsql=self.union_sql(union)# we are donereturnsql,self._query_attrs,self._state.needs_source_cbfinally:self._lock.release()defunion_sql(self,union,needalias=False):# pylint: disable=E0202iflen(union.children)==1:returnself.select_sql(union.children[0],needalias)sqls=('(%s)'%self.select_sql(select,needalias)forselectinunion.children)return'\nUNION ALL\n'.join(sqls)defnoparen_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=3074sqls=(self.select_sql(select,needalias)fori,selectinenumerate(union.children))return'\nUNION ALL\n'.join(sqls)defselect_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) """ifselect.distinct:distinct=Trueelifself.dbhelper.fti_need_distinct:distinct=getattr(select.parent,'has_text_query',False)else:distinct=Falsesorts=select.orderbygroups=select.groupbyhaving=select.havingforrestrinextract_fake_having_terms(having):scope=Noneforvrefinrestr.get_nodes(VariableRef):vscope=vref.variable.scopeifvscopeisselect:continue# ignore select scope, so restriction is added to# the inner most scope possibleifscopeisNone:scope=vscopeelifvscopeisnotscope:scope=common_parent(scope,vscope).scopeifscopeisNone:scope=selectscope.add_restriction(restr)# remember selection, it may be changed and have to be restoredorigselection=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=Falsesols=select.solutionsselectsortterms=distinctiflen(sols)>1:# remove invariant from solutionssols,existssols,unstable=remove_unused_solutions(select,sols,self._varmap,self.schema)iflen(sols)>1:# if there is still more than one solution, a UNION will be# generated and so sort terms have to be selectedselectsortterms=True# and if select is using group by or aggregat, a wrapping# query will be necessaryifgroupsorselect.has_aggregat:select.select_only_variables()needwrap=Trueelse:existssols,unstable={},()state=StateInfo(select,existssols,unstable)ifself._stateisnotNone:# state from a previous unioned selectstate.merge_source_cbs(self._state.needs_source_cb)# treat subqueriesself._subqueries_sql(select,state)# generate sql for this select nodeifneedwrap:outerselection=origselection[:]ifsortsandselectsortterms:ifdistinct:sort_term_selection(sorts,outerselection,groups)fix_selection_and_group(select,needwrap,selectsortterms,sorts,groups,having)ifneedwrap:fneedwrap=len(outerselection)!=len(origselection)else:fneedwrap=len(select.selection)!=len(origselection)iffneedwrap:needalias=Trueself._in_wrapping_query=Falseself._state=statetry:sql=self._solutions_sql(select,sols,distinct,needaliasorneedwrap)# generate groups / having before wrapping query selection to get# correct column aliasesself._in_wrapping_query=needwrapifgroups:# no constant should be inserted in GROUP BY else the backend# will interpret it as a positional index in the selectiongroups=','.join(vref.accept(self)forvrefingroupsifnotisinstance(vref,Constant))ifhaving:# filter out constants as for GROUP BYhaving=' AND '.join(term.accept(self)forterminhavingifnotisinstance(term,Constant))ifneedwrap:sql='%s FROM (%s) AS T1'%(self._selection_sql(outerselection,distinct,needalias),sql)ifgroups:sql+='\nGROUP BY %s'%groupsifhaving:sql+='\nHAVING %s'%having# sortifsorts:sqlsortterms=[]ifneedwrap:selectidx=[str(term)forterminouterselection]else:selectidx=[str(term)forterminselect.selection]forsortterminsorts:_term=self._sortterm_sql(sortterm,selectidx)if_termisnotNone:sqlsortterms.append(_term)ifsqlsortterms:sql=self.dbhelper.sql_add_order_by(sql,sqlsortterms,origselection,fneedwrap,select.limitorselect.offset)else:sqlsortterms=Nonestate.finalize_source_cbs()finally:select.selection=origselection# limit / offsetsql=self.dbhelper.sql_add_limit_offset(sql,select.limit,select.offset,sqlsortterms)returnsqldef_subqueries_sql(self,select,state):fori,subqueryinenumerate(select.with_):sql=self.union_sql(subquery.query,needalias=True)tablealias='_T%s'%i# XXX nested subqueriessql='(%s) AS %s'%(sql,tablealias)state.subtables[tablealias]=(0,sql)latest_state=self._stateforvrefinsubquery.aliases:alias=vref.variablealias._q_sqltable=tablealiasalias._q_sql='%s.C%s'%(tablealias,alias.colnum)try:stack=latest_state.needs_source_cb[alias.colnum]ifstate.subquery_source_cbisNone:state.subquery_source_cb={}forselectidx,vrefiniter_mapped_var_sels(select,alias):stack=stack[:]update_source_cb_stack(state,select,vref,stack)state.subquery_source_cb[selectidx]=stackexceptKeyError:continuedef_solutions_sql(self,select,solutions,distinct,needalias):sqls=[]forsolutioninsolutions:self._state.reset(solution)# visit restriction subtreeifselect.whereisnotNone:self._state.add_restriction(select.where.accept(self))sql=[self._selection_sql(select.selection,distinct,needalias)]ifself._state.restrictions:sql.append('WHERE %s'%' AND '.join(self._state.restrictions))self._state.merge_source_cbs(self._state._needs_source_cb)# add required tablesassertlen(self._state.actual_tables)==1,self._state.actual_tablestables=self._state.tables_sql()iftables:sql.insert(1,'FROM %s'%tables)elifself._state.restrictionsandself.dbhelper.needs_from_clause:sql.insert(1,'FROM (SELECT 1) AS _T')sqls.append('\n'.join(sql))ifdistinct:return'\nUNION\n'.join(sqls)else:return'\nUNION ALL\n'.join(sqls)def_selection_sql(self,selected,distinct,needaliasing=False):clause=[]forterminselected:sql=term.accept(self)ifneedaliasing:colalias='C%s'%len(clause)clause.append('%s AS %s'%(sql,colalias))ifisinstance(term,VariableRef):self._state.aliases[term.name]=colaliaselse:clause.append(sql)ifdistinct:return'SELECT DISTINCT %s'%', '.join(clause)return'SELECT %s'%', '.join(clause)def_sortterm_sql(self,sortterm,selectidx):term=sortterm.termtry:sqlterm=selectidx.index(str(term))+1exceptValueError:# Constant node or non selected termsqlterm=term.accept(self)ifsqltermisNone:returnNoneifsortterm.asc:returnstr(sqlterm)else:return'%s DESC'%sqltermdefvisit_and(self,et):"""generate SQL for a AND subtree"""res=[]forcinet.children:part=c.accept(self)ifpart:res.append(part)return' AND '.join(res)defvisit_or(self,ou):"""generate SQL for a OR subtree"""res=[]forcinou.children:part=c.accept(self)ifpart:res.append('(%s)'%part)ifres:iflen(res)>1:return'(%s)'%' OR '.join(res)returnres[0]return''defvisit_not(self,node):csql=node.children[0].accept(self)ifnodeinself._state.doneornotcsql:# already processed or no sql generated by childrenreturncsqlreturn'NOT (%s)'%csqldefvisit_exists(self,exists):"""generate SQL name for a exists subquery"""sqls=[]fordummyinself._state.iter_exists_sols(exists):sql=self._visit_exists(exists)ifsql:sqls.append(sql)ifnotsqls: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()ifrestriction:restrictions.append(restriction)restriction=' AND '.join(restrictions)ifnotrestriction:iftables:return'SELECT 1 FROM %s'%tablesreturn''ifnottables:# XXX could leave surrounding EXISTS() in this case no?sql='SELECT 1 WHERE %s'%restrictionelse:sql='SELECT 1 FROM %s WHERE %s'%(tables,restriction)returnsqldefvisit_relation(self,relation):"""generate SQL for a relation"""rtype=relation.r_type# don't care of type constraint statement (i.e. relation_type = 'is')ifrelation.is_types_restriction():return''lhs,rhs=relation.get_parts()rschema=self.schema.rschema(rtype)ifrschema.final:ifrtype=='eid'andlhs.variable._q_invariantand \lhs.variable.stinfo['constnode']:# special case where this restriction is already generated by# some other relationreturn''# attribute relationifrtype=='has_text':sql=self._visit_has_text_relation(relation)else:rhs_vars=rhs.get_nodes(VariableRef)ifrhs_vars:# if variable(s) in the RHSsql=self._visit_var_attr_relation(relation,rhs_vars)else:# no variables in the RHSsql=self._visit_attribute_relation(relation)elif(rtype=='is'andisinstance(rhs.children[0],Constant)andrhs.children[0].eval(self._args)isNone):# special case "C is NULL"iflhs.nameinself._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)inself._varmap:# relation has already been processed by a previous stepreturn''elifrelation.optional:# OPTIONAL relation, generate a left|right outer joinifrtype=='identity'orrschema.inlined:sql=self._visit_outer_join_inlined_relation(relation,rschema)else:sql=self._visit_outer_join_relation(relation,rschema)elifrschema.inlined:sql=self._visit_inlined_relation(relation)else:# regular (non final) relationsql=self._visit_relation(relation,rschema)returnsqldef_visit_inlined_relation(self,relation):lhsvar,_,rhsvar,rhsconst=relation_info(relation)# we are sure lhsvar is not Nonelhssql=self._inlined_var_sql(lhsvar,relation.r_type)ifrhsvarisNone:moresql=Noneelse:moresql=self._extra_join_sql(relation,lhssql,rhsvar)ifisinstance(relation.parent,Not):self._state.done.add(relation.parent)ifrhsvarisnotNoneandrhsvar._q_invariant:sql='%s IS NULL'%lhssqlelse:# column != 1234 may not get back rows where column is NULL...sql='(%s IS NULL OR %s!=%s)'%(lhssql,lhssql,(rhsvarorrhsconst).accept(self))elifrhsconstisnotNone:sql='%s=%s'%(lhssql,rhsconst.accept(self))elifisinstance(rhsvar,Variable)andrhsvar._q_invariantand \notrhsvar.nameinself._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# relationifmoresqlisnotNone:returnmoresqlreturn'%s IS NOT NULL'%lhssqlelse:sql='%s=%s'%(lhssql,rhsvar.accept(self))ifmoresqlisNone:returnsqlreturn'%s AND %s'%(sql,moresql)def_process_relation_term(self,relation,rid,termvar,termconst,relfield):iftermconstornottermvar._q_invariant:termsql=termconstandtermconst.accept(self)ortermvar.accept(self)yield'%s.%s=%s'%(rid,relfield,termsql)eliftermvar._q_invariant:# if the variable is mapped, generate restriction anywayiftermvar.nameinself._varmap:termsql=termvar.accept(self)yield'%s.%s=%s'%(rid,relfield,termsql)extrajoin=self._extra_join_sql(relation,'%s.%s'%(rid,relfield),termvar)ifextrajoinisnotNone:yieldextrajoindef_visit_relation(self,relation,rschema):"""generate SQL for a relation implements optimization 1. """ifrelation.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)returnsqldef_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)assertnot(leftconstandrightconst),"doesn't make sense"ifrelation.optional=='left':leftvar,rightvar=rightvar,leftvarleftconst,rightconst=rightconst,leftconstjoinattr,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 joinleftalias=Noneifleftvar:# take care, may return None for invariant variableleftalias=self._var_table(leftvar)ifleftaliasisNone:ifleftvar.stinfo['principal']isnotrelation:# use variable's principal relationleftalias=leftvar.stinfo['principal']._q_sqltableelse:# search for relation on which we should joinfororelationinleftvar.stinfo['relations']:if(orelationisnotrelationandnotself.schema.rschema(orelation.r_type).final):breakelse:fororelationinrightvar.stinfo['relations']:if(orelationisnotrelationandnotself.schema.rschema(orelation.r_type).finalandorelation.optional):breakelse:# unexpectedassertFalse,leftvarleftalias=self._state.relation_table(orelation)# right table of the outer joinrightalias=self._state.relation_table(relation)# compute join conditionifnotleftconstor(leftvarandnotleftvar._q_invariant):leftsql=leftvar.accept(self)else:leftsql=leftconst.accept(self)condition='%s.%s=%s'%(rightalias,joinattr,leftsql)ifrightconst:condition+=' AND %s.%s=%s'%(rightalias,restrattr,rightconst.accept(self))# record outer joinoutertype='FULL'ifrelation.optional=='both'else'LEFT'self._state.replace_tables_by_outer_join(leftalias,rightalias,outertype,condition)# need another join?ifrightconstisNone:# 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=rightaliasrightsql=rightvar.accept(self)# accept before using var_tablerightalias=self._var_table(rightvar)ifrightaliasisNone:ifrightvar.stinfo['principal']isnotrelation: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# herereturn''def_visit_outer_join_inlined_relation(self,relation,rschema):lhsvar,lhsconst,rhsvar,rhsconst=relation_info(relation)assertnot(lhsconstandrhsconst),"doesn't make sense"attr='eid'ifrelation.r_type=='identity'elserelation.r_typelhsalias=self._var_table(lhsvar)rhsalias=rhsvarandself._var_table(rhsvar)try:lhssql=self._varmap['%s.%s'%(lhsvar.name,attr)]exceptKeyError:iflhsaliasisNone:lhssql=lhsconst.accept(self)elifattr=='eid':lhssql=lhsvar.accept(self)else:lhssql='%s.%s%s'%(lhsalias,SQL_PREFIX,attr)condition='%s=%s'%(lhssql,(rhsconstorrhsvar).accept(self))# this is not a typo, rhs optional variable means lhs outer join and vice-versaifrelation.optional=='left':lhsvar,rhsvar=rhsvar,lhsvarlhsconst,rhsconst=rhsconst,lhsconstlhsalias,rhsalias=rhsalias,lhsaliasoutertype='LEFT'elifrelation.optional=='both':outertype='FULL'else:outertype='LEFT'ifrhsaliasisNone:ifrhsconstisnotNone:# inlined relation with invariant as rhsifrelation.r_type!='identity':condition='(%s OR %s IS NULL)'%(condition,lhssql)ifnotlhsvar.stinfo.get('optrelations'):returnconditionself._state.add_outer_join_condition(lhsalias,condition)returniflhsaliasisNone:iflhsconstisnotNoneandnotrhsvar.stinfo.get('optrelations'):returnconditionlhsalias=lhsvar._q_sql.split('.',1)[0]iflhsalias==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()forvrefinrhs_vars:var=vref.variableifvar.nameinself._varmap:# ensure table is addedself._var_info(var)ifisinstance(var,ColumnAlias):# force sql generation whatever the computed principalprincipal=1else: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.iforedor(principalisnotNoneandprincipalisnotrelationandnotgetattr(principal,'ored',lambda:0)()):# we have to generate unification expressionifprincipalisrelation:# take care if ored case and principal is the relation to# use the right relation in the unification term_rel=[relforrelinvar.stinfo['rhsrelations']ifnotrelisprincipal][0]else:_rel=relationlhssql=self._inlined_var_sql(_rel.children[0].variable,_rel.r_type)try:self._state.ignore_varmap=Truesql=lhssql+relation.children[1].accept(self)finally:self._state.ignore_varmap=Falseifrelation.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''returnsqlreturn''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)iftableisNone:assertrel.r_type=='eid'lhssql=lhs.accept(self)else:try:lhssql=self._varmap['%s.%s'%(lhs.name,rel.r_type)]exceptKeyError:mapkey='%s.%s'%(self._state.solution[lhs.name],rel.r_type)ifmapkeyinself.attr_map:cb,sourcecb=self.attr_map[mapkey]ifsourcecb:# callback is a source callback, we can't use this# attribute in restrictionraiseQueryError("can't use %s (%s) in restriction"%(mapkey,rel.as_string()))lhssql=cb(self,lhs.variable,rel)elifrel.r_type=='eid':lhssql=lhs.variable._q_sqlelse:lhssql='%s.%s%s'%(table,SQL_PREFIX,rel.r_type)try:ifrel._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)exceptAttributeError:sql='%s%s'%(lhssql,rhssql)iflhs.variable.stinfo.get('optrelations'):self._state.add_outer_join_condition(table,sql)else:returnsqldef_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.variableme_is_principal=lhsvar.stinfo.get('principal')isrelifme_is_principal:iflhsvar.stinfo['typerel']isNone:# the variable is using the fti table, no join neededjointo=Noneelifnotlhsvar.nameinself._varmap:# join on entities instead of etype's table to get result for# external entities on multisources configurationsealias=lhsvar._q_sqltable='_'+lhsvar.namejointo=lhsvar._q_sql='%s.eid'%ealiasself._state.add_table('entities AS %s'%ealias,ealias)ifnotlhsvar._q_invariantorlen(lhsvar.stinfo['possibletypes'])==1:restriction=" AND %s.type='%s'"%(ealias,self._state.solution[lhs.name])else:etypes=','.join("'%s'"%etypeforetypeinlhsvar.stinfo['possibletypes'])restriction=" AND %s.type IN (%s)"%(ealias,etypes)ifisinstance(rel.parent,Not):self._state.done.add(rel.parent)not_=Trueelse:not_=Falsequery=const.eval(self._args)returnself.dbhelper.fti_restriction_sql(alias,query,jointo,not_)+restrictiondefvisit_comparison(self,cmp):"""generate SQL for a comparison"""optional=getattr(cmp,'optional',None)# rql < 0.30iflen(cmp.children)==2:# simplified expression from HAVING clauselhs,rhs=cmp.childrenelse:lhs=Nonerhs=cmp.children[0]assertnotoptionalsql=Noneoperator=cmp.operatorifoperatorin('LIKE','ILIKE'):ifoperator=='ILIKE'andnotself.dbhelper.ilike_support:operator=' LIKE 'else:operator=' %s '%operatorelifoperator=='REGEXP':sql=' %s'%self.dbhelper.sql_regexp_match_expression(rhs.accept(self))elif(operator=='='andisinstance(rhs,Constant)andrhs.eval(self._args)isNone):iflhsisNone:sql=' IS NULL'else:sql='%s IS NULL'%lhs.accept(self)elifisinstance(rhs,Function)andrhs.name=='IN':assertoperator=='='operator=' 'ifsqlisNone:iflhsisNone:sql='%s%s'%(operator,rhs.accept(self))else:sql='%s%s%s'%(lhs.accept(self),operator,rhs.accept(self))ifoptionalisNone:returnsqlleftvars=cmp.children[0].get_nodes(VariableRef)assertlen(leftvars)==1ifleftvars[0].variable.stinfo['attrvar']isNone:assertisinstance(leftvars[0].variable,ColumnAlias)leftalias=leftvars[0].variable._q_sqltableelse:leftalias=self._var_table(leftvars[0].variable.stinfo['attrvar'])rightvars=cmp.children[1].get_nodes(VariableRef)assertlen(rightvars)==1ifrightvars[0].variable.stinfo['attrvar']isNone:assertisinstance(rightvars[0].variable,ColumnAlias)rightalias=rightvars[0].variable._q_sqltableelse:rightalias=self._var_table(rightvars[0].variable.stinfo['attrvar'])ifoptional=='right':self._state.replace_tables_by_outer_join(leftalias,rightalias,'LEFT',sql)elifoptional=='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''defvisit_mathexpression(self,mexpr):"""generate SQL for a mathematic expression"""lhs,rhs=mexpr.get_parts()# check for string concatenationoperator=mexpr.operatorifoperator=='%':operator='%%'try:ifmexpr.operator=='+'andmexpr.get_type(self._state.solution,self._args)=='String':return'(%s)'%self.dbhelper.sql_concat_string(lhs.accept(self),rhs.accept(self))exceptCoercionError:passreturn'(%s%s%s)'%(lhs.accept(self),operator,rhs.accept(self))defvisit_unaryexpression(self,uexpr):"""generate SQL for a unary expression"""return'%s%s'%(uexpr.operator,uexpr.children[0].accept(self))defvisit_function(self,func):"""generate SQL name for a function"""iffunc.name=='FTIRANK':try:rel=iter(func.children[0].variable.stinfo['ftirels']).next()exceptKeyError:raiseBadRQLQuery("can't use FTIRANK on variable not used in an"" 'has_text' relation (eg full-text search)")const=rel.get_parts()[1].children[0]returnself.dbhelper.fti_rank_order(self._state.fti_table(rel,self.dbhelper.fti_table),const.eval(self._args))args=[c.accept(self)forcinfunc.children]iffuncinself._state.source_cb_funcs:# function executed as a callback on the sourceassertlen(args)==1returnargs[0]# func_as_sql will check function is supported by the backendreturnself.dbhelper.func_as_sql(func.name,args)defvisit_constant(self,constant):"""generate SQL name for a constant"""ifconstant.typeisNone:return'NULL'value=constant.valueifconstant.type=='etype':returnvalue# don't substitute int, causes pb when used as sorting column numberifconstant.type=='Int':returnstr(value)ifconstant.typein('Date','Datetime'):rel=constant.relation()ifrelisnotNone:rel._q_needcast=valuereturnself.keyword_map[value]()ifconstant.type=='Substitute':try:# we may found constant from simplified var in varmapreturnself._mapped_term(constant,'%%(%s)s'%value)[0]exceptKeyError:_id=valueifisinstance(_id,unicode):_id=_id.encode()# convert timestamp to utc.# expect SET TiME ZONE to UTC at connection opening time.# This shouldn't change anything for datetime without TZ.value=self._args[_id]ifisinstance(value,datetime)andvalue.tzinfoisnotNone:self._query_attrs[_id]=utcdatetime(value)elifisinstance(value,time)andvalue.tzinfoisnotNone:self._query_attrs[_id]=utctime(value)else:_id=str(id(constant)).replace('-','',1)self._query_attrs[_id]=valuereturn'%%(%s)s'%_iddefvisit_variableref(self,variableref):"""get the sql name for a variable reference"""# use accept, .variable may be a variable or a columnaliasreturnvariableref.variable.accept(self)defvisit_columnalias(self,colalias):"""get the sql name for a subquery column alias"""ifcolalias.nameinself._varmap:sql=self._varmap[colalias.name]table=sql.split('.',1)[0]colalias._q_sqltable=tablecolalias._q_sql=sqlself._state.add_table(table)returnsqlreturncolalias._q_sqldefvisit_variable(self,variable):"""get the table name and sql string for a variable"""#if contextrels is None and variable.name in self._state.done:ifvariable.nameinself._state.done:ifself._in_wrapping_query:return'T1.%s'%self._state.aliases[variable.name]returnvariable._q_sqlself._state.done.add(variable.name)vtablename=Noneifnotself._state.ignore_varmapandvariable.nameinself._varmap:sql,vtablename=self._var_info(variable)elifvariable.stinfo['attrvar']:# attribute variable (systematically used in rhs of final# relation(s)), get table name and sql from any rhs relationsql=self._linked_var_sql(variable)elifvariable._q_invariant:# since variable is invariant, we know we won't found final relationprincipal=variable.stinfo['principal']ifprincipalisNone:vtablename='_'+variable.nameself._state.add_table('entities AS %s'%vtablename,vtablename)sql='%s.eid'%vtablenameifvariable.stinfo['typerel']isnotNone:# add additional restriction on entities.type columnpts=variable.stinfo['possibletypes']iflen(pts)==1:etype=iter(variable.stinfo['possibletypes']).next()restr="%s.type='%s'"%(vtablename,etype)else:etypes=','.join("'%s'"%etforetinpts)restr='%s.type IN (%s)'%(vtablename,etypes)self._state.add_restriction(restr)elifprincipal.r_type=='has_text':sql='%s.%s'%(self._state.fti_table(principal,self.dbhelper.fti_table),self.dbhelper.fti_uid_attr)elifprincipalinvariable.stinfo['rhsrelations']:ifself.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# attributesql,vtablename=self._var_info(variable)variable._q_sqltable=vtablenamevariable._q_sql=sqlreturnsql# various utilities #######################################################def_extra_join_sql(self,relation,sql,var):# if rhs var is invariant, and this relation is not its principal,# generate extra jointry:ifnotvar.stinfo['principal']isrelation:op=relation.operator()ifop=='=':# need a predicable result for testsargs=sorted((sql,var.accept(self)))args.insert(1,op)else:args=(sql,op,var.accept(self))return'%s%s%s'%tuple(args)exceptKeyError:# no principal defined, relation is necessarily the principal and# so nothing to return herepassreturnNonedef_temp_table_scope(self,select,table):scope=9999forvar,sqlinself._varmap.iteritems():# skip "attribute variable" in varmap (such 'T.login')ifnot'.'invarandtable==sql.split('.',1)[0]:try:scope=min(scope,self._state.scopes[select.defined_vars[var].scope])exceptKeyError:scope=0# XXXifscope==0:breakreturnscopedef_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)returnsql,tablealiasdef_var_info(self,var):try:returnself._mapped_term(var,var.name)exceptKeyError:scope=self._state.scopes[var.scope]etype=self._state.solution[var.name]# XXX this check should be moved in rql.stcheckifself.schema.eschema(etype).final:raiseBadRQLQuery(var.stmt.root)tablealias='_'+var.namesql='%s.%seid'%(tablealias,SQL_PREFIX)self._state.add_table('%s%s AS %s'%(SQL_PREFIX,etype,tablealias),tablealias,scope=scope)returnsql,tablealiasdef_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)exceptKeyError:# 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'sqlifrtype=='eid':sql=var.accept(self)else:sql='%s.%s%s'%(self._var_table(var),SQL_PREFIX,rtype)returnsqldef_linked_var_sql(self,variable):ifnotself._state.ignore_varmap:try:returnself._varmap[variable.name]exceptKeyError:passrel=(variable.stinfo.get('principal')oriter(variable.stinfo['rhsrelations']).next())linkedvar=rel.children[0].variableifrel.r_type=='eid':returnlinkedvar.accept(self)ifisinstance(linkedvar,ColumnAlias):raiseBadRQLQuery('variable %s should be selected by the subquery'%variable.name)try:sql=self._varmap['%s.%s'%(linkedvar.name,rel.r_type)]exceptKeyError:mapkey='%s.%s'%(self._state.solution[linkedvar.name],rel.r_type)ifmapkeyinself.attr_map:cb,sourcecb=self.attr_map[mapkey]ifnotsourcecb:returncb(self,linkedvar,rel)# attribute mapped at the source level (bfss for instance)stmt=rel.stmtforselectidx,vrefiniter_mapped_var_sels(stmt,variable):stack=[cb]update_source_cb_stack(self._state,stmt,vref,stack)self._state._needs_source_cb[selectidx]=stacklinkedvar.accept(self)sql='%s.%s%s'%(linkedvar._q_sqltable,SQL_PREFIX,rel.r_type)returnsql# tables handling #########################################################def_var_table(self,var):var.accept(self)#.visit_variable(var)returnvar._q_sqltable