# copyright 2003-2011 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/>."""Functions to add additional annotations on a rql syntax tree to ease latercode generation."""__docformat__="restructuredtext en"fromlogilab.common.compatimportanyfromrqlimportBadRQLQueryfromrql.nodesimportRelation,VariableRef,Constant,Variable,Or,Existsfromrql.utilsimportcommon_parentdef_annotate_select(annotator,rqlst):forsubqueryinrqlst.with_:annotator._annotate_union(subquery.query)#if server.DEBUG:# print '-------- sql annotate', repr(rqlst)getrschema=annotator.schema.rschemahas_text_query=Falseneed_distinct=rqlst.distinctforrelinrqlst.iget_nodes(Relation):ifgetrschema(rel.r_type).symmetricandnotisinstance(rel.parent,Exists):forvrefinrel.iget_nodes(VariableRef):stinfo=vref.variable.stinfoifnotstinfo['constnode']andstinfo['selected']:need_distinct=True# XXX could mark as not invariantbreakforvarinrqlst.defined_vars.itervalues():stinfo=var.stinfoifstinfo.get('ftirels'):has_text_query=Trueifstinfo['attrvar']:stinfo['invariant']=Falsestinfo['principal']=_select_main_var(stinfo['rhsrelations'])continueifnotstinfo['relations']andstinfo['typerel']isNone:# Any X, Any MAX(X)...# those particular queries should be executed using the system# entities table unless there is some type restrictionstinfo['invariant']=Truestinfo['principal']=Nonecontinueifany(relforrelinstinfo['relations']ifrel.r_type=='eid'andrel.operator()!='=')and \notany(rforrinvar.stinfo['relations']-var.stinfo['rhsrelations']ifr.r_type!='eid'and(getrschema(r.r_type).inlinedorgetrschema(r.r_type).final)):# Any X WHERE X eid > 2# those particular queries should be executed using the system entities tablestinfo['invariant']=Truestinfo['principal']=Nonecontinueifstinfo['selected']andvar.valuable_references()==1+bool(stinfo['constnode']):# "Any X", "Any X, Y WHERE X attr Y"stinfo['invariant']=Falsecontinuejoins=set()invariant=Falseforrefinvar.references():rel=ref.relation()ifrelisNoneorrel.is_types_restriction():continuelhs,rhs=rel.get_parts()onlhs=refislhsrole='subject'ifonlhselse'object'ifrel.r_type=='eid':ifnot(onlhsandlen(stinfo['relations'])>1):breakifnotstinfo['constnode']:joins.add((rel,role))continueelifrel.r_type=='identity':# identity can't be used as principal, so check other relation are used# XXX explain rhs.operator == '='ifrhs.operator!='='orlen(stinfo['relations'])<=1:#(stinfo['constnode'] and rhs.operator == '='):breakjoins.add((rel,role))continuerschema=getrschema(rel.r_type)ifrel.optional:ifrelinstinfo.get('optrelations',()):# optional variable can't be invariant if this is the lhs# variable of an inlined relationifnotrelinstinfo['rhsrelations']andrschema.inlined:break# variable used as main variable of an optional relation can't# be invariant, unless we can use some other relation as# reference for the outer joinelifnotstinfo['constnode']:breakeliflen(stinfo['relations'])==2:ifonlhs:ostinfo=rhs.children[0].variable.stinfoelse:ostinfo=lhs.variable.stinfoifnot(ostinfo.get('optcomparisons')orany(orelfororelinostinfo['relations']iforel.optionalandorelisnotrel)):breakifrschema.finalor(onlhsandrschema.inlined):ifrschema.type!='has_text':# need join anyway if the variable appears in a final or# inlined relationbreakjoins.add((rel,role))continueifnotstinfo['constnode']:ifrschema.inlinedandrel.neged(strict=True):# if relation is inlined, can't be invariant if that# variable is used anywhere else.# see 'Any P WHERE NOT N ecrit_par P, N eid 512':# sql for 'NOT N ecrit_par P' is 'N.ecrit_par is NULL' so P# can use N.ecrit_par as principalif(stinfo['selected']orlen(stinfo['relations'])>1):breakelifrschema.symmetricandstinfo['selected']:breakjoins.add((rel,role))else:# if there is at least one ambigous relation and no other to# restrict types, can't be invariant since we need to filter out# other typesifnotannotator.is_ambiguous(var):invariant=Truestinfo['invariant']=invariantifinvariantandjoins:# remember rqlst/solutions analyze information# we have to select a kindof "main" relation which will "extrajoins"# the other# priority should be given to relation which are not in inner queries# (eg exists)try:stinfo['principal']=principal=_select_principal(var.scope,joins)ifgetrschema(principal.r_type).inlined:# the scope of the lhs variable must be equal or outer to the# rhs variable's scope (since it's retrieved from lhs's table)sstinfo=principal.children[0].variable.stinfosstinfo['scope']=common_parent(sstinfo['scope'],stinfo['scope']).scopeexceptCantSelectPrincipal:stinfo['invariant']=Falserqlst.need_distinct=need_distinctreturnhas_text_queryclassCantSelectPrincipal(Exception):"""raised when no 'principal' variable can be found"""def_select_principal(scope,relations,_sort=lambdax:x):"""given a list of rqlst relations, select one which will be used to represent an invariant variable (e.g. using on extremity of the relation instead of the variable's type table """# _sort argument is there for testdiffscope_rels={}ored_rels=set()diffscope_rels=set()forrel,rolein_sort(relations):# note: only eid and has_text among all final relations may be thereifrel.r_typein('eid','identity'):continueifrel.optionalisnotNoneandlen(relations)>1:ifrole=='subject'andrel.optional=='right':continueifrole=='object'andrel.optional=='left':continueifrel.ored(traverse_scope=True):ored_rels.add(rel)elifrel.scopeisscope:returnrelelifnotrel.neged(traverse_scope=True):diffscope_rels.add(rel)iflen(ored_rels)>1:ored_rels_copy=tuple(ored_rels)forrel1inored_rels_copy:forrel2inored_rels_copy:ifrel1isrel2:continueifisinstance(common_parent(rel1,rel2),Or):ored_rels.discard(rel1)ored_rels.discard(rel2)forrelin_sort(ored_rels):ifrel.scopeisscope:returnreldiffscope_rels.add(rel)# if DISTINCT query, can use variable from a different scope as principal# since introduced duplicates will be removedifscope.stmt.distinctanddiffscope_rels:returniter(_sort(diffscope_rels)).next()# XXX could use a relation from a different scope if it can't generate# duplicates, so we should have to check cardinalityraiseCantSelectPrincipal()def_select_main_var(relations):"""given a list of rqlst relations, select one which will be used as main relation for the rhs variable """principal=Noneothers=[]# sort for test predictabilityforrelinsorted(relations,key=lambdax:(x.children[0].name,x.r_type)):# only equality relation with a variable as rhs may be principalifrel.operator()notin('=','IS') \ornotisinstance(rel.children[1].children[0],VariableRef)orrel.neged(strict=True):continueifrel.optional:others.append(rel)continueifrel.scopeisrel.stmt:returnrelprincipal=relifprincipalisNone:ifothers:returnothers[0]raiseBadRQLQuery('unable to find principal in %s'%', '.join(r.as_string()forrinrelations))returnprincipaldefset_qdata(getrschema,union,noinvariant):"""recursive function to set querier data on variables in the syntax tree """forselectinunion.children:forsubqueryinselect.with_:set_qdata(getrschema,subquery.query,noinvariant)forvarinselect.defined_vars.itervalues():ifvar.stinfo['invariant']:ifvarinnoinvariantandnotvar.stinfo['principal'].r_type=='has_text':var._q_invariant=Falseelse:var._q_invariant=Trueelse:var._q_invariant=FalseclassSQLGenAnnotator(object):def__init__(self,schema):self.schema=schemaself.nfdomain=frozenset(eschema.typeforeschemainschema.entities()ifnoteschema.final)defannotate(self,rqlst):"""add information to the rql syntax tree to help sources to do their job (read sql generation) a variable is tagged as invariant if: * it's a non final variable * it's not used as lhs in any final or inlined relation * there is no type restriction on this variable (either explicit in the syntax tree or because a solution for this variable has been removed due to security filtering) """#assert rqlst.TYPE == 'select', rqlstrqlst.has_text_query=self._annotate_union(rqlst)def_annotate_union(self,union):has_text_query=Falseforselectinunion.children:htq=_annotate_select(self,select)ifhtq:has_text_query=Truereturnhas_text_querydefis_ambiguous(self,var):# ignore has_text relation when we know it will be used as principal.# This is expected by the rql2sql generator which will use the `entities`# table to filter out by type if necessary, This optimisation is very# interesting in multi-sources cases, as it may avoid a costly query# on sources to get all entities of a given type to achieve this, while# we have all the necessary information.root=var.stmt.root# Union node# rel.scope -> Select or Exists node, so add .parent to get Union from# Select noderels=[relforrelinvar.stinfo['relations']ifrel.scope.parentisroot]iflen(rels)==1andrels[0].r_type=='has_text':returnFalsetry:data=var.stmt._deamb_dataexceptAttributeError:data=var.stmt._deamb_data=IsAmbData(self.schema,self.nfdomain)data.compute(var.stmt)returndata.is_ambiguous(var)classIsAmbData(object):def__init__(self,schema,nfdomain):self.schema=schema# shortcutsself.rschema=schema.rschemaself.eschema=schema.eschema# domain for non final variablesself.nfdomain=nfdomain# {var: possible solutions set}self.varsols={}# set of ambiguous variablesself.ambiguousvars=set()# remember if a variable has been deambiguified by another to avoid# doing the oppositeself.deambification_map={}# not invariant variables (access to final.inlined relation)self.not_invariants=set()defis_ambiguous(self,var):returnvarinself.ambiguousvarsdefrestrict(self,var,restricted_domain):self.varsols[var]&=restricted_domainifvarinself.ambiguousvarsandself.varsols[var]==var.stinfo['possibletypes']:self.ambiguousvars.remove(var)defcompute(self,rqlst):# set domains for each variableforvarname,varinrqlst.defined_vars.iteritems():ifvar.stinfo['uidrel']isnotNoneor \self.eschema(rqlst.solutions[0][varname]).final:ptypes=var.stinfo['possibletypes']else:ptypes=set(self.nfdomain)self.ambiguousvars.add(var)self.varsols[var]=ptypesifnotself.ambiguousvars:return# apply relation restrictionself.maydeambrels=maydeambrels={}forrelinrqlst.iget_nodes(Relation):ifrel.r_type=='eid'orrel.is_types_restriction():continuelhs,rhs=rel.get_variable_parts()ifisinstance(lhs,VariableRef)orisinstance(rhs,VariableRef):rschema=self.rschema(rel.r_type)ifrschema.inlinedorrschema.final:self.not_invariants.add(lhs.variable)self.set_rel_constraint(lhs,rel,rschema.subjects)self.set_rel_constraint(rhs,rel,rschema.objects)# try to deambiguify more variables by considering other variables'typemodified=Truewhilemodifiedandself.ambiguousvars:modified=Falseforvarinself.ambiguousvars.copy():try:forrelin(var.stinfo['relations']&maydeambrels[var]):ifself.deambiguifying_relation(var,rel):modified=TruebreakexceptKeyError:# no relation to deambiguifycontinuedef_debug_print(self):print'varsols',dict((x,sorted(str(v)forvinvalues))forx,valuesinself.varsols.iteritems())print'ambiguous vars',sorted(self.ambiguousvars)defset_rel_constraint(self,term,rel,etypes_func):ifisinstance(term,VariableRef)andself.is_ambiguous(term.variable):var=term.variableiflen(var.stinfo['relations'])==1 \orrel.scopeisvar.scopeorrel.r_type=='identity':self.restrict(var,frozenset(etypes_func()))try:self.maydeambrels[var].add(rel)exceptKeyError:self.maydeambrels[var]=set((rel,))defdeambiguifying_relation(self,var,rel):lhs,rhs=rel.get_variable_parts()onlhs=varisgetattr(lhs,'variable',None)other=onlhsandrhsorlhsotheretypes=None# XXX isinstance(other.variable, Variable) to skip column aliasifisinstance(other,VariableRef)andisinstance(other.variable,Variable):deambiguifier=other.variableifnotvarisself.deambification_map.get(deambiguifier):ifvar.stinfo['typerel']isNone:otheretypes=deambiguifier.stinfo['possibletypes']elifnotself.is_ambiguous(deambiguifier):otheretypes=self.varsols[deambiguifier]elifdeambiguifierinself.not_invariants:# we know variable won't be invariant, try to use# it to deambguify the current variableotheretypes=self.varsols[deambiguifier]ifdeambiguifier.stinfo['typerel']isNone:# if deambiguifier has no type restriction using 'is',# don't record itdeambiguifier=Noneelifisinstance(other,Constant)andother.uidtype:otheretypes=(other.uidtype,)deambiguifier=NoneifotheretypesisnotNone:# to restrict, we must check that for all type in othertypes,# possible types on the other end of the relation are matching# variable's possible typesrschema=self.rschema(rel.r_type)ifonlhs:rtypefunc=rschema.subjectselse:rtypefunc=rschema.objectsforotheretypeinotheretypes:reltypes=frozenset(rtypefunc(otheretype))ifvar.stinfo['possibletypes']!=reltypes:returnFalseself.restrict(var,var.stinfo['possibletypes'])self.deambification_map[var]=deambiguifierreturnTruereturnFalse