cubicweb/server/rqlannotation.py
changeset 12509 db81a99e9dd1
parent 12355 c703dc95c82e
child 12567 26744ad37953
equal deleted inserted replaced
12508:a8c1ea390400 12509:db81a99e9dd1
    22 from __future__ import print_function
    22 from __future__ import print_function
    23 
    23 
    24 from rql import BadRQLQuery
    24 from rql import BadRQLQuery
    25 from rql.nodes import Relation, VariableRef, Constant, Variable, Or
    25 from rql.nodes import Relation, VariableRef, Constant, Variable, Or
    26 from rql.utils import common_parent
    26 from rql.utils import common_parent
    27 
       
    28 
       
    29 def _annotate_select(annotator, rqlst):
       
    30     has_text_query = False
       
    31     for subquery in rqlst.with_:
       
    32         if annotator._annotate_union(subquery.query):
       
    33             has_text_query = True
       
    34     getrschema = annotator.schema.rschema
       
    35     for var in rqlst.defined_vars.values():
       
    36         stinfo = var.stinfo
       
    37         if stinfo.get('ftirels'):
       
    38             has_text_query = True
       
    39         if stinfo['attrvar']:
       
    40             stinfo['invariant'] = False
       
    41             stinfo['principal'] = _select_main_var(stinfo['rhsrelations'])
       
    42             continue
       
    43         if stinfo['typerel'] is None:
       
    44             # those particular queries should be executed using the system
       
    45             # entities table unless there is some type restriction
       
    46             if not stinfo['relations']:
       
    47                 # Any X, Any MAX(X)...
       
    48                 stinfo['invariant'] = True
       
    49                 stinfo['principal'] = None
       
    50                 continue
       
    51             if (any(rel for rel in stinfo['relations']
       
    52                     if rel.r_type == 'eid' and rel.operator() != '=')
       
    53                     and not any(r for r in var.stinfo['relations'] - var.stinfo['rhsrelations']
       
    54                                 if r.r_type != 'eid'
       
    55                                 and (getrschema(r.r_type).inlined or getrschema(r.r_type).final))):
       
    56                 # Any X WHERE X eid > 2
       
    57                 stinfo['invariant'] = True
       
    58                 stinfo['principal'] = None
       
    59                 continue
       
    60         if stinfo['selected'] and var.valuable_references() == 1 + bool(stinfo['constnode']):
       
    61             # "Any X", "Any X, Y WHERE X attr Y"
       
    62             stinfo['invariant'] = False
       
    63             continue
       
    64         joins = set()
       
    65         invariant = False
       
    66         for ref in var.references():
       
    67             rel = ref.relation()
       
    68             if rel is None or rel.is_types_restriction():
       
    69                 continue
       
    70             lhs, rhs = rel.get_parts()
       
    71             onlhs = ref is lhs
       
    72             role = 'subject' if onlhs else 'object'
       
    73             if rel.r_type == 'eid':
       
    74                 if not (onlhs and len(stinfo['relations']) > 1):
       
    75                     break
       
    76                 if not stinfo['constnode']:
       
    77                     joins.add((rel, role))
       
    78                 continue
       
    79             elif rel.r_type == 'identity':
       
    80                 # identity can't be used as principal, so check other relation are used
       
    81                 # XXX explain rhs.operator == '='
       
    82                 if rhs.operator != '=' or len(stinfo['relations']) <= 1:
       
    83                     break
       
    84                 joins.add((rel, role))
       
    85                 continue
       
    86             rschema = getrschema(rel.r_type)
       
    87             if rel.optional:
       
    88                 if rel in stinfo.get('optrelations', ()):
       
    89                     # optional variable can't be invariant if this is the lhs
       
    90                     # variable of an inlined relation
       
    91                     if rel not in stinfo['rhsrelations'] and rschema.inlined:
       
    92                         break
       
    93                 # variable used as main variable of an optional relation can't
       
    94                 # be invariant, unless we can use some other relation as
       
    95                 # reference for the outer join
       
    96                 elif not stinfo['constnode']:
       
    97                     break
       
    98                 elif len(stinfo['relations']) == 2:
       
    99                     if onlhs:
       
   100                         ostinfo = rhs.children[0].variable.stinfo
       
   101                     else:
       
   102                         ostinfo = lhs.variable.stinfo
       
   103                     if not (ostinfo.get('optcomparisons')
       
   104                             or any(orel for orel in ostinfo['relations']
       
   105                                    if orel.optional and orel is not rel)):
       
   106                         break
       
   107             if rschema.final or (onlhs and rschema.inlined):
       
   108                 if rschema.type != 'has_text':
       
   109                     # need join anyway if the variable appears in a final or
       
   110                     # inlined relation
       
   111                     break
       
   112                 joins.add((rel, role))
       
   113                 continue
       
   114             if not stinfo['constnode']:
       
   115                 if rschema.inlined and rel.neged(strict=True):
       
   116                     # if relation is inlined, can't be invariant if that
       
   117                     # variable is used anywhere else.
       
   118                     # 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
       
   120                     # can use N.ecrit_par as principal
       
   121                     if (stinfo['selected'] or len(stinfo['relations']) > 1):
       
   122                         break
       
   123             joins.add((rel, role))
       
   124         else:
       
   125             # 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
       
   127             # other types
       
   128             if not annotator.is_ambiguous(var):
       
   129                 invariant = True
       
   130         stinfo['invariant'] = invariant
       
   131         if invariant and joins:
       
   132             # remember rqlst/solutions analyze information
       
   133             # we have to select a kindof "main" relation which will "extrajoins"
       
   134             # the other
       
   135             # priority should be given to relation which are not in inner queries
       
   136             # (eg exists)
       
   137             try:
       
   138                 stinfo['principal'] = principal = _select_principal(var.scope, joins)
       
   139                 if getrschema(principal.r_type).inlined:
       
   140                     # the scope of the lhs variable must be equal or outer to the
       
   141                     # rhs variable's scope (since it's retrieved from lhs's table)
       
   142                     sstinfo = principal.children[0].variable.stinfo
       
   143                     sstinfo['scope'] = common_parent(sstinfo['scope'], stinfo['scope']).scope
       
   144             except CantSelectPrincipal:
       
   145                 stinfo['invariant'] = False
       
   146     # see unittest_rqlannotation. test_has_text_security_cache_bug
       
   147     # XXX probably more to do, but yet that work without more...
       
   148     for col_alias in rqlst.aliases.values():
       
   149         if col_alias.stinfo.get('ftirels'):
       
   150             has_text_query = True
       
   151     return has_text_query
       
   152 
    27 
   153 
    28 
   154 class CantSelectPrincipal(Exception):
    29 class CantSelectPrincipal(Exception):
   155     """raised when no 'principal' variable can be found"""
    30     """raised when no 'principal' variable can be found"""
   156 
    31 
   243             else:
   118             else:
   244                 var._q_invariant = False
   119                 var._q_invariant = False
   245 
   120 
   246 
   121 
   247 class SQLGenAnnotator(object):
   122 class SQLGenAnnotator(object):
       
   123 
   248     def __init__(self, schema):
   124     def __init__(self, schema):
   249         self.schema = schema
   125         self.schema = schema
   250         self.nfdomain = frozenset(eschema.type for eschema in schema.entities()
   126         self.nfdomain = frozenset(eschema.type for eschema in schema.entities()
   251                                   if not eschema.final)
   127                                   if not eschema.final)
   252 
   128 
   253     def annotate(self, rqlst):
   129     def annotate(self, rqlst):
   254         """add information to the rql syntax tree to help sources to do their
   130         """add information to the rql syntax tree to help sources to do their
   255         job (read sql generation)
   131         job (read sql generation)
   256 
   132 
   257         a variable is tagged as invariant if:
   133         a variable is tagged as invariant if:
   258         * it's a non final variable
   134         * it is a non final variable
   259         * it's not used as lhs in any final or inlined relation
   135         * it is not used as lhs in any final or inlined relation
   260         * there is no type restriction on this variable (either explicit in the
   136         * there is no type restriction on this variable (either explicit in the
   261           syntax tree or because a solution for this variable has been removed
   137           syntax tree or because a solution for this variable has been removed
   262           due to security filtering)
   138           due to security filtering)
   263         """
   139         """
   264         # assert rqlst.TYPE == 'select', rqlst
   140         # assert rqlst.TYPE == 'select', rqlst
   265         rqlst.has_text_query = self._annotate_union(rqlst)
   141         rqlst.has_text_query = self._annotate_union(rqlst)
   266 
   142 
   267     def _annotate_union(self, union):
   143     def _annotate_union(self, union):
   268         has_text_query = False
   144         has_text_query = False
   269         for select in union.children:
   145         for select in union.children:
   270             if _annotate_select(self, select):
   146             if self._annotate_select(select):
       
   147                 has_text_query = True
       
   148         return has_text_query
       
   149 
       
   150     def _annotate_select(self, rqlst):
       
   151         has_text_query = False
       
   152         for subquery in rqlst.with_:
       
   153             if self._annotate_union(subquery.query):
       
   154                 has_text_query = True
       
   155         getrschema = self.schema.rschema
       
   156         for var in rqlst.defined_vars.values():
       
   157             stinfo = var.stinfo
       
   158             if stinfo.get('ftirels'):
       
   159                 has_text_query = True
       
   160             if stinfo['attrvar']:
       
   161                 stinfo['invariant'] = False
       
   162                 stinfo['principal'] = _select_main_var(stinfo['rhsrelations'])
       
   163                 continue
       
   164             if stinfo['typerel'] is None:
       
   165                 # those particular queries should be executed using the system
       
   166                 # entities table unless there is some type restriction
       
   167                 if not stinfo['relations']:
       
   168                     # Any X, Any MAX(X)...
       
   169                     stinfo['invariant'] = True
       
   170                     stinfo['principal'] = None
       
   171                     continue
       
   172                 if (any(rel for rel in stinfo['relations']
       
   173                         if rel.r_type == 'eid' and rel.operator() != '=')
       
   174                         and not any(r for r in var.stinfo['relations'] - var.stinfo['rhsrelations']
       
   175                                     if r.r_type != 'eid'
       
   176                                     and (getrschema(r.r_type).inlined
       
   177                                          or getrschema(r.r_type).final))):
       
   178                     # Any X WHERE X eid > 2
       
   179                     stinfo['invariant'] = True
       
   180                     stinfo['principal'] = None
       
   181                     continue
       
   182             if stinfo['selected'] and var.valuable_references() == 1 + bool(stinfo['constnode']):
       
   183                 # "Any X", "Any X, Y WHERE X attr Y"
       
   184                 stinfo['invariant'] = False
       
   185                 continue
       
   186             joins = set()
       
   187             invariant = False
       
   188             for ref in var.references():
       
   189                 rel = ref.relation()
       
   190                 if rel is None or rel.is_types_restriction():
       
   191                     continue
       
   192                 lhs, rhs = rel.get_parts()
       
   193                 onlhs = ref is lhs
       
   194                 role = 'subject' if onlhs else 'object'
       
   195                 if rel.r_type == 'eid':
       
   196                     if not (onlhs and len(stinfo['relations']) > 1):
       
   197                         break
       
   198                     if not stinfo['constnode']:
       
   199                         joins.add((rel, role))
       
   200                     continue
       
   201                 elif rel.r_type == 'identity':
       
   202                     # identity can't be used as principal, so check other relation are used
       
   203                     # XXX explain rhs.operator == '='
       
   204                     if rhs.operator != '=' or len(stinfo['relations']) <= 1:
       
   205                         break
       
   206                     joins.add((rel, role))
       
   207                     continue
       
   208                 rschema = getrschema(rel.r_type)
       
   209                 if rel.optional:
       
   210                     if rel in stinfo.get('optrelations', ()):
       
   211                         # optional variable can't be invariant if this is the lhs
       
   212                         # variable of an inlined relation
       
   213                         if rel not in stinfo['rhsrelations'] and rschema.inlined:
       
   214                             break
       
   215                     # variable used as main variable of an optional relation can't
       
   216                     # be invariant, unless we can use some other relation as
       
   217                     # reference for the outer join
       
   218                     elif not stinfo['constnode']:
       
   219                         break
       
   220                     elif len(stinfo['relations']) == 2:
       
   221                         if onlhs:
       
   222                             ostinfo = rhs.children[0].variable.stinfo
       
   223                         else:
       
   224                             ostinfo = lhs.variable.stinfo
       
   225                         if not (ostinfo.get('optcomparisons')
       
   226                                 or any(orel for orel in ostinfo['relations']
       
   227                                        if orel.optional and orel is not rel)):
       
   228                             break
       
   229                 if rschema.final or (onlhs and rschema.inlined):
       
   230                     if rschema.type != 'has_text':
       
   231                         # need join anyway if the variable appears in a final or
       
   232                         # inlined relation
       
   233                         break
       
   234                     joins.add((rel, role))
       
   235                     continue
       
   236                 if not stinfo['constnode']:
       
   237                     if rschema.inlined and rel.neged(strict=True):
       
   238                         # if relation is inlined, can't be invariant if that
       
   239                         # variable is used anywhere else.
       
   240                         # see 'Any P WHERE NOT N ecrit_par P, N eid 512':
       
   241                         # sql for 'NOT N ecrit_par P' is 'N.ecrit_par is NULL' so P
       
   242                         # can use N.ecrit_par as principal
       
   243                         if (stinfo['selected'] or len(stinfo['relations']) > 1):
       
   244                             break
       
   245                 joins.add((rel, role))
       
   246             else:
       
   247                 # if there is at least one ambigous relation and no other to
       
   248                 # restrict types, can't be invariant since we need to filter out
       
   249                 # other types
       
   250                 if not self.is_ambiguous(var):
       
   251                     invariant = True
       
   252             stinfo['invariant'] = invariant
       
   253             if invariant and joins:
       
   254                 # remember rqlst/solutions analyze information
       
   255                 # we have to select a kindof "main" relation which will "extrajoins"
       
   256                 # the other
       
   257                 # priority should be given to relation which are not in inner queries
       
   258                 # (eg exists)
       
   259                 try:
       
   260                     stinfo['principal'] = principal = _select_principal(var.scope, joins)
       
   261                     if getrschema(principal.r_type).inlined:
       
   262                         # the scope of the lhs variable must be equal or outer to the
       
   263                         # rhs variable's scope (since it's retrieved from lhs's table)
       
   264                         sstinfo = principal.children[0].variable.stinfo
       
   265                         sstinfo['scope'] = common_parent(sstinfo['scope'], stinfo['scope']).scope
       
   266                 except CantSelectPrincipal:
       
   267                     stinfo['invariant'] = False
       
   268         # see unittest_rqlannotation. test_has_text_security_cache_bug
       
   269         # XXX probably more to do, but yet that work without more...
       
   270         for col_alias in rqlst.aliases.values():
       
   271             if col_alias.stinfo.get('ftirels'):
   271                 has_text_query = True
   272                 has_text_query = True
   272         return has_text_query
   273         return has_text_query
   273 
   274 
   274     def is_ambiguous(self, var):
   275     def is_ambiguous(self, var):
   275         # ignore has_text relation when we know it will be used as principal.
   276         # ignore has_text relation when we know it will be used as principal.