package org.aksw.sparqlify.core.cast; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import org.aksw.commons.collections.multimaps.IBiSetMultimap; import org.aksw.jena_sparql_api.utils.ExprUtils; import org.aksw.jena_sparql_api.views.E_RdfTerm; import org.aksw.jena_sparql_api.views.ExprCopy; import org.aksw.jena_sparql_api.views.SqlTranslationUtils; import org.aksw.sparqlify.algebra.sql.exprs2.ExprSqlBridge; import org.aksw.sparqlify.algebra.sql.exprs2.S_ColumnRef; import org.aksw.sparqlify.algebra.sql.exprs2.S_Constant; import org.aksw.sparqlify.algebra.sql.exprs2.S_Function; import org.aksw.sparqlify.algebra.sql.exprs2.SqlExpr; import org.aksw.sparqlify.algebra.sql.exprs2.SqlExprFunction; import org.aksw.sparqlify.algebra.sql.nodes.Projection; import org.aksw.sparqlify.core.TypeToken; import org.aksw.sparqlify.core.algorithms.ExprSqlRewrite; import org.aksw.sparqlify.core.datatypes.SparqlFunction; import org.aksw.sparqlify.core.sql.expr.evaluation.SqlExprEvaluator; import org.aksw.sparqlify.type_system.CandidateMethod; import org.aksw.sparqlify.type_system.FunctionModel; import org.aksw.sparqlify.type_system.FunctionModelMeta; import org.aksw.sparqlify.type_system.MethodEntry; import org.aksw.sparqlify.type_system.MethodSignature; import org.aksw.sparqlify.type_system.TypeSystemUtils; import org.apache.jena.sdb.core.Generator; import org.apache.jena.sdb.core.Gensym; import org.apache.jena.sparql.core.Var; import org.apache.jena.sparql.expr.Expr; import org.apache.jena.sparql.expr.ExprFunction; import org.apache.jena.sparql.expr.ExprVar; import org.apache.jena.sparql.expr.NodeValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Multimap; class ExprHolder { private Object expr; public ExprHolder(SqlExpr expr) { this.expr = expr; } public ExprHolder(Expr expr) { this.expr = expr; } public boolean isSqlExpr() { boolean result = this.expr instanceof SqlExpr; return result; } public boolean isExpr() { boolean result = this.expr instanceof Expr; return result; } public Expr getExpr() { return (Expr)expr; } public SqlExpr getSqlExpr() { return (SqlExpr) expr; } @Override public String toString() { return "ExprHolder: " + expr; } } class RewriteState { private Generator genSym; private Projection projection = new Projection(); public RewriteState() { this(Gensym.create("s")); } public RewriteState(Generator genSym) { super(); this.genSym = genSym; this.projection = projection; } public Generator getGenSym() { return genSym; } public Projection getProjection() { return projection; } } /** * Computes the datatype of each expression node. * * TODO: I think this class should not actually transform expressions * * @author Claus Stadler <cstadler@informatik.uni-leipzig.de> * */ public class TypedExprTransformerImpl implements TypedExprTransformer { private static final Logger logger = LoggerFactory.getLogger(TypedExprTransformerImpl.class); // TODO Get rid of the typeSystem here, and replace it by more fine granular // objects private TypeSystem typeSystem; // private constantConverter private SparqlFunctionProvider functionProvider; public TypedExprTransformerImpl(TypeSystem typeSystem) { //SparqlFunctionProvider functionProvider) { this.typeSystem = typeSystem; this.functionProvider = typeSystem; //this.constantSqlConverter = //this.functionProvider = functionProvider; } public TypeSystem getTypeSystem() { return typeSystem; } public static List<TypeToken> getTypes(Collection<SqlExpr> sqlExprs) { List<TypeToken> result = new ArrayList<TypeToken>(sqlExprs.size()); for(SqlExpr sqlExpr : sqlExprs) { TypeToken typeName = sqlExpr.getDatatype(); result.add(typeName); } return result; } public static boolean containsTypeError(Iterable<SqlExpr> exprs) { for(SqlExpr expr : exprs) { if(S_Constant.TYPE_ERROR.equals(expr)) { return true; } } return false; } // public List<ExprHolder> rewriteArgsDefault(ExprFunction fn, Map<String, TypeToken> typeMap, RewriteState state) // { // List<ExprHolder> evaledArgs = new ArrayList<ExprHolder>(); // boolean isAllSqlExpr = true; // for(Expr arg : fn.getArgs()) { // // ExprHolder evaledArg = rewrite(arg, typeMap, state); // // // isAllSqlExpr = isAllSqlExpr && evaledArg.isSqlExpr(); // // // If an argument evaluated to type error, return type error // // TODO: Distinguish between null and type error. Currently we use nvNothing which actually corresponds to NULL // // (currently represented with nvNothing - is that safe? - Rather no - see above) // /* // if(evaledArg.equals(NodeValue.nvNothing)) { // return NodeValue.nvNothing; // } // */ // // evaledArgs.add(evaledArg); // } // // return evaledArgs; // } // // public List<ExprHolder> rewriteArgsRdfTerm(E_RdfTerm rdfTerm, Map<String, TypeToken> typeMap, RewriteState state) { // // List<Expr> args = rdfTerm.getArgs(); // List<ExprHolder> rewrittenArgs = new ArrayList<ExprHolder>(); // // for(Expr arg : args) { // ExprHolder rewrittenArg; // if(arg.isConstant()) { // rewrittenArg = new ExprHolder(arg); // Do not rewrite constants // } else { // rewrittenArg = rewrite(arg, typeMap, state); // } // // rewrittenArgs.add(rewrittenArg); // } // // return rewrittenArgs; // } public ExprVar allocateVariable(SqlExpr sqlExpr, RewriteState state) { String varName = state.getGenSym().next(); Var var = Var.alloc(varName); ExprVar result = new ExprVar(var); state.getProjection().put(varName, sqlExpr); return result; } public ExprSqlRewrite rewrite(Expr expr, Map<String, TypeToken> typeMap) { E_RdfTerm rdfTerm; if(expr.isConstant()) { rdfTerm = SqlTranslationUtils.expandConstant(expr); } else { rdfTerm = SqlTranslationUtils.expandRdfTerm(expr); } RewriteState state = new RewriteState(); ExprHolder rewrite = rewrite(rdfTerm, typeMap, state); Expr resultExpr; if(rewrite.isSqlExpr()) { SqlExpr sqlExpr = rewrite.getSqlExpr(); resultExpr = allocateVariable(sqlExpr, state); } else { resultExpr = rewrite.getExpr(); } ExprSqlRewrite result = new ExprSqlRewrite(resultExpr, state.getProjection()); return result; } public ExprSqlRewrite rewriteOld(Expr expr, Map<String, TypeToken> typeMap) { RewriteState state = new RewriteState(); ExprHolder rewrite = rewrite(expr, typeMap, state); Expr resultExpr; if(rewrite.isSqlExpr()) { resultExpr = new ExprSqlBridge(rewrite.getSqlExpr()); } else { resultExpr = rewrite.getExpr(); } ExprSqlRewrite result = new ExprSqlRewrite(resultExpr, state.getProjection()); return result; } public ExprHolder rewrite(Expr expr, Map<String, TypeToken> typeMap, RewriteState state) { ExprHolder result; if(expr.isConstant()) { NodeValue nodeValue = expr.getConstant(); SqlExpr sqlExpr = translate(nodeValue); result = new ExprHolder(sqlExpr); } else if(expr.isVariable()) { ExprVar var = expr.getExprVar(); SqlExpr sqlExpr = translate(var, typeMap); result = new ExprHolder(sqlExpr); } else if(expr.isFunction()) { result = rewrite(expr.getFunction(), typeMap, state); } else { throw new RuntimeException("Should not happen: " + expr); } if(result.equals(TypeToken.TypeError)) { System.err.println("Got type error for " + expr); } return result; } public ExprHolder rewrite(ExprFunction fn, Map<String, TypeToken> typeMap, RewriteState state) { ExprHolder result; String functionId = ExprUtils.getFunctionId(fn); logger.debug("Processing: " + fn); /* if(containsTypeError(evaledArgs)) { logger.debug("Type error in argument (" + evaledArgs + ")"); return S_Constant.TYPE_ERROR; } */ boolean pushConstants = true; if(fn instanceof E_RdfTerm) { // We always push constants to make data access uniform //pushConstants = false; } List<ExprHolder> evaledArgs = new ArrayList<ExprHolder>(); boolean isAllSqlExpr = true; for(Expr arg : fn.getArgs()) { ExprHolder evaledArg; if(arg.isConstant() && !pushConstants) { evaledArg = new ExprHolder(arg); } else { evaledArg = rewrite(arg, typeMap, state); } isAllSqlExpr = isAllSqlExpr && evaledArg.isSqlExpr(); // If an argument evaluated to type error, return type error // TODO: Distinguish between null and type error. Currently we use nvNothing which actually corresponds to NULL // (currently represented with nvNothing - is that safe? - Rather no - see above) /* if(evaledArg.equals(NodeValue.nvNothing)) { return NodeValue.nvNothing; } */ evaledArgs.add(evaledArg); } boolean isFnRewritable = isAllSqlExpr; if(fn instanceof E_RdfTerm) { isFnRewritable = false; } // Allocate variables for all SqlExprs, pass on Exprs if(!isFnRewritable) { List<Expr> newArgs = new ArrayList<Expr>(evaledArgs.size()); for(ExprHolder holder : evaledArgs) { Expr arg; if(holder.isSqlExpr()) { SqlExpr typedExpr = holder.getSqlExpr(); arg = allocateVariable(typedExpr, state); } else { arg = holder.getExpr(); } newArgs.add(arg); } Expr expr = ExprCopy.getInstance().copy(fn, newArgs); result = new ExprHolder(expr); } else { // All arguments are SQL Exprs List<SqlExpr> newArgs = new ArrayList<SqlExpr>(evaledArgs.size()); for(ExprHolder holder : evaledArgs) { SqlExpr sqlExpr = holder.getSqlExpr(); newArgs.add(sqlExpr); } SqlExpr sqlExpr = processFunction(functionId, newArgs); result = new ExprHolder(sqlExpr); } return result; } public SqlExpr processFunction(String functionId, List<SqlExpr> newArgs) { // There must be a function registered for the argument types // String datatype = exprTypeEvaluator.evaluateType(fn); // if(datatype == null) { // throw new RuntimeException("No datatype could be obtained for " + fn); // } // // //result = S_Function.create(datatype, functionId, evaledArgs); // Get the types of the evaluated arguments List<TypeToken> argTypes = new ArrayList<TypeToken>(newArgs.size()); for(SqlExpr newArg : newArgs) { argTypes.add(newArg.getDatatype()); } FunctionModel<TypeToken> functionModel = typeSystem.getSqlFunctionModel(); Multimap<String, String> sparqlSqlDecls = typeSystem.getSparqlSqlDecls(); CandidateMethod<TypeToken> candidate = TypeSystemUtils.lookupSqlCandidate(functionModel, sparqlSqlDecls, functionId, argTypes); SqlExpr result = null; if(candidate != null) { /* Check if we can apply an inverse function: /* The rules are: * - The function must be a comparator (these implicitly take 2 arguments) * - One of the arguments is a function expression whose symbol has an inverse defined * - The other operand is a constant */ FunctionModelMeta sqlMetaModel = typeSystem.getSqlFunctionMetaModel(); String opId = candidate.getMethod().getId(); // TODO Perform short cut evaluation of logical operators if(sqlMetaModel.getLogicalAnds().contains(opId)) { SqlExpr a = newArgs.get(0); SqlExpr b = newArgs.get(1); if(b.isConstant()) { SqlExpr tmp = a; a = b; b = tmp; } if(a.isConstant() && a.asConstant().equals(S_Constant.TRUE)) { result = b; } } // else if(sqlMetaModel.getLogicalOrs().contains(opId)) { // // } // else if(sqlMetaModel.getLogicalNots().contains(opId)) { // // } else if(sqlMetaModel.getComparators().contains(opId)) { // TODO: We need the name of the comparator //typeSystem.getSparqlSqlDecls() SqlExpr a = newArgs.get(0); SqlExpr b = newArgs.get(1); if(b.isConstant()) { SqlExpr tmp = a; a = b; b = tmp; } if(a.isConstant() && b.isFunction()) { String fnId = b.asFunction().getName(); String invId = sqlMetaModel.getInverses().get(fnId); if(invId != null) { Map<String, SqlExprEvaluator> sqlImpls = typeSystem.getSqlImpls(); SqlExprEvaluator see = sqlImpls.get(invId); SqlExpr c = b.getArgs().get(0); if(see == null) { throw new RuntimeException("Inverse " + invId + " of " + fnId + " declared, but no implementation provided"); } SqlExpr d = see.eval(Collections.singletonList(a)); List<SqlExpr> aa = Arrays.asList(d, c); result = processFunction(functionId, aa); // Lookp an comparator for the new argument types } } // Optimize expressions such as equals(str(int), str(int)) to simply equals(int, int) else if(a.isFunction() && b.isFunction()) { SqlExprFunction fnA = a.asFunction(); SqlExprFunction fnB = b.asFunction(); String nameA = fnA.getName(); String nameB = fnB.getName(); if(nameA.equals(nameB)) { // TODO ... and if nameA and nameB are injective... //String name = functionModel.getNameById(nameA); List<SqlExpr> argsA = fnA.getArgs(); List<SqlExpr> argsB = fnB.getArgs(); List<SqlExpr> bb = new ArrayList<SqlExpr>(argsA.size() + argsB.size()); bb.addAll(argsA); bb.addAll(argsB); /* //List<TypeToken> List<TypeToken> types = SqlExprUtils.getTypes(fnA.getArgs()); List<TypeToken> typesB = SqlExprUtils.getTypes(fnB.getArgs()); types.addAll(typesB); CandidateMethod<TypeToken> cd = TypeSystemImpl.lookupSqlCandidate(functionModel, sparqlSqlDecls, name, types); */ result = processFunction(functionId, bb); } } } if(result == null) { result = createSqlExpr(candidate, newArgs); } } else { // Type error logger.info("Yielding type error because no signature found for: " + functionId + " with arguments " + argTypes); result = S_Constant.TYPE_ERROR; //throw new RuntimeException("Type error.... needs to be handled - No function found: " + functionId + " with argtypes " + argTypes); } return result; } // SparqlFunction sparqlFunction = functionProvider.getSparqlFunction(functionId); // if(sparqlFunction == null) { // throw new RuntimeException("Sparql function not declared: " + functionId); // } // // SqlExprEvaluator evaluator = sparqlFunction.getEvaluator(); // // logger.debug("Evaluator for '" + functionId + "': " + evaluator); // // // If there is an evaluator, we can pass all arguments to it, and see if it yields a new expression // if(evaluator != null) { // SqlExpr tmp = evaluator.eval(newArgs); // if(tmp != null) { // result = new ExprHolder(tmp); // //return tmp; // } else { // throw new RuntimeException("Evaluator yeld null value"); // } // } else { // If there is no evaluator, use the default behavior: // MethodSignature<TypeToken> signature = sparqlFunction.getSignature(); // if(signature != null) { // // TypeToken returnType = signature.getReturnType(); // if(returnType != null) { //SqlExpr tmp = S_Function.create(returnType, functionId, newArgs); // } else { // throw new RuntimeException("Return type is null: " + signature); // } // // } else { // throw new RuntimeException("Neither evaluator nor signature found for " + functionId + " in " + fn); // } // } // Check if the functionProvider has a definition for the functionId public static SqlExpr createSqlExpr(CandidateMethod<TypeToken> candidate, SqlExpr ... args) { return createSqlExpr(candidate, Arrays.asList(args)); } public static SqlExpr createSqlExpr(CandidateMethod<TypeToken> candidate, List<SqlExpr> args) { MethodEntry<TypeToken> method = candidate.getMethod(); List<CandidateMethod<TypeToken>> coercions = candidate.getCoercions(); List<SqlExpr> newArgs; if(coercions != null) { // Apply coercions newArgs = new ArrayList<SqlExpr>(args.size()); for(int i = 0; i < args.size(); ++i) { SqlExpr arg = args.get(i); CandidateMethod<TypeToken> coercion = coercions.get(i); SqlExpr newArg; if(coercion != null) { newArg = createSqlExpr(coercion, Collections.singletonList(arg)); } else { newArg = arg; } newArgs.add(newArg); } } else { newArgs = args; } TypeToken returnType = method.getSignature().getReturnType(); String functionId = method.getId(); SqlExpr result = S_Function.create(returnType, functionId, newArgs); return result; } /** * This function requires a type-constructor free expression as input: * That is an expression that can be translated directly to SQL - * i.e. all bnode/uri/literal type constructors have been removed from it * * * TODO How to pass the type error to SPARQL functions, * such as logical AND/OR/NOT, so they get a chance to deal with it? * * Using the SPARQL level evaluator is not really possible anymore, because we already translated to the SQL level. * * We could either: * . have special treatment for logical and/or/not * But then we can't extend the system to our liking * . have an evaluator on the SqlExpr level, rather than the expr level * Very generic, but can we avoid the duplication with Expr and SqlExpr? * Probably we can't. * The expr structure does not allow adding a custom datatype, and mapping it externally turned out to be quite a hassle. * * * * @param fn * @param binding * @param typeMap * @return */ public SqlExpr translate(ExprFunction fn, Map<String, TypeToken> typeMap) { SqlExpr result; List<SqlExpr> evaledArgs = new ArrayList<SqlExpr>(); logger.debug("Processing: " + fn); /* if(containsTypeError(evaledArgs)) { logger.debug("Type error in argument (" + evaledArgs + ")"); return S_Constant.TYPE_ERROR; } */ for(Expr arg : fn.getArgs()) { SqlExpr evaledArg = translate(arg, typeMap); // If an argument evaluated to type error, return type error // TODO: Distinguish between null and type error. Currently we use nvNothing which actually corresponds to NULL // (currently represented with nvNothing - is that safe? - Rather no - see above) /* if(evaledArg.equals(NodeValue.nvNothing)) { return NodeValue.nvNothing; } */ evaledArgs.add(evaledArg); } // List<TypeToken> argTypes = getTypes(evaledArgs); // There must be a function registered for the argument types String functionId = ExprUtils.getFunctionId(fn); // String datatype = exprTypeEvaluator.evaluateType(fn); // if(datatype == null) { // throw new RuntimeException("No datatype could be obtained for " + fn); // } // // //result = S_Function.create(datatype, functionId, evaledArgs); SparqlFunction sparqlFunction = functionProvider.getSparqlFunction(functionId); if(sparqlFunction == null) { throw new RuntimeException("Sparql function not declared: " + functionId); } SqlExprEvaluator evaluator = sparqlFunction.getEvaluator(); logger.debug("Evaluator for '" + functionId + "': " + evaluator); // If there is an evaluator, we can pass all arguments to it, and see if it yields a new expression if(evaluator != null) { SqlExpr tmp = evaluator.eval(evaledArgs); if(tmp != null) { return tmp; } else { throw new RuntimeException("Evaluator yeld null value"); } } // If there is no evaluator, use the default behaviour: MethodSignature<TypeToken> signature = sparqlFunction.getSignature(); if(signature != null) { TypeToken returnType = signature.getReturnType(); if(returnType != null) { result = S_Function.create(returnType, functionId, evaledArgs); } } // Check if the functionProvider has a definition for the functionId throw new RuntimeException("Neither evaluator nor signature found for " + fn); // If there was no evaluator, or if the evaluator returned null, continue here. // // TODO: New approach: There must always be an evaluator // // // If one of the arguments is a type error, we must return a type error. // if(containsTypeError(evaledArgs)) { // return S_Constant.TYPE_ERROR; // } // // SqlMethodCandidate castMethod = datatypeSystem.lookupMethod(functionId, argTypes); // // if(castMethod == null) { // //throw new RuntimeException("No method found for " + fn); // logger.debug("No method found for " + fn); // // return S_Constant.TYPE_ERROR; // } // // // TODO: Invoke the SQL method's invocable if it exists and all arguments are constants // // result = S_Method.createOrEvaluate(castMethod, evaledArgs); // // logger.debug("[Result] " + result); // // return result; } public SqlExpr translate(NodeValue expr) { SqlValue sqlValue = typeSystem.convertSql(expr); SqlExpr result = S_Constant.create(sqlValue); return result; } public SqlExpr translate(ExprVar expr, Map<String, TypeToken> typeMap) { String varName = expr.getVarName(); TypeToken datatype = typeMap.get(varName); if(datatype == null) { throw new RuntimeException("No datatype found for " + varName); } IBiSetMultimap<TypeToken, TypeToken> ptm = typeSystem.getPhysicalTypeMap(); Set<TypeToken> schematicTypes = ptm.get(datatype); TypeToken schematicType; if(schematicTypes.isEmpty()) { schematicType = datatype; //typeSystem.get //throw new RuntimeException("Physical type " + datatype + " not known"); } else if(schematicTypes.size() > 1) { throw new RuntimeException("Multiple mappings for physical type " + datatype + ": " + schematicTypes); } else { schematicType = schematicTypes.iterator().next(); } // We need to map the phyical datatype to a schematic one // e.g. varchar -> string SqlExpr result = new S_ColumnRef(schematicType, varName); return result; } /* * How to best add interceptors (callbacks with transformation) for certain functions? * * e.g.: concat(foo, concat(?x...)) -> concat(foo, ?x) * lang(rdfterm(2, ?x, ?y, '')) -> ?y * * The main question is, whether to apply to callback before or after the arguments are evaluated. * * -> After makes more sense: Then we have constant folder arguments */ public SqlExpr translate(Expr expr, Map<String, TypeToken> typeMap) { //assert expr != null : "Null pointer exception"; if(expr == null) { throw new NullPointerException(); } //System.out.println(expr); SqlExpr result = null; if(expr.isConstant()) { result = translate(expr.getConstant()); } else if(expr.isFunction()) { ExprFunction fn = expr.getFunction(); result = translate(fn, typeMap); } else if(expr.isVariable()) { result = translate(expr.getExprVar(), typeMap); } else { throw new RuntimeException("Unknown expression type encountered: " + expr); } return result; } }