cubicweb/server/rqlannotation.py
changeset 11703 670aa9bf0b6c
parent 11057 0b59724cb3f2
child 11770 22b854d3e8b2
equal deleted inserted replaced
11702:be23c3813bbf 11703:670aa9bf0b6c
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    16 # You should have received a copy of the GNU Lesser General Public License along
    16 # You should have received a copy of the GNU Lesser General Public License along
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """Functions to add additional annotations on a rql syntax tree to ease later
    18 """Functions to add additional annotations on a rql syntax tree to ease later
    19 code generation.
    19 code generation.
    20 """
    20 """
       
    21 
    21 from __future__ import print_function
    22 from __future__ import print_function
    22 
    23 
    23 __docformat__ = "restructuredtext en"
       
    24 
       
    25 from rql import BadRQLQuery
    24 from rql import BadRQLQuery
    26 from rql.nodes import Relation, VariableRef, Constant, Variable, Or, Exists
    25 from rql.nodes import Relation, VariableRef, Constant, Variable, Or
    27 from rql.utils import common_parent
    26 from rql.utils import common_parent
       
    27 
    28 
    28 
    29 def _annotate_select(annotator, rqlst):
    29 def _annotate_select(annotator, rqlst):
    30     has_text_query = False
    30     has_text_query = False
    31     for subquery in rqlst.with_:
    31     for subquery in rqlst.with_:
    32         if annotator._annotate_union(subquery.query):
    32         if annotator._annotate_union(subquery.query):
    33             has_text_query = True
    33             has_text_query = True
    34     #if server.DEBUG:
       
    35     #    print '-------- sql annotate', repr(rqlst)
       
    36     getrschema = annotator.schema.rschema
    34     getrschema = annotator.schema.rschema
    37     for var in rqlst.defined_vars.values():
    35     for var in rqlst.defined_vars.values():
    38         stinfo = var.stinfo
    36         stinfo = var.stinfo
    39         if stinfo.get('ftirels'):
    37         if stinfo.get('ftirels'):
    40             has_text_query = True
    38             has_text_query = True
    47             # those particular queries should be executed using the system
    45             # those particular queries should be executed using the system
    48             # entities table unless there is some type restriction
    46             # entities table unless there is some type restriction
    49             stinfo['invariant'] = True
    47             stinfo['invariant'] = True
    50             stinfo['principal'] = None
    48             stinfo['principal'] = None
    51             continue
    49             continue
    52         if any(rel for rel in stinfo['relations'] if rel.r_type == 'eid' and rel.operator() != '=') and \
    50         if (any(rel for rel in stinfo['relations'] if rel.r_type == 'eid' and rel.operator() != '=')
    53                not any(r for r in var.stinfo['relations'] - var.stinfo['rhsrelations']
    51                 and not any(r for r in var.stinfo['relations'] - var.stinfo['rhsrelations']
    54                        if r.r_type != 'eid' and (getrschema(r.r_type).inlined or getrschema(r.r_type).final)):
    52                             if r.r_type != 'eid'
       
    53                             and (getrschema(r.r_type).inlined or getrschema(r.r_type).final))):
    55             # Any X WHERE X eid > 2
    54             # Any X WHERE X eid > 2
    56             # those particular queries should be executed using the system entities table
    55             # those particular queries should be executed using the system entities table
    57             stinfo['invariant'] = True
    56             stinfo['invariant'] = True
    58             stinfo['principal'] = None
    57             stinfo['principal'] = None
    59             continue
    58             continue
    60         if stinfo['selected'] and var.valuable_references() == 1+bool(stinfo['constnode']):
    59         if stinfo['selected'] and var.valuable_references() == 1 + bool(stinfo['constnode']):
    61             # "Any X", "Any X, Y WHERE X attr Y"
    60             # "Any X", "Any X, Y WHERE X attr Y"
    62             stinfo['invariant'] = False
    61             stinfo['invariant'] = False
    63             continue
    62             continue
    64         joins = set()
    63         joins = set()
    65         invariant = False
    64         invariant = False
    72             role = 'subject' if onlhs else 'object'
    71             role = 'subject' if onlhs else 'object'
    73             if rel.r_type == 'eid':
    72             if rel.r_type == 'eid':
    74                 if not (onlhs and len(stinfo['relations']) > 1):
    73                 if not (onlhs and len(stinfo['relations']) > 1):
    75                     break
    74                     break
    76                 if not stinfo['constnode']:
    75                 if not stinfo['constnode']:
    77                     joins.add( (rel, role) )
    76                     joins.add((rel, role))
    78                 continue
    77                 continue
    79             elif rel.r_type == 'identity':
    78             elif rel.r_type == 'identity':
    80                 # identity can't be used as principal, so check other relation are used
    79                 # identity can't be used as principal, so check other relation are used
    81                 # XXX explain rhs.operator == '='
    80                 # XXX explain rhs.operator == '='
    82                 if rhs.operator != '=' or len(stinfo['relations']) <= 1: #(stinfo['constnode'] and rhs.operator == '='):
    81                 if rhs.operator != '=' or len(stinfo['relations']) <= 1:
    83                     break
    82                     break
    84                 joins.add( (rel, role) )
    83                 joins.add((rel, role))
    85                 continue
    84                 continue
    86             rschema = getrschema(rel.r_type)
    85             rschema = getrschema(rel.r_type)
    87             if rel.optional:
    86             if rel.optional:
    88                 if rel in stinfo.get('optrelations', ()):
    87                 if rel in stinfo.get('optrelations', ()):
    89                     # optional variable can't be invariant if this is the lhs
    88                     # optional variable can't be invariant if this is the lhs
    90                     # variable of an inlined relation
    89                     # variable of an inlined relation
    91                     if not rel in stinfo['rhsrelations'] and rschema.inlined:
    90                     if rel not in stinfo['rhsrelations'] and rschema.inlined:
    92                         break
    91                         break
    93                 # variable used as main variable of an optional relation can't
    92                 # variable used as main variable of an optional relation can't
    94                 # be invariant, unless we can use some other relation as
    93                 # be invariant, unless we can use some other relation as
    95                 # reference for the outer join
    94                 # reference for the outer join
    96                 elif not stinfo['constnode']:
    95                 elif not stinfo['constnode']:
   107             if rschema.final or (onlhs and rschema.inlined):
   106             if rschema.final or (onlhs and rschema.inlined):
   108                 if rschema.type != 'has_text':
   107                 if rschema.type != 'has_text':
   109                     # need join anyway if the variable appears in a final or
   108                     # need join anyway if the variable appears in a final or
   110                     # inlined relation
   109                     # inlined relation
   111                     break
   110                     break
   112                 joins.add( (rel, role) )
   111                 joins.add((rel, role))
   113                 continue
   112                 continue
   114             if not stinfo['constnode']:
   113             if not stinfo['constnode']:
   115                 if rschema.inlined and rel.neged(strict=True):
   114                 if rschema.inlined and rel.neged(strict=True):
   116                     # if relation is inlined, can't be invariant if that
   115                     # if relation is inlined, can't be invariant if that
   117                     # variable is used anywhere else.
   116                     # variable is used anywhere else.
   118                     # see 'Any P WHERE NOT N ecrit_par P, N eid 512':
   117                     # see 'Any P WHERE NOT N ecrit_par P, N eid 512':
   119                     # sql for 'NOT N ecrit_par P' is 'N.ecrit_par is NULL' so P
   118                     # sql for 'NOT N ecrit_par P' is 'N.ecrit_par is NULL' so P
   120                     # can use N.ecrit_par as principal
   119                     # can use N.ecrit_par as principal
   121                     if (stinfo['selected'] or len(stinfo['relations']) > 1):
   120                     if (stinfo['selected'] or len(stinfo['relations']) > 1):
   122                         break
   121                         break
   123             joins.add( (rel, role) )
   122             joins.add((rel, role))
   124         else:
   123         else:
   125             # if there is at least one ambigous relation and no other to
   124             # if there is at least one ambigous relation and no other to
   126             # restrict types, can't be invariant since we need to filter out
   125             # restrict types, can't be invariant since we need to filter out
   127             # other types
   126             # other types
   128             if not annotator.is_ambiguous(var):
   127             if not annotator.is_ambiguous(var):
   149         if col_alias.stinfo.get('ftirels'):
   148         if col_alias.stinfo.get('ftirels'):
   150             has_text_query = True
   149             has_text_query = True
   151     return has_text_query
   150     return has_text_query
   152 
   151 
   153 
   152 
   154 
       
   155 class CantSelectPrincipal(Exception):
   153 class CantSelectPrincipal(Exception):
   156     """raised when no 'principal' variable can be found"""
   154     """raised when no 'principal' variable can be found"""
   157 
   155 
   158 def _select_principal(scope, relations, _sort=lambda x:x):
   156 
       
   157 def _select_principal(scope, relations, _sort=lambda x: x):
   159     """given a list of rqlst relations, select one which will be used to
   158     """given a list of rqlst relations, select one which will be used to
   160     represent an invariant variable (e.g. using on extremity of the relation
   159     represent an invariant variable (e.g. using on extremity of the relation
   161     instead of the variable's type table
   160     instead of the variable's type table
   162     """
   161     """
   163     # _sort argument is there for test
   162     # _sort argument is there for test
   198         return next(iter(_sort(diffscope_rels)))
   197         return next(iter(_sort(diffscope_rels)))
   199     # XXX could use a relation from a different scope if it can't generate
   198     # XXX could use a relation from a different scope if it can't generate
   200     # duplicates, so we should have to check cardinality
   199     # duplicates, so we should have to check cardinality
   201     raise CantSelectPrincipal()
   200     raise CantSelectPrincipal()
   202 
   201 
       
   202 
   203 def _select_main_var(relations):
   203 def _select_main_var(relations):
   204     """given a list of rqlst relations, select one which will be used as main
   204     """given a list of rqlst relations, select one which will be used as main
   205     relation for the rhs variable
   205     relation for the rhs variable
   206     """
   206     """
   207     principal = None
   207     principal = None
   208     others = []
   208     others = []
   209     # sort for test predictability
   209     # sort for test predictability
   210     for rel in sorted(relations, key=lambda x: (x.children[0].name, x.r_type)):
   210     for rel in sorted(relations, key=lambda x: (x.children[0].name, x.r_type)):
   211         # only equality relation with a variable as rhs may be principal
   211         # only equality relation with a variable as rhs may be principal
   212         if rel.operator() not in ('=', 'IS') \
   212         if (rel.operator() not in ('=', 'IS')
   213                or not isinstance(rel.children[1].children[0], VariableRef) or rel.neged(strict=True):
   213                 or not isinstance(rel.children[1].children[0], VariableRef)
       
   214                 or rel.neged(strict=True)):
   214             continue
   215             continue
   215         if rel.optional:
   216         if rel.optional:
   216             others.append(rel)
   217             others.append(rel)
   217             continue
   218             continue
   218         if rel.scope is rel.stmt:
   219         if rel.scope is rel.stmt:
   257         * it's not used as lhs in any final or inlined relation
   258         * it's not used as lhs in any final or inlined relation
   258         * there is no type restriction on this variable (either explicit in the
   259         * there is no type restriction on this variable (either explicit in the
   259           syntax tree or because a solution for this variable has been removed
   260           syntax tree or because a solution for this variable has been removed
   260           due to security filtering)
   261           due to security filtering)
   261         """
   262         """
   262         #assert rqlst.TYPE == 'select', rqlst
   263         # assert rqlst.TYPE == 'select', rqlst
   263         rqlst.has_text_query = self._annotate_union(rqlst)
   264         rqlst.has_text_query = self._annotate_union(rqlst)
   264 
   265 
   265     def _annotate_union(self, union):
   266     def _annotate_union(self, union):
   266         has_text_query = False
   267         has_text_query = False
   267         for select in union.children:
   268         for select in union.children:
   274         # This is expected by the rql2sql generator which will use the `entities`
   275         # This is expected by the rql2sql generator which will use the `entities`
   275         # table to filter out by type if necessary, This optimisation is very
   276         # table to filter out by type if necessary, This optimisation is very
   276         # interesting in multi-sources cases, as it may avoid a costly query
   277         # interesting in multi-sources cases, as it may avoid a costly query
   277         # on sources to get all entities of a given type to achieve this, while
   278         # on sources to get all entities of a given type to achieve this, while
   278         # we have all the necessary information.
   279         # we have all the necessary information.
   279         root = var.stmt.root # Union node
   280         root = var.stmt.root  # Union node
   280         # rel.scope -> Select or Exists node, so add .parent to get Union from
   281         # rel.scope -> Select or Exists node, so add .parent to get Union from
   281         # Select node
   282         # Select node
   282         rels = [rel for rel in var.stinfo['relations'] if rel.scope.parent is root]
   283         rels = [rel for rel in var.stinfo['relations'] if rel.scope.parent is root]
   283         if len(rels) == 1 and rels[0].r_type == 'has_text':
   284         if len(rels) == 1 and rels[0].r_type == 'has_text':
   284             return False
   285             return False
   317             self.ambiguousvars.remove(var)
   318             self.ambiguousvars.remove(var)
   318 
   319 
   319     def compute(self, rqlst):
   320     def compute(self, rqlst):
   320         # set domains for each variable
   321         # set domains for each variable
   321         for varname, var in rqlst.defined_vars.items():
   322         for varname, var in rqlst.defined_vars.items():
   322             if var.stinfo['uidrel'] is not None or \
   323             if (var.stinfo['uidrel'] is not None
   323                    self.eschema(rqlst.solutions[0][varname]).final:
   324                     or self.eschema(rqlst.solutions[0][varname]).final):
   324                 ptypes = var.stinfo['possibletypes']
   325                 ptypes = var.stinfo['possibletypes']
   325             else:
   326             else:
   326                 ptypes = set(self.nfdomain)
   327                 ptypes = set(self.nfdomain)
   327                 self.ambiguousvars.add(var)
   328                 self.ambiguousvars.add(var)
   328             self.varsols[var] = ptypes
   329             self.varsols[var] = ptypes
   354                     # no relation to deambiguify
   355                     # no relation to deambiguify
   355                     continue
   356                     continue
   356 
   357 
   357     def _debug_print(self):
   358     def _debug_print(self):
   358         print('varsols', dict((x, sorted(str(v) for v in values))
   359         print('varsols', dict((x, sorted(str(v) for v in values))
   359                                for x, values in self.varsols.items()))
   360                               for x, values in self.varsols.items()))
   360         print('ambiguous vars', sorted(self.ambiguousvars))
   361         print('ambiguous vars', sorted(self.ambiguousvars))
   361 
   362 
   362     def set_rel_constraint(self, term, rel, etypes_func):
   363     def set_rel_constraint(self, term, rel, etypes_func):
   363         if isinstance(term, VariableRef) and self.is_ambiguous(term.variable):
   364         if isinstance(term, VariableRef) and self.is_ambiguous(term.variable):
   364             var = term.variable
   365             var = term.variable
   365             if len(var.stinfo['relations']) == 1 \
   366             if (len(var.stinfo['relations']) == 1
   366                    or rel.scope is var.scope or rel.r_type == 'identity':
   367                     or rel.scope is var.scope
       
   368                     or rel.r_type == 'identity'):
   367                 self.restrict(var, frozenset(etypes_func()))
   369                 self.restrict(var, frozenset(etypes_func()))
   368                 try:
   370                 try:
   369                     self.maydeambrels[var].add(rel)
   371                     self.maydeambrels[var].add(rel)
   370                 except KeyError:
   372                 except KeyError:
   371                     self.maydeambrels[var] = set((rel,))
   373                     self.maydeambrels[var] = set((rel,))
   376         other = onlhs and rhs or lhs
   378         other = onlhs and rhs or lhs
   377         otheretypes = None
   379         otheretypes = None
   378         # XXX isinstance(other.variable, Variable) to skip column alias
   380         # XXX isinstance(other.variable, Variable) to skip column alias
   379         if isinstance(other, VariableRef) and isinstance(other.variable, Variable):
   381         if isinstance(other, VariableRef) and isinstance(other.variable, Variable):
   380             deambiguifier = other.variable
   382             deambiguifier = other.variable
   381             if not var is self.deambification_map.get(deambiguifier):
   383             if var is not self.deambification_map.get(deambiguifier):
   382                 if var.stinfo['typerel'] is None:
   384                 if var.stinfo['typerel'] is None:
   383                     otheretypes = deambiguifier.stinfo['possibletypes']
   385                     otheretypes = deambiguifier.stinfo['possibletypes']
   384                 elif not self.is_ambiguous(deambiguifier):
   386                 elif not self.is_ambiguous(deambiguifier):
   385                     otheretypes = self.varsols[deambiguifier]
   387                     otheretypes = self.varsols[deambiguifier]
   386                 elif deambiguifier in self.not_invariants:
   388                 elif deambiguifier in self.not_invariants: