cubicweb/spa2rql.py
changeset 11057 0b59724cb3f2
parent 10669 155c29e0ed1c
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    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/>.
       
    18 """SPARQL -> RQL translator"""
       
    19 
       
    20 from logilab.common import make_domains
       
    21 from rql import TypeResolverException
       
    22 from fyzz.yappsparser import parse
       
    23 from fyzz import ast
       
    24 
       
    25 from cubicweb.xy import xy
       
    26 
       
    27 
       
    28 class UnsupportedQuery(Exception): pass
       
    29 
       
    30 def order_limit_offset(sparqlst):
       
    31     addons = ''
       
    32     if sparqlst.orderby:
       
    33         sortterms = ', '.join('%s %s' % (var.name.upper(), ascdesc.upper())
       
    34                               for var, ascdesc in sparqlst.orderby)
       
    35         addons += ' ORDERBY %s' % sortterms
       
    36     if sparqlst.limit:
       
    37         addons += ' LIMIT %s' % sparqlst.limit
       
    38     if sparqlst.offset:
       
    39         addons += ' OFFSET %s' % sparqlst.offset
       
    40     return addons
       
    41 
       
    42 
       
    43 class QueryInfo(object):
       
    44     """wrapper class containing necessary information to generate a RQL query
       
    45     from a sparql syntax tree
       
    46     """
       
    47     def __init__(self, sparqlst):
       
    48         self.sparqlst = sparqlst
       
    49         if sparqlst.selected == ['*']:
       
    50             self.selection = [var.upper() for var in sparqlst.variables]
       
    51         else:
       
    52             self.selection = [var.name.upper() for var in sparqlst.selected]
       
    53         self.possible_types = {}
       
    54         self.infer_types_info = []
       
    55         self.union_params = []
       
    56         self.restrictions = []
       
    57         self.literals = {}
       
    58         self._litcount = 0
       
    59 
       
    60     def add_literal(self, value):
       
    61         key = chr(ord('a') + self._litcount)
       
    62         self._litcount += 1
       
    63         self.literals[key] = value
       
    64         return key
       
    65 
       
    66     def set_possible_types(self, var, varpossibletypes):
       
    67         """set/restrict possible types for the given variable.
       
    68 
       
    69         :return: True if something changed, else false.
       
    70         :raise: TypeResolverException if no more type allowed
       
    71         """
       
    72         varpossibletypes = set(varpossibletypes)
       
    73         try:
       
    74             ctypes = self.possible_types[var]
       
    75             nbctypes = len(ctypes)
       
    76             ctypes &= varpossibletypes
       
    77             if not ctypes:
       
    78                 raise TypeResolverException('No possible type')
       
    79             return len(ctypes) != nbctypes
       
    80         except KeyError:
       
    81             self.possible_types[var] = varpossibletypes
       
    82             return True
       
    83 
       
    84     def infer_types(self):
       
    85         # XXX should use something similar to rql.analyze for proper type inference
       
    86         modified = True
       
    87         # loop to infer types until nothing changed
       
    88         while modified:
       
    89             modified = False
       
    90             for yams_predicates, subjvar, obj in self.infer_types_info:
       
    91                 nbchoices = len(yams_predicates)
       
    92                 # get possible types for the subject variable, according to the
       
    93                 # current predicate
       
    94                 svptypes = set(s for s, r, o in yams_predicates)
       
    95                 if not '*' in svptypes:
       
    96                     if self.set_possible_types(subjvar, svptypes):
       
    97                         modified = True
       
    98                 # restrict predicates according to allowed subject var types
       
    99                 if subjvar in self.possible_types:
       
   100                     yams_predicates[:] = [(s, r, o) for s, r, o in yams_predicates
       
   101                                           if s == '*' or s in self.possible_types[subjvar]]
       
   102                 if isinstance(obj, ast.SparqlVar):
       
   103                     # make a valid rql var name
       
   104                     objvar = obj.name.upper()
       
   105                     # get possible types for the object variable, according to
       
   106                     # the current predicate
       
   107                     ovptypes = set(o for s, r, o in yams_predicates)
       
   108                     if not '*' in ovptypes:
       
   109                         if self.set_possible_types(objvar, ovptypes):
       
   110                             modified = True
       
   111                     # restrict predicates according to allowed object var types
       
   112                     if objvar in self.possible_types:
       
   113                         yams_predicates[:] = [(s, r, o) for s, r, o in yams_predicates
       
   114                                               if o == '*' or o in self.possible_types[objvar]]
       
   115                 # ensure this still make sense
       
   116                 if not yams_predicates:
       
   117                     raise TypeResolverException('No yams predicate')
       
   118                 if len(yams_predicates) != nbchoices:
       
   119                     modified = True
       
   120 
       
   121     def build_restrictions(self):
       
   122         # now, for each predicate
       
   123         for yams_predicates, subjvar, obj in self.infer_types_info:
       
   124             rel = yams_predicates[0]
       
   125             # if there are several yams relation type equivalences, we will have
       
   126             # to generate several unioned rql queries
       
   127             for s, r, o in yams_predicates[1:]:
       
   128                 if r != rel[1]:
       
   129                     self.union_params.append((yams_predicates, subjvar, obj))
       
   130                     break
       
   131             # else we can simply add it to base rql restrictions
       
   132             else:
       
   133                 restr = self.build_restriction(subjvar, rel[1], obj)
       
   134                 self.restrictions.append(restr)
       
   135 
       
   136     def build_restriction(self, subjvar, rtype, obj):
       
   137         if isinstance(obj, ast.SparqlLiteral):
       
   138             key = self.add_literal(obj.value)
       
   139             objvar = '%%(%s)s' % key
       
   140         else:
       
   141             assert isinstance(obj, ast.SparqlVar)
       
   142             # make a valid rql var name
       
   143             objvar = obj.name.upper()
       
   144         # else we can simply add it to base rql restrictions
       
   145         return '%s %s %s' % (subjvar, rtype, objvar)
       
   146 
       
   147     def finalize(self):
       
   148         """return corresponding rql query (string) / args (dict)"""
       
   149         for varname, ptypes in self.possible_types.items():
       
   150             if len(ptypes) == 1:
       
   151                 self.restrictions.append('%s is %s' % (varname, next(iter(ptypes))))
       
   152         unions = []
       
   153         for releq, subjvar, obj in self.union_params:
       
   154             thisunions = []
       
   155             for st, rt, ot in releq:
       
   156                 thisunions.append([self.build_restriction(subjvar, rt, obj)])
       
   157                 if st != '*':
       
   158                     thisunions[-1].append('%s is %s' % (subjvar, st))
       
   159                 if isinstance(obj, ast.SparqlVar) and ot != '*':
       
   160                     objvar = obj.name.upper()
       
   161                     thisunions[-1].append('%s is %s' % (objvar, objvar))
       
   162             if not unions:
       
   163                 unions = thisunions
       
   164             else:
       
   165                 unions = zip(*make_domains([unions, thisunions]))
       
   166         selection = 'Any ' + ', '.join(self.selection)
       
   167         sparqlst = self.sparqlst
       
   168         if sparqlst.distinct:
       
   169             selection = 'DISTINCT ' + selection
       
   170         if unions:
       
   171             baserql = '%s WHERE %s' % (selection, ', '.join(self.restrictions))
       
   172             rqls = ['(%s, %s)' % (baserql, ', '.join(unionrestrs))
       
   173                     for unionrestrs in unions]
       
   174             rql = ' UNION '.join(rqls)
       
   175             if sparqlst.orderby or sparqlst.limit or sparqlst.offset:
       
   176                 rql = '%s%s WITH %s BEING (%s)' % (
       
   177                     selection, order_limit_offset(sparqlst),
       
   178                     ', '.join(self.selection), rql)
       
   179         else:
       
   180             rql = '%s%s WHERE %s' % (selection, order_limit_offset(sparqlst),
       
   181                                       ', '.join(self.restrictions))
       
   182         return rql, self.literals
       
   183 
       
   184 
       
   185 class Sparql2rqlTranslator(object):
       
   186     def __init__(self, yschema):
       
   187         self.yschema = yschema
       
   188 
       
   189     def translate(self, sparql):
       
   190         sparqlst = parse(sparql)
       
   191         if sparqlst.type != 'select':
       
   192             raise UnsupportedQuery()
       
   193         qi = QueryInfo(sparqlst)
       
   194         for subj, predicate, obj in sparqlst.where:
       
   195             if not isinstance(subj, ast.SparqlVar):
       
   196                 raise UnsupportedQuery()
       
   197             # make a valid rql var name
       
   198             subjvar = subj.name.upper()
       
   199             if predicate in [('', 'a'),
       
   200                              ('http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'type')]:
       
   201                 # special 'is' relation
       
   202                 if not isinstance(obj, tuple):
       
   203                     raise UnsupportedQuery()
       
   204                 # restrict possible types for the subject variable
       
   205                 qi.set_possible_types(
       
   206                     subjvar, xy.yeq(':'.join(obj), isentity=True))
       
   207             else:
       
   208                 # 'regular' relation (eg not 'is')
       
   209                 if not isinstance(predicate, tuple):
       
   210                     raise UnsupportedQuery()
       
   211                 # list of 3-uple
       
   212                 #   (yams etype (subject), yams rtype, yams etype (object))
       
   213                 # where subject / object entity type may '*' if not specified
       
   214                 yams_predicates = xy.yeq(':'.join(predicate))
       
   215                 qi.infer_types_info.append((yams_predicates, subjvar, obj))
       
   216                 if not isinstance(obj, (ast.SparqlLiteral, ast.SparqlVar)):
       
   217                     raise UnsupportedQuery()
       
   218         qi.infer_types()
       
   219         qi.build_restrictions()
       
   220         return qi