author | Denis Laxalde <denis.laxalde@logilab.fr> |
Mon, 16 Jun 2014 10:08:32 +0200 | |
changeset 9965 | a8769b752299 |
parent 5621 | 6f32f08d91f3 |
child 10662 | 10942ed172de |
permissions | -rw-r--r-- |
# copyright 2003-2010 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/>. """SPARQL -> RQL translator""" from logilab.common import make_domains from rql import TypeResolverException from fyzz.yappsparser import parse from fyzz import ast from cubicweb.xy import xy class UnsupportedQuery(Exception): pass def order_limit_offset(sparqlst): addons = '' if sparqlst.orderby: sortterms = ', '.join('%s %s' % (var.name.upper(), ascdesc.upper()) for var, ascdesc in sparqlst.orderby) addons += ' ORDERBY %s' % sortterms if sparqlst.limit: addons += ' LIMIT %s' % sparqlst.limit if sparqlst.offset: addons += ' OFFSET %s' % sparqlst.offset return addons class QueryInfo(object): """wrapper class containing necessary information to generate a RQL query from a sparql syntax tree """ def __init__(self, sparqlst): self.sparqlst = sparqlst if sparqlst.selected == ['*']: self.selection = [var.upper() for var in sparqlst.variables] else: self.selection = [var.name.upper() for var in sparqlst.selected] self.possible_types = {} self.infer_types_info = [] self.union_params = [] self.restrictions = [] self.literals = {} self._litcount = 0 def add_literal(self, value): key = chr(ord('a') + self._litcount) self._litcount += 1 self.literals[key] = value return key def set_possible_types(self, var, varpossibletypes): """set/restrict possible types for the given variable. :return: True if something changed, else false. :raise: TypeResolverException if no more type allowed """ varpossibletypes = set(varpossibletypes) try: ctypes = self.possible_types[var] nbctypes = len(ctypes) ctypes &= varpossibletypes if not ctypes: raise TypeResolverException('No possible type') return len(ctypes) != nbctypes except KeyError: self.possible_types[var] = varpossibletypes return True def infer_types(self): # XXX should use something similar to rql.analyze for proper type inference modified = True # loop to infer types until nothing changed while modified: modified = False for yams_predicates, subjvar, obj in self.infer_types_info: nbchoices = len(yams_predicates) # get possible types for the subject variable, according to the # current predicate svptypes = set(s for s, r, o in yams_predicates) if not '*' in svptypes: if self.set_possible_types(subjvar, svptypes): modified = True # restrict predicates according to allowed subject var types if subjvar in self.possible_types: yams_predicates[:] = [(s, r, o) for s, r, o in yams_predicates if s == '*' or s in self.possible_types[subjvar]] if isinstance(obj, ast.SparqlVar): # make a valid rql var name objvar = obj.name.upper() # get possible types for the object variable, according to # the current predicate ovptypes = set(o for s, r, o in yams_predicates) if not '*' in ovptypes: if self.set_possible_types(objvar, ovptypes): modified = True # restrict predicates according to allowed object var types if objvar in self.possible_types: yams_predicates[:] = [(s, r, o) for s, r, o in yams_predicates if o == '*' or o in self.possible_types[objvar]] # ensure this still make sense if not yams_predicates: raise TypeResolverException('No yams predicate') if len(yams_predicates) != nbchoices: modified = True def build_restrictions(self): # now, for each predicate for yams_predicates, subjvar, obj in self.infer_types_info: rel = yams_predicates[0] # if there are several yams relation type equivalences, we will have # to generate several unioned rql queries for s, r, o in yams_predicates[1:]: if r != rel[1]: self.union_params.append((yams_predicates, subjvar, obj)) break # else we can simply add it to base rql restrictions else: restr = self.build_restriction(subjvar, rel[1], obj) self.restrictions.append(restr) def build_restriction(self, subjvar, rtype, obj): if isinstance(obj, ast.SparqlLiteral): key = self.add_literal(obj.value) objvar = '%%(%s)s' % key else: assert isinstance(obj, ast.SparqlVar) # make a valid rql var name objvar = obj.name.upper() # else we can simply add it to base rql restrictions return '%s %s %s' % (subjvar, rtype, objvar) def finalize(self): """return corresponding rql query (string) / args (dict)""" for varname, ptypes in self.possible_types.iteritems(): if len(ptypes) == 1: self.restrictions.append('%s is %s' % (varname, iter(ptypes).next())) unions = [] for releq, subjvar, obj in self.union_params: thisunions = [] for st, rt, ot in releq: thisunions.append([self.build_restriction(subjvar, rt, obj)]) if st != '*': thisunions[-1].append('%s is %s' % (subjvar, st)) if isinstance(obj, ast.SparqlVar) and ot != '*': objvar = obj.name.upper() thisunions[-1].append('%s is %s' % (objvar, objvar)) if not unions: unions = thisunions else: unions = zip(*make_domains([unions, thisunions])) selection = 'Any ' + ', '.join(self.selection) sparqlst = self.sparqlst if sparqlst.distinct: selection = 'DISTINCT ' + selection if unions: baserql = '%s WHERE %s' % (selection, ', '.join(self.restrictions)) rqls = ['(%s, %s)' % (baserql, ', '.join(unionrestrs)) for unionrestrs in unions] rql = ' UNION '.join(rqls) if sparqlst.orderby or sparqlst.limit or sparqlst.offset: rql = '%s%s WITH %s BEING (%s)' % ( selection, order_limit_offset(sparqlst), ', '.join(self.selection), rql) else: rql = '%s%s WHERE %s' % (selection, order_limit_offset(sparqlst), ', '.join(self.restrictions)) return rql, self.literals class Sparql2rqlTranslator(object): def __init__(self, yschema): self.yschema = yschema def translate(self, sparql): sparqlst = parse(sparql) if sparqlst.type != 'select': raise UnsupportedQuery() qi = QueryInfo(sparqlst) for subj, predicate, obj in sparqlst.where: if not isinstance(subj, ast.SparqlVar): raise UnsupportedQuery() # make a valid rql var name subjvar = subj.name.upper() if predicate in [('', 'a'), ('http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'type')]: # special 'is' relation if not isinstance(obj, tuple): raise UnsupportedQuery() # restrict possible types for the subject variable qi.set_possible_types( subjvar, xy.yeq(':'.join(obj), isentity=True)) else: # 'regular' relation (eg not 'is') if not isinstance(predicate, tuple): raise UnsupportedQuery() # list of 3-uple # (yams etype (subject), yams rtype, yams etype (object)) # where subject / object entity type may '*' if not specified yams_predicates = xy.yeq(':'.join(predicate)) qi.infer_types_info.append((yams_predicates, subjvar, obj)) if not isinstance(obj, (ast.SparqlLiteral, ast.SparqlVar)): raise UnsupportedQuery() qi.infer_types() qi.build_restrictions() return qi