Repository: marmotta Updated Branches: refs/heads/develop 9926a4e81 -> 60867dac7
SPARQL: - support GROUP_CONCAT - support IF - implement SupportFinder as visitor Project: http://git-wip-us.apache.org/repos/asf/marmotta/repo Commit: http://git-wip-us.apache.org/repos/asf/marmotta/commit/60867dac Tree: http://git-wip-us.apache.org/repos/asf/marmotta/tree/60867dac Diff: http://git-wip-us.apache.org/repos/asf/marmotta/diff/60867dac Branch: refs/heads/develop Commit: 60867dac79460fb3399ce2860302cf15989243d6 Parents: 9926a4e Author: Sebastian Schaffert <[email protected]> Authored: Fri Nov 7 14:43:21 2014 +0100 Committer: Sebastian Schaffert <[email protected]> Committed: Fri Nov 7 14:43:21 2014 +0100 ---------------------------------------------------------------------- .../kiwi/sparql/builder/SQLBuilder.java | 4 +- .../sparql/builder/collect/OPTypeFinder.java | 13 +- .../sparql/builder/collect/SupportedFinder.java | 161 ++++ .../builder/eval/ExpressionEvaluator.java | 752 ----------------- .../builder/eval/ValueExpressionEvaluator.java | 810 +++++++++++++++++++ .../evaluation/KiWiEvaluationStrategyImpl.java | 192 +---- .../marmotta/kiwi/persistence/KiWiDialect.java | 5 + .../marmotta/kiwi/persistence/h2/H2Dialect.java | 18 + .../kiwi/persistence/mysql/MySQLDialect.java | 18 + .../persistence/pgsql/PostgreSQLDialect.java | 18 + 10 files changed, 1046 insertions(+), 945 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/marmotta/blob/60867dac/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/SQLBuilder.java ---------------------------------------------------------------------- diff --git a/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/SQLBuilder.java b/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/SQLBuilder.java index 99a3537..08b602c 100644 --- a/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/SQLBuilder.java +++ b/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/SQLBuilder.java @@ -23,7 +23,7 @@ import org.apache.marmotta.kiwi.model.rdf.KiWiNode; import org.apache.marmotta.kiwi.persistence.KiWiDialect; import org.apache.marmotta.kiwi.sail.KiWiValueFactory; import org.apache.marmotta.kiwi.sparql.builder.collect.*; -import org.apache.marmotta.kiwi.sparql.builder.eval.ExpressionEvaluator; +import org.apache.marmotta.kiwi.sparql.builder.eval.ValueExpressionEvaluator; import org.apache.marmotta.kiwi.sparql.builder.model.SQLAbstractSubquery; import org.apache.marmotta.kiwi.sparql.builder.model.SQLFragment; import org.apache.marmotta.kiwi.sparql.builder.model.SQLPattern; @@ -807,7 +807,7 @@ public class SQLBuilder { private String evaluateExpression(ValueExpr expr, final OPTypes optype) { - return new ExpressionEvaluator(expr, this, optype).build(); + return new ValueExpressionEvaluator(expr, this, optype).build(); } http://git-wip-us.apache.org/repos/asf/marmotta/blob/60867dac/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/collect/OPTypeFinder.java ---------------------------------------------------------------------- diff --git a/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/collect/OPTypeFinder.java b/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/collect/OPTypeFinder.java index 5dc7938..47c0d5a 100644 --- a/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/collect/OPTypeFinder.java +++ b/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/collect/OPTypeFinder.java @@ -72,7 +72,7 @@ public class OPTypeFinder extends QueryModelVisitorBase<RuntimeException> { || StringUtils.equals(Namespaces.NS_XSD + "time", type)) { optypes.add(OPTypes.DATE); } else { - optypes.add(OPTypes.STRING); + optypes.add(OPTypes.ANY); } } else { optypes.add(OPTypes.STRING); @@ -80,6 +80,12 @@ public class OPTypeFinder extends QueryModelVisitorBase<RuntimeException> { } @Override + public void meet(SameTerm node) throws RuntimeException { + optypes.add(OPTypes.BOOL); + } + + + @Override public void meet(Str node) throws RuntimeException { optypes.add(OPTypes.STRING); } @@ -109,6 +115,11 @@ public class OPTypeFinder extends QueryModelVisitorBase<RuntimeException> { } } + @Override + public void meet(If node) throws RuntimeException { + node.getResult().visit(this); + node.getAlternative().visit(this); + } public OPTypes coerce() { OPTypes left = OPTypes.ANY; http://git-wip-us.apache.org/repos/asf/marmotta/blob/60867dac/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/collect/SupportedFinder.java ---------------------------------------------------------------------- diff --git a/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/collect/SupportedFinder.java b/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/collect/SupportedFinder.java new file mode 100644 index 0000000..02946a3 --- /dev/null +++ b/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/collect/SupportedFinder.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.marmotta.kiwi.sparql.builder.collect; + +import org.apache.marmotta.kiwi.persistence.KiWiDialect; +import org.apache.marmotta.kiwi.sparql.function.NativeFunctionRegistry; +import org.openrdf.query.algebra.*; +import org.openrdf.query.algebra.helpers.QueryModelVisitorBase; + +/** + * Check if all constructs in the query are supported natively. Whenever you add a new construct to SQLBuilder + * or ValueExpressionEvaluator, it should be removed here. + * + * @author Sebastian Schaffert ([email protected]) + */ +public class SupportedFinder extends QueryModelVisitorBase<RuntimeException> { + + private boolean supported = true; + private KiWiDialect dialect; + + public SupportedFinder(TupleExpr expr, KiWiDialect dialect) { + this.dialect = dialect; + + expr.visit(this); + } + + public SupportedFinder(ValueExpr expr, KiWiDialect dialect) { + this.dialect = dialect; + + expr.visit(this); + } + + public boolean isSupported() { + return supported; + } + + + @Override + public void meet(ArbitraryLengthPath node) throws RuntimeException { + supported = false; + } + + @Override + public void meet(BindingSetAssignment node) throws RuntimeException { + supported = false; + } + + @Override + public void meet(CompareAll node) throws RuntimeException { + supported = false; + } + + @Override + public void meet(CompareAny node) throws RuntimeException { + supported = false; + } + + @Override + public void meet(Count node) throws RuntimeException { + if(!dialect.isArraySupported()) { + supported = false; + } else { + super.meet(node); + } + } + + + @Override + public void meet(DescribeOperator node) throws RuntimeException { + supported = false; + } + + + @Override + public void meet(Difference node) throws RuntimeException { + supported = false; + } + + @Override + public void meet(EmptySet node) throws RuntimeException { + supported = false; + } + + @Override + public void meet(FunctionCall node) throws RuntimeException { + if(!isFunctionSupported(node)) { + supported = false; + } else { + super.meet(node); + } + } + + @Override + public void meet(Intersection node) throws RuntimeException { + supported = false; + } + + + + @Override + public void meet(MultiProjection node) throws RuntimeException { + supported = false; + } + + @Override + public void meet(Sample node) throws RuntimeException { + supported = false; + } + + @Override + public void meet(Service node) throws RuntimeException { + supported = false; + } + + @Override + public void meet(ZeroLengthPath node) throws RuntimeException { + supported = false; + } + + @Override + public void meet(ListMemberOperator node) throws RuntimeException { + supported = false; + } + + /** + * All update expressions are not directly supported; however, their query parts should work fine! + */ + @Override + protected void meetUpdateExpr(UpdateExpr node) throws RuntimeException { + supported = false; + } + + private boolean isFunctionSupported(FunctionCall fc) { + return NativeFunctionRegistry.getInstance().get(fc.getURI()) != null && NativeFunctionRegistry.getInstance().get(fc.getURI()).isSupported(dialect); + } + + + private static boolean isAtomic(ValueExpr expr) { + return expr instanceof Var || expr instanceof ValueConstant; + } + + private static boolean isConstant(ValueExpr expr) { + return expr instanceof ValueConstant; + } + +} http://git-wip-us.apache.org/repos/asf/marmotta/blob/60867dac/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/eval/ExpressionEvaluator.java ---------------------------------------------------------------------- diff --git a/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/eval/ExpressionEvaluator.java b/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/eval/ExpressionEvaluator.java deleted file mode 100644 index 8518070..0000000 --- a/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/eval/ExpressionEvaluator.java +++ /dev/null @@ -1,752 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.marmotta.kiwi.sparql.builder.eval; - -import com.google.common.base.Preconditions; -import org.apache.commons.lang3.StringUtils; -import org.apache.marmotta.commons.collections.CollectionUtils; -import org.apache.marmotta.commons.util.DateUtils; -import org.apache.marmotta.kiwi.model.rdf.KiWiNode; -import org.apache.marmotta.kiwi.sparql.builder.OPTypes; -import org.apache.marmotta.kiwi.sparql.builder.ProjectionType; -import org.apache.marmotta.kiwi.sparql.builder.SQLBuilder; -import org.apache.marmotta.kiwi.sparql.builder.collect.OPTypeFinder; -import org.apache.marmotta.kiwi.sparql.builder.model.SQLVariable; -import org.apache.marmotta.kiwi.sparql.function.NativeFunction; -import org.apache.marmotta.kiwi.sparql.function.NativeFunctionRegistry; -import org.openrdf.model.BNode; -import org.openrdf.model.Literal; -import org.openrdf.model.URI; -import org.openrdf.model.vocabulary.FN; -import org.openrdf.model.vocabulary.XMLSchema; -import org.openrdf.query.algebra.*; -import org.openrdf.query.algebra.helpers.QueryModelVisitorBase; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.regex.Pattern; - -/** - * Evaluate a SPARQL ValueExpr by translating it into a SQL expression. - * - * @author Sebastian Schaffert ([email protected]) - */ -public class ExpressionEvaluator extends QueryModelVisitorBase<RuntimeException> { - - private static Logger log = LoggerFactory.getLogger(ExpressionEvaluator.class); - - /** - * Date format used for SQL timestamps. - */ - private static final DateFormat sqlDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S"); - - /** - * Reference to the registry of natively supported functions with parameter and return types as well as SQL translation - */ - private static NativeFunctionRegistry functionRegistry = NativeFunctionRegistry.getInstance(); - - // used by BNodeGenerator - private static Random anonIdGenerator = new Random(); - - - private StringBuilder builder = new StringBuilder(); - - private Deque<OPTypes> optypes = new ArrayDeque<>(); - - private SQLBuilder parent; - - public ExpressionEvaluator(ValueExpr expr, SQLBuilder parent) { - this(expr,parent, OPTypes.ANY); - } - - public ExpressionEvaluator(ValueExpr expr, SQLBuilder parent, OPTypes optype) { - this.parent = parent; - - optypes.push(optype); - - if(log.isTraceEnabled()) { - long start = System.currentTimeMillis(); - expr.visit(this); - log.trace("expression evaluated in {} ms", (System.currentTimeMillis()-start)); - } else { - expr.visit(this); - } - } - - - /** - * Create the actual SQL string generated by this evaluator. - * - * @return - */ - public String build() { - return builder.toString(); - } - - @Override - public void meet(And node) throws RuntimeException { - builder.append("("); - node.getLeftArg().visit(this); - builder.append(" AND "); - node.getRightArg().visit(this); - builder.append(")"); - } - - @Override - public void meet(Or node) throws RuntimeException { - builder.append("("); - node.getLeftArg().visit(this); - builder.append(" OR "); - node.getRightArg().visit(this); - builder.append(")"); - } - - @Override - public void meet(Not node) throws RuntimeException { - builder.append("NOT ("); - node.getArg().visit(this); - builder.append(")"); - } - - @Override - public void meet(Exists node) throws RuntimeException { - // TODO: need to make sure that variables of the parent are visible in the subquery - // - pattern names need to be unique even in subqueries - // - variable lookup for expressions in the subquery need to refer to the parent - SQLBuilder sq_builder = new SQLBuilder(node.getSubQuery(), parent.getBindings(), parent.getDataset(), parent.getConverter(), parent.getDialect(), "_", Collections.EMPTY_SET, copyVariables(parent.getVariables())); - - builder.append("EXISTS (").append(sq_builder.build()).append(")"); - } - - @Override - public void meet(FunctionCall fc) throws RuntimeException { - // special optimizations for frequent cases with variables - if((XMLSchema.DOUBLE.toString().equals(fc.getURI()) || XMLSchema.FLOAT.toString().equals(fc.getURI()) ) && fc.getArgs().size() == 1) { - optypes.push(OPTypes.DOUBLE); - fc.getArgs().get(0).visit(this); - optypes.pop(); - } else if((XMLSchema.INTEGER.toString().equals(fc.getURI()) || XMLSchema.INT.toString().equals(fc.getURI())) && fc.getArgs().size() == 1) { - optypes.push(OPTypes.INT); - fc.getArgs().get(0).visit(this); - optypes.pop(); - } else if(XMLSchema.BOOLEAN.toString().equals(fc.getURI()) && fc.getArgs().size() == 1) { - optypes.push(OPTypes.BOOL); - fc.getArgs().get(0).visit(this); - optypes.pop(); - } else if(XMLSchema.DATE.toString().equals(fc.getURI()) && fc.getArgs().size() == 1) { - optypes.push(OPTypes.DATE); - fc.getArgs().get(0).visit(this); - optypes.pop(); - } else { - - String fnUri = fc.getURI(); - - String[] args = new String[fc.getArgs().size()]; - - NativeFunction nf = functionRegistry.get(fnUri); - - if (nf != null && nf.isSupported(parent.getDialect())) { - - for (int i = 0; i < args.length; i++) { - args[i] = new ExpressionEvaluator(fc.getArgs().get(i), parent, nf.getArgumentType(i)).build(); - } - - if (optypes.peek() != nf.getReturnType()) { - builder.append(castExpression(nf.getNative(parent.getDialect(), args), optypes.peek())); - } else { - builder.append(nf.getNative(parent.getDialect(), args)); - } - } else { - throw new IllegalArgumentException("the function " + fnUri + " is not supported by the SQL translation"); - } - } - - } - - @Override - public void meet(Avg node) throws RuntimeException { - builder.append("AVG("); - optypes.push(OPTypes.DOUBLE); - node.getArg().visit(this); - optypes.pop(); - builder.append(")"); - } - - @Override - public void meet(BNodeGenerator gen) throws RuntimeException { - if(gen.getNodeIdExpr() != null) { - // get value of argument and express it as string - optypes.push(OPTypes.STRING); - gen.getNodeIdExpr().visit(this); - optypes.pop(); - } else { - builder.append("'").append(Long.toHexString(System.currentTimeMillis())+Integer.toHexString(anonIdGenerator.nextInt(1000))).append("'"); - } - } - - @Override - public void meet(Bound node) throws RuntimeException { - ValueExpr arg = node.getArg(); - - if(arg instanceof ValueConstant) { - builder.append(Boolean.toString(true)); - } else if(arg instanceof Var) { - builder.append("("); - arg.visit(this); - builder.append(" IS NOT NULL)"); - } - } - - @Override - public void meet(Coalesce node) throws RuntimeException { - builder.append("COALESCE("); - for(Iterator<ValueExpr> it = node.getArguments().iterator(); it.hasNext(); ) { - it.next().visit(this); - if(it.hasNext()) { - builder.append(", "); - } - } - builder.append(")"); - } - - @Override - public void meet(Compare cmp) throws RuntimeException { - optypes.push(new OPTypeFinder(cmp).coerce()); - cmp.getLeftArg().visit(this); - builder.append(getSQLOperator(cmp.getOperator())); - cmp.getRightArg().visit(this); - optypes.pop(); - } - - @Override - public void meet(Count node) throws RuntimeException { - builder.append("COUNT("); - - if(node.isDistinct()) { - builder.append("DISTINCT "); - } - - if(node.getArg() == null) { - // this is a weird special case where we need to expand to all variables selected in the query wrapped - // by the group; we cannot simply use "*" because the concept of variables is a different one in SQL, - // so instead we construct an ARRAY of the bindings of all variables - - List<String> countVariables = new ArrayList<>(); - for(SQLVariable v : parent.getVariables().values()) { - if(v.getProjectionType() == ProjectionType.NONE) { - Preconditions.checkState(v.getExpressions().size() > 0, "no expressions available for variable"); - - countVariables.add(v.getExpressions().get(0)); - } - } - builder.append("ARRAY["); - builder.append(CollectionUtils.fold(countVariables,",")); - builder.append("]"); - - } else { - optypes.push(OPTypes.ANY); - node.getArg().visit(this); - optypes.pop(); - } - builder.append(")"); - } - - @Override - public void meet(IsBNode node) throws RuntimeException { - ValueExpr arg = node.getArg(); - - // operator must be a variable or a constant - if(arg instanceof ValueConstant) { - builder.append(Boolean.toString(((ValueConstant) arg).getValue() instanceof BNode)); - } else if(arg instanceof Var) { - String var = getVariableAlias((Var) arg); - - builder.append(var).append(".ntype = 'bnode'"); - } - } - - @Override - public void meet(IsLiteral node) throws RuntimeException { - ValueExpr arg = node.getArg(); - - // operator must be a variable or a constant - if (arg instanceof ValueConstant) { - builder.append(Boolean.toString(((ValueConstant) arg).getValue() instanceof Literal)); - } else if(arg instanceof Var) { - String var = getVariableAlias((Var) arg); - - Preconditions.checkState(var != null, "no alias available for variable"); - - builder.append("(") - .append(var) - .append(".ntype = 'string' OR ") - .append(var) - .append(".ntype = 'int' OR ") - .append(var) - .append(".ntype = 'double' OR ") - .append(var) - .append(".ntype = 'date' OR ") - .append(var) - .append(".ntype = 'boolean')"); - } - } - - @Override - public void meet(IsNumeric node) throws RuntimeException { - ValueExpr arg = node.getArg(); - - // operator must be a variable or a constant - if (arg instanceof ValueConstant) { - try { - Double.parseDouble(((ValueConstant) arg).getValue().stringValue()); - builder.append(Boolean.toString(true)); - } catch (NumberFormatException ex) { - builder.append(Boolean.toString(false)); - } - } else if(arg instanceof Var) { - String var = getVariableAlias((Var) arg); - - Preconditions.checkState(var != null, "no alias available for variable"); - - builder.append("(") - .append(var) - .append(".ntype = 'int' OR ") - .append(var) - .append(".ntype = 'double')"); - } - } - - @Override - public void meet(IsResource node) throws RuntimeException { - ValueExpr arg = node.getArg(); - - // operator must be a variable or a constant - if(arg instanceof ValueConstant) { - builder.append(Boolean.toString(((ValueConstant) arg).getValue() instanceof URI || ((ValueConstant) arg).getValue() instanceof BNode)); - } else if(arg instanceof Var) { - String var = getVariableAlias((Var) arg); - - Preconditions.checkState(var != null, "no alias available for variable"); - - builder .append("(") - .append(var) - .append(".ntype = 'uri' OR ") - .append(var) - .append(".ntype = 'bnode')"); - } - } - - @Override - public void meet(IsURI node) throws RuntimeException { - ValueExpr arg = node.getArg(); - - // operator must be a variable or a constant - if(arg instanceof ValueConstant) { - builder.append(Boolean.toString(((ValueConstant) arg).getValue() instanceof URI)); - } else if(arg instanceof Var) { - String var = getVariableAlias((Var) arg); - - Preconditions.checkState(var != null, "no alias available for variable"); - - builder.append(var).append(".ntype = 'uri'"); - } - } - - @Override - public void meet(IRIFunction fun) throws RuntimeException { - if(fun.getBaseURI() != null) { - - String ex = new ExpressionEvaluator(fun.getArg(), parent, OPTypes.STRING).build(); - - builder - .append("CASE WHEN position(':' IN ").append(ex).append(") > 0 THEN ").append(ex) - .append(" ELSE ").append(functionRegistry.get(FN.CONCAT.stringValue()).getNative(parent.getDialect(), "'" + fun.getBaseURI() + "'", ex)) - .append(" END "); - } else { - // get value of argument and express it as string - optypes.push(OPTypes.STRING); - fun.getArg().visit(this); - optypes.pop(); - } - } - - @Override - public void meet(Label node) throws RuntimeException { - optypes.push(OPTypes.STRING); - node.getArg().visit(this); - optypes.pop(); - } - - @Override - public void meet(Lang lang) throws RuntimeException { - if(lang.getArg() instanceof Var) { - String var = getVariableAlias((Var) lang.getArg()); - Preconditions.checkState(var != null, "no alias available for variable"); - - builder.append(var); - builder.append(".lang"); - } - } - - @Override - public void meet(LangMatches lm) throws RuntimeException { - ValueConstant pattern = (ValueConstant) lm.getRightArg(); - - if(pattern.getValue().stringValue().equals("*")) { - lm.getLeftArg().visit(this); - builder.append(" LIKE '%'"); - } else if(pattern.getValue().stringValue().equals("")) { - lm.getLeftArg().visit(this); - builder.append(" IS NULL"); - } else { - builder.append("("); - lm.getLeftArg().visit(this); - builder.append(" = '"); - builder.append(pattern.getValue().stringValue().toLowerCase()); - builder.append("' OR "); - lm.getLeftArg().visit(this); - builder.append(" LIKE '"); - builder.append(pattern.getValue().stringValue().toLowerCase()); - builder.append("-%' )"); - } - } - - @Override - public void meet(LocalName node) throws RuntimeException { - super.meet(node); - } - - @Override - public void meet(MathExpr expr) throws RuntimeException { - OPTypes ot = new OPTypeFinder(expr).coerce(); - - if(ot == OPTypes.STRING) { - if(expr.getOperator() == MathExpr.MathOp.PLUS) { - builder.append(functionRegistry.get(FN.CONCAT.stringValue()).getNative(parent.getDialect(),new ExpressionEvaluator(expr.getLeftArg(), parent, ot).build(), new ExpressionEvaluator(expr.getRightArg(), parent, ot).build())); - } else { - throw new IllegalArgumentException("operation "+expr.getOperator()+" is not supported on strings"); - } - } else { - if(ot == OPTypes.ANY || ot == OPTypes.TERM) { - ot = OPTypes.DOUBLE; - } - - optypes.push(ot); - expr.getLeftArg().visit(this); - builder.append(getSQLOperator(expr.getOperator())); - expr.getRightArg().visit(this); - optypes.pop(); - } - } - - @Override - public void meet(Max node) throws RuntimeException { - builder.append("MAX("); - optypes.push(OPTypes.DOUBLE); - node.getArg().visit(this); - optypes.pop(); - builder.append(")"); - } - - @Override - public void meet(Min node) throws RuntimeException { - builder.append("MIN("); - optypes.push(OPTypes.DOUBLE); - node.getArg().visit(this); - optypes.pop(); - builder.append(")"); - } - - @Override - public void meet(Regex re) throws RuntimeException { - builder.append(optimizeRegexp( - new ExpressionEvaluator(re.getArg(), parent, OPTypes.STRING).build(), - new ExpressionEvaluator(re.getPatternArg(), parent, OPTypes.STRING).build(), - re.getFlagsArg() - )); - } - - @Override - public void meet(SameTerm cmp) throws RuntimeException { - // covered by value binding in variables - optypes.push(OPTypes.TERM); - cmp.getLeftArg().visit(this); - builder.append(" = "); - cmp.getRightArg().visit(this); - optypes.pop(); - } - - @Override - public void meet(Str node) throws RuntimeException { - optypes.push(OPTypes.STRING); - node.getArg().visit(this); - optypes.pop(); - } - - @Override - public void meet(Sum node) throws RuntimeException { - builder.append("SUM("); - optypes.push(OPTypes.DOUBLE); - node.getArg().visit(this); - optypes.pop(); - builder.append(")"); - } - - @Override - public void meet(Var node) throws RuntimeException { - // distinguish between the case where the variable is plain and the variable is bound - SQLVariable sv = parent.getVariables().get(node.getName()); - - if(sv == null) { - builder.append("NULL"); - } else if(sv.getBindings().size() > 0) { - // in case the variable is actually an alias for an expression, we evaluate that expression instead, effectively replacing the - // variable occurrence with its value - sv.getBindings().get(0).visit(this); - } else { - String var = sv.getAlias(); - - if(sv.getProjectionType() != ProjectionType.NODE && sv.getProjectionType() != ProjectionType.NONE) { - // in case the variable represents a constructed or bound value instead of a node, we need to - // use the SQL expression as value; SQL should take care of proper casting... - // TODO: explicit casting needed? - builder.append(sv.getExpressions().get(0)); - } else { - // in case the variable represents an entry from the NODES table (i.e. has been bound to a node - // in the database, we take the NODES alias and resolve to the correct column according to the - // operator type - switch (optypes.peek()) { - case STRING: - Preconditions.checkState(var != null, "no alias available for variable"); - builder.append(var).append(".svalue"); - break; - case INT: - Preconditions.checkState(var != null, "no alias available for variable"); - builder.append(var).append(".ivalue"); - break; - case DOUBLE: - Preconditions.checkState(var != null, "no alias available for variable"); - builder.append(var).append(".dvalue"); - break; - case DATE: - Preconditions.checkState(var != null, "no alias available for variable"); - builder.append(var).append(".tvalue"); - break; - case VALUE: - Preconditions.checkState(var != null, "no alias available for variable"); - builder.append(var).append(".svalue"); - break; - case URI: - Preconditions.checkState(var != null, "no alias available for variable"); - builder.append(var).append(".svalue"); - break; - case TERM: - case ANY: - if(sv.getExpressions().size() > 0) { - // this allows us to avoid joins with the nodes table for simple expressions that only need the ID - builder.append(sv.getExpressions().get(0)); - } else { - Preconditions.checkState(var != null, "no alias available for variable"); - builder.append(var).append(".id"); - } - break; - } - } - } - } - - @Override - public void meet(ValueConstant node) throws RuntimeException { - String val = node.getValue().stringValue(); - - switch (optypes.peek()) { - case STRING: - case VALUE: - case URI: - builder.append("'").append(val).append("'"); - break; - case INT: - builder.append(Integer.parseInt(val)); - break; - case DOUBLE: - builder.append(Double.parseDouble(val)); - break; - case DATE: - builder.append("'").append(sqlDateFormat.format(DateUtils.parseDate(val))).append("'"); - break; - - // in this case we should return a node ID and also need to make sure it actually exists - case TERM: - case ANY: - KiWiNode n = parent.getConverter().convert(node.getValue()); - builder.append(n.getId()); - break; - - default: throw new IllegalArgumentException("unsupported value type: " + optypes.peek()); - } - } - - private String getVariableAlias(Var var) { - return parent.getVariables().get(var.getName()).getAlias(); - } - - - private String getVariableAlias(String varName) { - return parent.getVariables().get(varName).getAlias(); - } - - /** - * Copy variables from the set to a new set suitable for a subquery; this allows passing over variable expressions - * from parent queries to subqueries without the subquery adding expressions that are then not visible outside - * @param variables - * @return - */ - private static Map<String, SQLVariable> copyVariables(Map<String, SQLVariable> variables) { - Map<String,SQLVariable> copy = new HashMap<>(); - try { - for(Map.Entry<String,SQLVariable> entry : variables.entrySet()) { - copy.put(entry.getKey(), (SQLVariable) entry.getValue().clone()); - } - } catch (CloneNotSupportedException e) { - log.error("could not clone SQL variable:",e); - } - - return copy; - } - - private String castExpression(String arg, OPTypes type) { - if(type == null) { - return arg; - } - - switch (type) { - case DOUBLE: - return functionRegistry.get(XMLSchema.DOUBLE).getNative(parent.getDialect(), arg); - case INT: - return functionRegistry.get(XMLSchema.INTEGER).getNative(parent.getDialect(), arg); - case BOOL: - return functionRegistry.get(XMLSchema.BOOLEAN).getNative(parent.getDialect(), arg); - case DATE: - return functionRegistry.get(XMLSchema.DATETIME).getNative(parent.getDialect(), arg); - case VALUE: - return arg; - case ANY: - return arg; - default: - return arg; - } - } - - private static String getSQLOperator(Compare.CompareOp op) { - switch (op) { - case EQ: return " = "; - case GE: return " >= "; - case GT: return " > "; - case LE: return " <= "; - case LT: return " < "; - case NE: return " <> "; - } - throw new IllegalArgumentException("unsupported operator type for comparison: "+op); - } - - - private static String getSQLOperator(MathExpr.MathOp op) { - switch (op) { - case PLUS: return " + "; - case MINUS: return " - "; - case DIVIDE: return " / "; - case MULTIPLY: return " / "; - } - throw new IllegalArgumentException("unsupported operator type for math expression: "+op); - } - - /** - * Test if the regular expression given in the pattern can be simplified to a LIKE SQL statement; these are - * considerably more efficient to evaluate in most databases, so in case we can simplify, we return a LIKE. - * - * @param value - * @param pattern - * @return - */ - private String optimizeRegexp(String value, String pattern, ValueExpr flags) { - String _flags = flags != null && flags instanceof ValueConstant ? ((ValueConstant)flags).getValue().stringValue() : null; - - String simplified = pattern; - - // apply simplifications - - // remove SQL quotes at beginning and end - simplified = simplified.replaceFirst("^'",""); - simplified = simplified.replaceFirst("'$",""); - - - // remove .* at beginning and end, they are the default anyways - simplified = simplified.replaceFirst("^\\.\\*",""); - simplified = simplified.replaceFirst("\\.\\*$",""); - - // replace all occurrences of % with \% and _ with \_, as they are special characters in SQL - simplified = simplified.replaceAll("%","\\%"); - simplified = simplified.replaceAll("_","\\_"); - - // if pattern now does not start with a ^, we put a "%" in front - if(!simplified.startsWith("^")) { - simplified = "%" + simplified; - } else { - simplified = simplified.substring(1); - } - - // if pattern does not end with a "$", we put a "%" at the end - if(!simplified.endsWith("$")) { - simplified = simplified + "%"; - } else { - simplified = simplified.substring(0,simplified.length()-1); - } - - // replace all non-escaped occurrences of .* with % - simplified = simplified.replaceAll("(?<!\\\\)\\.\\*","%"); - - // replace all non-escaped occurrences of .+ with _% - simplified = simplified.replaceAll("(?<!\\\\)\\.\\+","_%"); - - // the pattern is not simplifiable if the simplification still contains unescaped regular expression constructs - Pattern notSimplifiable = Pattern.compile("(?<!\\\\)[\\.\\*\\+\\{\\}\\[\\]\\|]"); - - if(notSimplifiable.matcher(simplified).find()) { - return parent.getDialect().getRegexp(value, pattern, _flags); - } else { - if(!simplified.startsWith("%") && !simplified.endsWith("%")) { - if(StringUtils.containsIgnoreCase(_flags, "i")) { - return String.format("lower(%s) = lower('%s')", value, simplified); - } else { - return String.format("%s = '%s'", value, simplified); - } - } else { - if(StringUtils.containsIgnoreCase(_flags,"i")) { - return parent.getDialect().getILike(value, "'" + simplified + "'"); - } else { - return value + " LIKE '"+simplified+"'"; - } - } - } - - } - -} http://git-wip-us.apache.org/repos/asf/marmotta/blob/60867dac/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/eval/ValueExpressionEvaluator.java ---------------------------------------------------------------------- diff --git a/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/eval/ValueExpressionEvaluator.java b/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/eval/ValueExpressionEvaluator.java new file mode 100644 index 0000000..4edf154 --- /dev/null +++ b/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/builder/eval/ValueExpressionEvaluator.java @@ -0,0 +1,810 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.marmotta.kiwi.sparql.builder.eval; + +import com.google.common.base.Preconditions; +import org.apache.commons.lang3.StringUtils; +import org.apache.marmotta.commons.collections.CollectionUtils; +import org.apache.marmotta.commons.util.DateUtils; +import org.apache.marmotta.kiwi.model.rdf.KiWiNode; +import org.apache.marmotta.kiwi.sparql.builder.OPTypes; +import org.apache.marmotta.kiwi.sparql.builder.ProjectionType; +import org.apache.marmotta.kiwi.sparql.builder.SQLBuilder; +import org.apache.marmotta.kiwi.sparql.builder.collect.OPTypeFinder; +import org.apache.marmotta.kiwi.sparql.builder.model.SQLVariable; +import org.apache.marmotta.kiwi.sparql.function.NativeFunction; +import org.apache.marmotta.kiwi.sparql.function.NativeFunctionRegistry; +import org.openrdf.model.BNode; +import org.openrdf.model.Literal; +import org.openrdf.model.URI; +import org.openrdf.model.vocabulary.FN; +import org.openrdf.model.vocabulary.XMLSchema; +import org.openrdf.query.algebra.*; +import org.openrdf.query.algebra.helpers.QueryModelVisitorBase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.regex.Pattern; + +/** + * Evaluate a SPARQL ValueExpr by translating it into a SQL expression. + * + * @author Sebastian Schaffert ([email protected]) + */ +public class ValueExpressionEvaluator extends QueryModelVisitorBase<RuntimeException> { + + private static Logger log = LoggerFactory.getLogger(ValueExpressionEvaluator.class); + + /** + * Date format used for SQL timestamps. + */ + private static final DateFormat sqlDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S"); + + /** + * Reference to the registry of natively supported functions with parameter and return types as well as SQL translation + */ + private static NativeFunctionRegistry functionRegistry = NativeFunctionRegistry.getInstance(); + + // used by BNodeGenerator + private static Random anonIdGenerator = new Random(); + + + private StringBuilder builder = new StringBuilder(); + + private Deque<OPTypes> optypes = new ArrayDeque<>(); + + private SQLBuilder parent; + + public ValueExpressionEvaluator(ValueExpr expr, SQLBuilder parent) { + this(expr,parent, OPTypes.ANY); + } + + public ValueExpressionEvaluator(ValueExpr expr, SQLBuilder parent, OPTypes optype) { + this.parent = parent; + + optypes.push(optype); + + if(log.isTraceEnabled()) { + long start = System.currentTimeMillis(); + expr.visit(this); + log.trace("expression evaluated in {} ms", (System.currentTimeMillis()-start)); + } else { + expr.visit(this); + } + } + + + /** + * Create the actual SQL string generated by this evaluator. + * + * @return + */ + public String build() { + return builder.toString(); + } + + @Override + public void meet(And node) throws RuntimeException { + builder.append("("); + node.getLeftArg().visit(this); + builder.append(" AND "); + node.getRightArg().visit(this); + builder.append(")"); + } + + @Override + public void meet(Or node) throws RuntimeException { + builder.append("("); + node.getLeftArg().visit(this); + builder.append(" OR "); + node.getRightArg().visit(this); + builder.append(")"); + } + + @Override + public void meet(Not node) throws RuntimeException { + builder.append("NOT ("); + node.getArg().visit(this); + builder.append(")"); + } + + @Override + public void meet(Exists node) throws RuntimeException { + // TODO: need to make sure that variables of the parent are visible in the subquery + // - pattern names need to be unique even in subqueries + // - variable lookup for expressions in the subquery need to refer to the parent + SQLBuilder sq_builder = new SQLBuilder(node.getSubQuery(), parent.getBindings(), parent.getDataset(), parent.getConverter(), parent.getDialect(), "_", Collections.EMPTY_SET, copyVariables(parent.getVariables())); + + builder.append("EXISTS (").append(sq_builder.build()).append(")"); + } + + @Override + public void meet(FunctionCall fc) throws RuntimeException { + // special optimizations for frequent cases with variables + if((XMLSchema.DOUBLE.toString().equals(fc.getURI()) || XMLSchema.FLOAT.toString().equals(fc.getURI()) ) && fc.getArgs().size() == 1) { + optypes.push(OPTypes.DOUBLE); + fc.getArgs().get(0).visit(this); + optypes.pop(); + } else if((XMLSchema.INTEGER.toString().equals(fc.getURI()) || XMLSchema.INT.toString().equals(fc.getURI())) && fc.getArgs().size() == 1) { + optypes.push(OPTypes.INT); + fc.getArgs().get(0).visit(this); + optypes.pop(); + } else if(XMLSchema.BOOLEAN.toString().equals(fc.getURI()) && fc.getArgs().size() == 1) { + optypes.push(OPTypes.BOOL); + fc.getArgs().get(0).visit(this); + optypes.pop(); + } else if(XMLSchema.DATE.toString().equals(fc.getURI()) && fc.getArgs().size() == 1) { + optypes.push(OPTypes.DATE); + fc.getArgs().get(0).visit(this); + optypes.pop(); + } else { + + String fnUri = fc.getURI(); + + String[] args = new String[fc.getArgs().size()]; + + NativeFunction nf = functionRegistry.get(fnUri); + + if (nf != null && nf.isSupported(parent.getDialect())) { + + for (int i = 0; i < args.length; i++) { + args[i] = new ValueExpressionEvaluator(fc.getArgs().get(i), parent, nf.getArgumentType(i)).build(); + } + + if (optypes.peek() != nf.getReturnType()) { + builder.append(castExpression(nf.getNative(parent.getDialect(), args), optypes.peek())); + } else { + builder.append(nf.getNative(parent.getDialect(), args)); + } + } else { + throw new IllegalArgumentException("the function " + fnUri + " is not supported by the SQL translation"); + } + } + + } + + @Override + public void meet(Avg node) throws RuntimeException { + builder.append("AVG("); + optypes.push(OPTypes.DOUBLE); + node.getArg().visit(this); + optypes.pop(); + builder.append(")"); + } + + @Override + public void meet(BNodeGenerator gen) throws RuntimeException { + if(gen.getNodeIdExpr() != null) { + // get value of argument and express it as string + optypes.push(OPTypes.STRING); + gen.getNodeIdExpr().visit(this); + optypes.pop(); + } else { + builder.append("'").append(Long.toHexString(System.currentTimeMillis())+Integer.toHexString(anonIdGenerator.nextInt(1000))).append("'"); + } + } + + @Override + public void meet(Bound node) throws RuntimeException { + ValueExpr arg = node.getArg(); + + if(arg instanceof ValueConstant) { + builder.append(Boolean.toString(true)); + } else if(arg instanceof Var) { + builder.append("("); + optypes.push(OPTypes.ANY); + arg.visit(this); + optypes.pop(); + builder.append(" IS NOT NULL)"); + } + } + + @Override + public void meet(Coalesce node) throws RuntimeException { + builder.append("COALESCE("); + for(Iterator<ValueExpr> it = node.getArguments().iterator(); it.hasNext(); ) { + it.next().visit(this); + if(it.hasNext()) { + builder.append(", "); + } + } + builder.append(")"); + } + + @Override + public void meet(Compare cmp) throws RuntimeException { + optypes.push(new OPTypeFinder(cmp).coerce()); + cmp.getLeftArg().visit(this); + builder.append(getSQLOperator(cmp.getOperator())); + cmp.getRightArg().visit(this); + optypes.pop(); + } + + @Override + public void meet(Count node) throws RuntimeException { + builder.append("COUNT("); + + if(node.isDistinct()) { + builder.append("DISTINCT "); + } + + if(node.getArg() == null) { + // this is a weird special case where we need to expand to all variables selected in the query wrapped + // by the group; we cannot simply use "*" because the concept of variables is a different one in SQL, + // so instead we construct an ARRAY of the bindings of all variables + + List<String> countVariables = new ArrayList<>(); + for(SQLVariable v : parent.getVariables().values()) { + if(v.getProjectionType() == ProjectionType.NONE) { + Preconditions.checkState(v.getExpressions().size() > 0, "no expressions available for variable"); + + countVariables.add(v.getExpressions().get(0)); + } + } + builder.append("ARRAY["); + builder.append(CollectionUtils.fold(countVariables,",")); + builder.append("]"); + + } else { + optypes.push(OPTypes.ANY); + node.getArg().visit(this); + optypes.pop(); + } + builder.append(")"); + } + + + @Override + public void meet(GroupConcat node) throws RuntimeException { + if(node.getSeparator() == null) { + builder.append(parent.getDialect().getGroupConcat(new ValueExpressionEvaluator(node.getArg(), parent, OPTypes.STRING).build(), null, node.isDistinct())); + } else { + builder.append(parent.getDialect().getGroupConcat( + new ValueExpressionEvaluator(node.getArg(), parent, OPTypes.STRING).build(), + new ValueExpressionEvaluator(node.getSeparator(), parent, OPTypes.STRING).build(), + node.isDistinct() + )); + } + } + + + @Override + public void meet(If node) throws RuntimeException { + builder.append("CASE WHEN "); + + optypes.push(OPTypes.BOOL); + node.getCondition().visit(this); + optypes.pop(); + + optypes.push(new OPTypeFinder(node).coerce()); + builder.append(" THEN "); + node.getResult().visit(this); + builder.append(" ELSE "); + node.getAlternative().visit(this); + builder.append(" END"); + optypes.pop(); + } + + + @Override + public void meet(IsBNode node) throws RuntimeException { + ValueExpr arg = node.getArg(); + + // operator must be a variable or a constant + if(arg instanceof ValueConstant) { + builder.append(Boolean.toString(((ValueConstant) arg).getValue() instanceof BNode)); + } else if(arg instanceof Var) { + String var = getVariableAlias((Var) arg); + + builder.append(var).append(".ntype = 'bnode'"); + } + } + + @Override + public void meet(IsLiteral node) throws RuntimeException { + ValueExpr arg = node.getArg(); + + // operator must be a variable or a constant + if (arg instanceof ValueConstant) { + builder.append(Boolean.toString(((ValueConstant) arg).getValue() instanceof Literal)); + } else if(arg instanceof Var) { + String var = getVariableAlias((Var) arg); + + Preconditions.checkState(var != null, "no alias available for variable"); + + builder.append("(") + .append(var) + .append(".ntype = 'string' OR ") + .append(var) + .append(".ntype = 'int' OR ") + .append(var) + .append(".ntype = 'double' OR ") + .append(var) + .append(".ntype = 'date' OR ") + .append(var) + .append(".ntype = 'boolean')"); + } + } + + @Override + public void meet(IsNumeric node) throws RuntimeException { + ValueExpr arg = node.getArg(); + + // operator must be a variable or a constant + if (arg instanceof ValueConstant) { + try { + Double.parseDouble(((ValueConstant) arg).getValue().stringValue()); + builder.append(Boolean.toString(true)); + } catch (NumberFormatException ex) { + builder.append(Boolean.toString(false)); + } + } else if(arg instanceof Var) { + String var = getVariableAlias((Var) arg); + + Preconditions.checkState(var != null, "no alias available for variable"); + + builder.append("(") + .append(var) + .append(".ntype = 'int' OR ") + .append(var) + .append(".ntype = 'double')"); + } + } + + @Override + public void meet(IsResource node) throws RuntimeException { + ValueExpr arg = node.getArg(); + + // operator must be a variable or a constant + if(arg instanceof ValueConstant) { + builder.append(Boolean.toString(((ValueConstant) arg).getValue() instanceof URI || ((ValueConstant) arg).getValue() instanceof BNode)); + } else if(arg instanceof Var) { + String var = getVariableAlias((Var) arg); + + Preconditions.checkState(var != null, "no alias available for variable"); + + builder .append("(") + .append(var) + .append(".ntype = 'uri' OR ") + .append(var) + .append(".ntype = 'bnode')"); + } + } + + @Override + public void meet(IsURI node) throws RuntimeException { + ValueExpr arg = node.getArg(); + + // operator must be a variable or a constant + if(arg instanceof ValueConstant) { + builder.append(Boolean.toString(((ValueConstant) arg).getValue() instanceof URI)); + } else if(arg instanceof Var) { + String var = getVariableAlias((Var) arg); + + Preconditions.checkState(var != null, "no alias available for variable"); + + builder.append(var).append(".ntype = 'uri'"); + } + } + + @Override + public void meet(IRIFunction fun) throws RuntimeException { + if(fun.getBaseURI() != null) { + + String ex = new ValueExpressionEvaluator(fun.getArg(), parent, OPTypes.STRING).build(); + + builder + .append("CASE WHEN position(':' IN ").append(ex).append(") > 0 THEN ").append(ex) + .append(" ELSE ").append(functionRegistry.get(FN.CONCAT.stringValue()).getNative(parent.getDialect(), "'" + fun.getBaseURI() + "'", ex)) + .append(" END "); + } else { + // get value of argument and express it as string + optypes.push(OPTypes.STRING); + fun.getArg().visit(this); + optypes.pop(); + } + } + + @Override + public void meet(Label node) throws RuntimeException { + optypes.push(OPTypes.STRING); + node.getArg().visit(this); + optypes.pop(); + } + + @Override + public void meet(Lang lang) throws RuntimeException { + if(lang.getArg() instanceof Var) { + String var = getVariableAlias((Var) lang.getArg()); + Preconditions.checkState(var != null, "no alias available for variable"); + + builder.append(var); + builder.append(".lang"); + } + } + + @Override + public void meet(LangMatches lm) throws RuntimeException { + ValueConstant pattern = (ValueConstant) lm.getRightArg(); + + if(pattern.getValue().stringValue().equals("*")) { + lm.getLeftArg().visit(this); + builder.append(" LIKE '%'"); + } else if(pattern.getValue().stringValue().equals("")) { + lm.getLeftArg().visit(this); + builder.append(" IS NULL"); + } else { + builder.append("("); + lm.getLeftArg().visit(this); + builder.append(" = '"); + builder.append(pattern.getValue().stringValue().toLowerCase()); + builder.append("' OR "); + lm.getLeftArg().visit(this); + builder.append(" LIKE '"); + builder.append(pattern.getValue().stringValue().toLowerCase()); + builder.append("-%' )"); + } + } + + @Override + public void meet(Like node) throws RuntimeException { + if(node.isCaseSensitive()) { + optypes.push(OPTypes.STRING); + node.getArg().visit(this); + optypes.pop(); + + builder.append(" LIKE "); + node.getPattern(); + } else { + builder.append(parent.getDialect().getILike(new ValueExpressionEvaluator(node.getArg(),parent, OPTypes.STRING).build(), node.getOpPattern())); + } + + } + + + @Override + public void meet(LocalName node) throws RuntimeException { + super.meet(node); + } + + @Override + public void meet(MathExpr expr) throws RuntimeException { + OPTypes ot = new OPTypeFinder(expr).coerce(); + + if(ot == OPTypes.STRING) { + if(expr.getOperator() == MathExpr.MathOp.PLUS) { + builder.append(functionRegistry.get(FN.CONCAT.stringValue()).getNative(parent.getDialect(),new ValueExpressionEvaluator(expr.getLeftArg(), parent, ot).build(), new ValueExpressionEvaluator(expr.getRightArg(), parent, ot).build())); + } else { + throw new IllegalArgumentException("operation "+expr.getOperator()+" is not supported on strings"); + } + } else { + if(ot == OPTypes.ANY || ot == OPTypes.TERM) { + ot = OPTypes.DOUBLE; + } + + optypes.push(ot); + expr.getLeftArg().visit(this); + builder.append(getSQLOperator(expr.getOperator())); + expr.getRightArg().visit(this); + optypes.pop(); + } + } + + @Override + public void meet(Max node) throws RuntimeException { + builder.append("MAX("); + optypes.push(OPTypes.DOUBLE); + node.getArg().visit(this); + optypes.pop(); + builder.append(")"); + } + + @Override + public void meet(Min node) throws RuntimeException { + builder.append("MIN("); + optypes.push(OPTypes.DOUBLE); + node.getArg().visit(this); + optypes.pop(); + builder.append(")"); + } + + @Override + public void meet(Regex re) throws RuntimeException { + builder.append(optimizeRegexp( + new ValueExpressionEvaluator(re.getArg(), parent, OPTypes.STRING).build(), + new ValueExpressionEvaluator(re.getPatternArg(), parent, OPTypes.STRING).build(), + re.getFlagsArg() + )); + } + + @Override + public void meet(SameTerm cmp) throws RuntimeException { + // covered by value binding in variables + optypes.push(OPTypes.TERM); + cmp.getLeftArg().visit(this); + builder.append(" = "); + cmp.getRightArg().visit(this); + optypes.pop(); + } + + @Override + public void meet(Str node) throws RuntimeException { + optypes.push(OPTypes.STRING); + node.getArg().visit(this); + optypes.pop(); + } + + @Override + public void meet(Sum node) throws RuntimeException { + builder.append("SUM("); + optypes.push(OPTypes.DOUBLE); + node.getArg().visit(this); + optypes.pop(); + builder.append(")"); + } + + @Override + public void meet(Var node) throws RuntimeException { + // distinguish between the case where the variable is plain and the variable is bound + SQLVariable sv = parent.getVariables().get(node.getName()); + + if(sv == null) { + builder.append("NULL"); + } else if(sv.getBindings().size() > 0) { + // in case the variable is actually an alias for an expression, we evaluate that expression instead, effectively replacing the + // variable occurrence with its value + sv.getBindings().get(0).visit(this); + } else { + String var = sv.getAlias(); + + if(sv.getProjectionType() != ProjectionType.NODE && sv.getProjectionType() != ProjectionType.NONE) { + // in case the variable represents a constructed or bound value instead of a node, we need to + // use the SQL expression as value; SQL should take care of proper casting... + // TODO: explicit casting needed? + builder.append(sv.getExpressions().get(0)); + } else { + // in case the variable represents an entry from the NODES table (i.e. has been bound to a node + // in the database, we take the NODES alias and resolve to the correct column according to the + // operator type + switch (optypes.peek()) { + case STRING: + Preconditions.checkState(var != null, "no alias available for variable"); + builder.append(var).append(".svalue"); + break; + case INT: + Preconditions.checkState(var != null, "no alias available for variable"); + builder.append(var).append(".ivalue"); + break; + case DOUBLE: + Preconditions.checkState(var != null, "no alias available for variable"); + builder.append(var).append(".dvalue"); + break; + case BOOL: + Preconditions.checkState(var != null, "no alias available for variable"); + builder.append(var).append(".bvalue"); + break; + case DATE: + Preconditions.checkState(var != null, "no alias available for variable"); + builder.append(var).append(".tvalue"); + break; + case VALUE: + Preconditions.checkState(var != null, "no alias available for variable"); + builder.append(var).append(".svalue"); + break; + case URI: + Preconditions.checkState(var != null, "no alias available for variable"); + builder.append(var).append(".svalue"); + break; + case TERM: + case ANY: + if(sv.getExpressions().size() > 0) { + // this allows us to avoid joins with the nodes table for simple expressions that only need the ID + builder.append(sv.getExpressions().get(0)); + } else { + Preconditions.checkState(var != null, "no alias available for variable"); + builder.append(var).append(".id"); + } + break; + } + } + } + } + + @Override + public void meet(ValueConstant node) throws RuntimeException { + String val = node.getValue().stringValue(); + + switch (optypes.peek()) { + case STRING: + case VALUE: + case URI: + builder.append("'").append(val).append("'"); + break; + case INT: + builder.append(Integer.parseInt(val)); + break; + case DOUBLE: + builder.append(Double.parseDouble(val)); + break; + case BOOL: + builder.append(Boolean.parseBoolean(val)); + break; + case DATE: + builder.append("'").append(sqlDateFormat.format(DateUtils.parseDate(val))).append("'"); + break; + + // in this case we should return a node ID and also need to make sure it actually exists + case TERM: + case ANY: + KiWiNode n = parent.getConverter().convert(node.getValue()); + builder.append(n.getId()); + break; + + default: throw new IllegalArgumentException("unsupported value type: " + optypes.peek()); + } + } + + private String getVariableAlias(Var var) { + return parent.getVariables().get(var.getName()).getAlias(); + } + + + private String getVariableAlias(String varName) { + return parent.getVariables().get(varName).getAlias(); + } + + /** + * Copy variables from the set to a new set suitable for a subquery; this allows passing over variable expressions + * from parent queries to subqueries without the subquery adding expressions that are then not visible outside + * @param variables + * @return + */ + private static Map<String, SQLVariable> copyVariables(Map<String, SQLVariable> variables) { + Map<String,SQLVariable> copy = new HashMap<>(); + try { + for(Map.Entry<String,SQLVariable> entry : variables.entrySet()) { + copy.put(entry.getKey(), (SQLVariable) entry.getValue().clone()); + } + } catch (CloneNotSupportedException e) { + log.error("could not clone SQL variable:",e); + } + + return copy; + } + + private String castExpression(String arg, OPTypes type) { + if(type == null) { + return arg; + } + + switch (type) { + case DOUBLE: + return functionRegistry.get(XMLSchema.DOUBLE).getNative(parent.getDialect(), arg); + case INT: + return functionRegistry.get(XMLSchema.INTEGER).getNative(parent.getDialect(), arg); + case BOOL: + return functionRegistry.get(XMLSchema.BOOLEAN).getNative(parent.getDialect(), arg); + case DATE: + return functionRegistry.get(XMLSchema.DATETIME).getNative(parent.getDialect(), arg); + case VALUE: + return arg; + case ANY: + return arg; + default: + return arg; + } + } + + private static String getSQLOperator(Compare.CompareOp op) { + switch (op) { + case EQ: return " = "; + case GE: return " >= "; + case GT: return " > "; + case LE: return " <= "; + case LT: return " < "; + case NE: return " <> "; + } + throw new IllegalArgumentException("unsupported operator type for comparison: "+op); + } + + + private static String getSQLOperator(MathExpr.MathOp op) { + switch (op) { + case PLUS: return " + "; + case MINUS: return " - "; + case DIVIDE: return " / "; + case MULTIPLY: return " / "; + } + throw new IllegalArgumentException("unsupported operator type for math expression: "+op); + } + + /** + * Test if the regular expression given in the pattern can be simplified to a LIKE SQL statement; these are + * considerably more efficient to evaluate in most databases, so in case we can simplify, we return a LIKE. + * + * @param value + * @param pattern + * @return + */ + private String optimizeRegexp(String value, String pattern, ValueExpr flags) { + String _flags = flags != null && flags instanceof ValueConstant ? ((ValueConstant)flags).getValue().stringValue() : null; + + String simplified = pattern; + + // apply simplifications + + // remove SQL quotes at beginning and end + simplified = simplified.replaceFirst("^'",""); + simplified = simplified.replaceFirst("'$",""); + + + // remove .* at beginning and end, they are the default anyways + simplified = simplified.replaceFirst("^\\.\\*",""); + simplified = simplified.replaceFirst("\\.\\*$",""); + + // replace all occurrences of % with \% and _ with \_, as they are special characters in SQL + simplified = simplified.replaceAll("%","\\%"); + simplified = simplified.replaceAll("_","\\_"); + + // if pattern now does not start with a ^, we put a "%" in front + if(!simplified.startsWith("^")) { + simplified = "%" + simplified; + } else { + simplified = simplified.substring(1); + } + + // if pattern does not end with a "$", we put a "%" at the end + if(!simplified.endsWith("$")) { + simplified = simplified + "%"; + } else { + simplified = simplified.substring(0,simplified.length()-1); + } + + // replace all non-escaped occurrences of .* with % + simplified = simplified.replaceAll("(?<!\\\\)\\.\\*","%"); + + // replace all non-escaped occurrences of .+ with _% + simplified = simplified.replaceAll("(?<!\\\\)\\.\\+","_%"); + + // the pattern is not simplifiable if the simplification still contains unescaped regular expression constructs + Pattern notSimplifiable = Pattern.compile("(?<!\\\\)[\\.\\*\\+\\{\\}\\[\\]\\|]"); + + if(notSimplifiable.matcher(simplified).find()) { + return parent.getDialect().getRegexp(value, pattern, _flags); + } else { + if(!simplified.startsWith("%") && !simplified.endsWith("%")) { + if(StringUtils.containsIgnoreCase(_flags, "i")) { + return String.format("lower(%s) = lower('%s')", value, simplified); + } else { + return String.format("%s = '%s'", value, simplified); + } + } else { + if(StringUtils.containsIgnoreCase(_flags,"i")) { + return parent.getDialect().getILike(value, "'" + simplified + "'"); + } else { + return value + " LIKE '"+simplified+"'"; + } + } + } + + } + +} http://git-wip-us.apache.org/repos/asf/marmotta/blob/60867dac/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/evaluation/KiWiEvaluationStrategyImpl.java ---------------------------------------------------------------------- diff --git a/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/evaluation/KiWiEvaluationStrategyImpl.java b/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/evaluation/KiWiEvaluationStrategyImpl.java index b21b2ab..7e6d7bb 100644 --- a/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/evaluation/KiWiEvaluationStrategyImpl.java +++ b/libraries/kiwi/kiwi-sparql/src/main/java/org/apache/marmotta/kiwi/sparql/evaluation/KiWiEvaluationStrategyImpl.java @@ -19,7 +19,7 @@ package org.apache.marmotta.kiwi.sparql.evaluation; import info.aduna.iteration.CloseableIteration; import info.aduna.iteration.ExceptionConvertingIteration; -import org.apache.marmotta.kiwi.sparql.function.NativeFunctionRegistry; +import org.apache.marmotta.kiwi.sparql.builder.collect.SupportedFinder; import org.apache.marmotta.kiwi.sparql.persistence.KiWiSparqlConnection; import org.openrdf.query.BindingSet; import org.openrdf.query.Dataset; @@ -53,48 +53,6 @@ public class KiWiEvaluationStrategyImpl extends EvaluationStrategyImpl{ private static Logger log = LoggerFactory.getLogger(KiWiEvaluationStrategyImpl.class); - // TODO: supported features should be checked based on this Set - private static Set<Class> supportedConstructs = new HashSet<>(); - static { - supportedConstructs.add(Join.class); - supportedConstructs.add(LeftJoin.class); - supportedConstructs.add(Filter.class); - supportedConstructs.add(Extension.class); - supportedConstructs.add(StatementPattern.class); - supportedConstructs.add(Slice.class); - supportedConstructs.add(Reduced.class); - supportedConstructs.add(Distinct.class); - supportedConstructs.add(Union.class); - supportedConstructs.add(Projection.class); // subquery only - supportedConstructs.add(Order.class); - supportedConstructs.add(Group.class); - - supportedConstructs.add(Coalesce.class); - supportedConstructs.add(Count.class); - supportedConstructs.add(Avg.class); - supportedConstructs.add(Min.class); - supportedConstructs.add(Max.class); - supportedConstructs.add(Sum.class); - supportedConstructs.add(Compare.class); - supportedConstructs.add(MathExpr.class); - supportedConstructs.add(And.class); - supportedConstructs.add(Or.class); - supportedConstructs.add(Not.class); - supportedConstructs.add(Var.class); - supportedConstructs.add(Str.class); - supportedConstructs.add(Label.class); - supportedConstructs.add(BNodeGenerator.class); - supportedConstructs.add(IRIFunction.class); - supportedConstructs.add(IsResource.class); - supportedConstructs.add(IsURI.class); - supportedConstructs.add(IsBNode.class); - supportedConstructs.add(IsLiteral.class); - supportedConstructs.add(Lang.class); - supportedConstructs.add(LangMatches.class); - supportedConstructs.add(Regex.class); - supportedConstructs.add(FunctionCall.class); // need to check for supported functions - } - /** * The database connection offering specific SPARQL-SQL optimizations. @@ -234,157 +192,11 @@ public class KiWiEvaluationStrategyImpl extends EvaluationStrategyImpl{ /** * Test if a tuple expression is supported nby the optimized evaluation; in this case we can apply a specific optimization. * - * TODO: implement as visitor - * * @param expr * @return */ private boolean isSupported(TupleExpr expr) { - if(expr instanceof Join) { - return isSupported(((Join) expr).getLeftArg()) && isSupported(((Join) expr).getRightArg()); - } else if(expr instanceof LeftJoin) { - return isSupported(((LeftJoin) expr).getLeftArg()) && isSupported(((LeftJoin) expr).getRightArg()) && isSupported(((LeftJoin)expr).getCondition()); - } else if(expr instanceof Filter) { - return isSupported(((Filter) expr).getArg()) && isSupported(((Filter) expr).getCondition()); - } else if(expr instanceof Extension) { - for(ExtensionElem elem : ((Extension) expr).getElements()) { - if(!isSupported(elem.getExpr())) { - return false; - } - } - return isSupported(((Extension) expr).getArg()); - } else if(expr instanceof StatementPattern) { - return true; - } else if(expr instanceof Slice) { - return isSupported(((Slice) expr).getArg()); - } else if(expr instanceof Reduced) { - return isSupported(((Reduced) expr).getArg()); - } else if(expr instanceof Distinct) { - return isSupported(((Distinct) expr).getArg()); - } else if(expr instanceof Union) { - return isSupported(((Union) expr).getLeftArg()) && isSupported(((Union)expr).getRightArg()); - } else if(expr instanceof Projection) { - return isSupported(((Projection) expr).getArg()); - } else if(expr instanceof Order) { - for(OrderElem elem : ((Order) expr).getElements()) { - if(!isSupported(elem.getExpr())) { - return false; - } - } - return isSupported(((Order) expr).getArg()); - } else if(expr instanceof Group) { - for(GroupElem elem : ((Group) expr).getGroupElements()) { - if(!isSupported(elem.getOperator())) { - return false; - } - } - return isSupported(((Group) expr).getArg()); - } else if(expr instanceof SingletonSet) { - return true; - } else { - return false; - } - } - - /** - * Test if the value expression construct and all its subexpressions are supported by the optimized evaluation - * strategy. Returns true if yes, false otherwise. - * - * TODO: implement as visitor - * - * @param expr - * @return - */ - private boolean isSupported(ValueExpr expr) { - if(expr == null) { - return true; - } else if(expr instanceof Coalesce) { - for(ValueExpr e : ((Coalesce) expr).getArguments()) { - if(!isSupported(e)) { - return false; - } - } - return true; - } else if(expr instanceof Count) { - if(((Count) expr).getArg() == null) { - return connection.getDialect().isArraySupported(); - } else { - return isSupported(((Count) expr).getArg()); - } - } else if(expr instanceof Avg) { - return isSupported(((Avg) expr).getArg()); - } else if(expr instanceof Min) { - return isSupported(((Min) expr).getArg()); - } else if(expr instanceof Max) { - return isSupported(((Max) expr).getArg()); - } else if(expr instanceof Sum) { - return isSupported(((Sum) expr).getArg()); - } else if(expr instanceof Compare) { - return isSupported(((Compare) expr).getLeftArg()) && isSupported(((Compare) expr).getRightArg()); - } else if(expr instanceof SameTerm) { - return isSupported(((SameTerm) expr).getLeftArg()) && isSupported(((SameTerm) expr).getRightArg()); - } else if(expr instanceof MathExpr) { - return isSupported(((MathExpr) expr).getLeftArg()) && isSupported(((MathExpr) expr).getRightArg()); - } else if(expr instanceof And) { - return isSupported(((And) expr).getLeftArg()) && isSupported(((And) expr).getRightArg()); - } else if(expr instanceof Or) { - return isSupported(((Or) expr).getLeftArg()) && isSupported(((Or) expr).getRightArg()); - } else if(expr instanceof Not) { - return isSupported(((Not) expr).getArg()); - } else if(expr instanceof Exists) { - return isSupported(((Exists) expr).getSubQuery()); - } else if(expr instanceof ValueConstant) { - return true; - } else if(expr instanceof Var) { - return true; - } else if(expr instanceof Str) { - return isAtomic(((Str) expr).getArg()); - } else if(expr instanceof Label) { - return isAtomic(((UnaryValueOperator) expr).getArg()); - } else if(expr instanceof BNodeGenerator) { - if(((BNodeGenerator) expr).getNodeIdExpr() != null) { - return isAtomic(((BNodeGenerator) expr).getNodeIdExpr()); - } else { - return true; - } - } else if(expr instanceof IRIFunction) { - return isAtomic(((UnaryValueOperator) expr).getArg()); - } else if(expr instanceof Bound) { - return true; - } else if(expr instanceof IsResource) { - return isAtomic(((UnaryValueOperator) expr).getArg()); - } else if(expr instanceof IsURI) { - return isAtomic(((UnaryValueOperator) expr).getArg()); - } else if(expr instanceof IsBNode) { - return isAtomic(((UnaryValueOperator) expr).getArg()); - } else if(expr instanceof IsLiteral) { - return isAtomic(((UnaryValueOperator) expr).getArg()); - } else if(expr instanceof Lang) { - return isAtomic(((Lang) expr).getArg()); - } else if(expr instanceof LangMatches) { - return isSupported(((LangMatches) expr).getLeftArg()) && isConstant(((LangMatches) expr).getRightArg()); - } else if(expr instanceof Regex) { - ValueExpr flags = ((Regex) expr).getFlagsArg(); - String _flags = flags != null && flags instanceof ValueConstant ? ((ValueConstant)flags).getValue().stringValue() : null; - return isSupported(((Regex) expr).getArg()) && isAtomic(((Regex) expr).getPatternArg()) && connection.getDialect().isRegexpSupported(_flags); - } else if(expr instanceof FunctionCall) { - return isFunctionSupported((FunctionCall)expr); - } else { - return false; - } - } - - private boolean isFunctionSupported(FunctionCall fc) { - return NativeFunctionRegistry.getInstance().get(fc.getURI()) != null && NativeFunctionRegistry.getInstance().get(fc.getURI()).isSupported(connection.getDialect()); - } - - - private static boolean isAtomic(ValueExpr expr) { - return expr instanceof Var || expr instanceof ValueConstant; - } - - private static boolean isConstant(ValueExpr expr) { - return expr instanceof ValueConstant; + return new SupportedFinder(expr, connection.getDialect()).isSupported(); } } http://git-wip-us.apache.org/repos/asf/marmotta/blob/60867dac/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/KiWiDialect.java ---------------------------------------------------------------------- diff --git a/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/KiWiDialect.java b/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/KiWiDialect.java index b87972a..d54df6d 100644 --- a/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/KiWiDialect.java +++ b/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/KiWiDialect.java @@ -199,6 +199,11 @@ public abstract class KiWiDialect { public abstract String getILike(String text, String pattern); + /** + * Return the name of the aggregate function for group concatenation (string_agg in postgres, GROUP_CONCAT in MySQL) + * @return + */ + public abstract String getGroupConcat(String value, String separator, boolean distinct); /** http://git-wip-us.apache.org/repos/asf/marmotta/blob/60867dac/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/h2/H2Dialect.java ---------------------------------------------------------------------- diff --git a/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/h2/H2Dialect.java b/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/h2/H2Dialect.java index fdbbc63..05913b2 100644 --- a/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/h2/H2Dialect.java +++ b/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/h2/H2Dialect.java @@ -86,6 +86,24 @@ public class H2Dialect extends KiWiDialect { return "lower("+text+") LIKE lower("+pattern+")"; } + /** + * Return the name of the aggregate function for group concatenation (string_agg in postgres, GROUP_CONCAT in MySQL) + * + * @param value + * @param separator + * @return + */ + @Override + public String getGroupConcat(String value, String separator, boolean distinct) { + if(distinct) { + value = "DISTINCT " + value; + } + if(separator != null) { + return String.format("GROUP_CONCAT(%s SEPARATOR %s)", value, separator); + } else { + return String.format("GROUP_CONCAT(%s)", value); + } + } /** http://git-wip-us.apache.org/repos/asf/marmotta/blob/60867dac/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/mysql/MySQLDialect.java ---------------------------------------------------------------------- diff --git a/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/mysql/MySQLDialect.java b/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/mysql/MySQLDialect.java index 0a24b75..82ed3f6 100644 --- a/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/mysql/MySQLDialect.java +++ b/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/mysql/MySQLDialect.java @@ -99,6 +99,24 @@ public class MySQLDialect extends KiWiDialect { } + /** + * Return the name of the aggregate function for group concatenation (string_agg in postgres, GROUP_CONCAT in MySQL) + * + * @param value + * @param separator + * @return + */ + @Override + public String getGroupConcat(String value, String separator, boolean distinct) { + if(distinct) { + value = "DISTINCT " + value; + } + if(separator != null) { + return String.format("GROUP_CONCAT(%s SEPARATOR %s)", value, separator); + } else { + return String.format("GROUP_CONCAT(%s)", value); + } + } /** * Get the query string that can be used for validating that a JDBC connection to this database is still valid. http://git-wip-us.apache.org/repos/asf/marmotta/blob/60867dac/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/pgsql/PostgreSQLDialect.java ---------------------------------------------------------------------- diff --git a/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/pgsql/PostgreSQLDialect.java b/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/pgsql/PostgreSQLDialect.java index bc369d1..28fa98e 100644 --- a/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/pgsql/PostgreSQLDialect.java +++ b/libraries/kiwi/kiwi-triplestore/src/main/java/org/apache/marmotta/kiwi/persistence/pgsql/PostgreSQLDialect.java @@ -90,6 +90,24 @@ public class PostgreSQLDialect extends KiWiDialect { return text + " ILIKE " + pattern; } + /** + * Return the name of the aggregate function for group concatenation (string_agg in postgres, GROUP_CONCAT in MySQL) + * + * @param value + * @param separator + * @return + */ + @Override + public String getGroupConcat(String value, String separator, boolean distinct) { + if(distinct) { + value = "DISTINCT " + value; + } + if(separator != null) { + return String.format("string_agg(%s, %s)", value, separator); + } else { + return String.format("string_agg(%s, '')", value); + } + } /** * Get the query string that can be used for validating that a JDBC connection to this database is still valid.
