| /* |
| * 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; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Preconditions; |
| 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.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; |
| import org.apache.marmotta.kiwi.sparql.builder.model.SQLVariable; |
| import org.apache.marmotta.kiwi.sparql.exception.UnsatisfiableQueryException; |
| import org.apache.marmotta.kiwi.sparql.function.NativeFunctionRegistry; |
| import org.openrdf.model.Resource; |
| import org.openrdf.model.URI; |
| import org.openrdf.model.Value; |
| import org.openrdf.model.vocabulary.SESAME; |
| import org.openrdf.query.BindingSet; |
| import org.openrdf.query.Dataset; |
| import org.openrdf.query.algebra.*; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.util.*; |
| |
| /** |
| * A builder for translating SPARQL queries into SQL. |
| * |
| * @author Sebastian Schaffert (sschaffert@apache.org) |
| */ |
| public class SQLBuilder { |
| |
| private static Logger log = LoggerFactory.getLogger(SQLBuilder.class); |
| |
| /** |
| * Simplify access to different node ids |
| */ |
| private static final String[] positions = new String[] {"subject","predicate","object","context"}; |
| |
| |
| /** |
| * Reference to the registry of natively supported functions with parameter and return types as well as SQL translation |
| */ |
| private NativeFunctionRegistry functionRegistry = NativeFunctionRegistry.getInstance(); |
| |
| /** |
| * Query results should be DISTINCT. |
| */ |
| private boolean distinct = false; |
| |
| /** |
| * Result offset, translated into an OFFSET expression; ignored if <0 |
| */ |
| private long offset = -1; |
| |
| /** |
| * Result limit, translated into a LIMIT expression; ignored if <= 0 |
| */ |
| private long limit = -1; |
| |
| private List<OrderElem> orderby; |
| private List<ExtensionElem> extensions; |
| |
| private Set<String> groupLabels; |
| |
| // list of SPARQL variable names that are actually contained in the projection |
| private Set<String> projectedVars; |
| |
| /** |
| * Maintains a mapping between SPARQL variable names and internal variable descriptions. The internal description |
| * contains information whether the variable needs to be projected, what SQL expressions represent this variable, |
| * and what internal aliases to use. |
| */ |
| private Map<String,SQLVariable> variables; |
| protected void addVariable(SQLVariable v) { |
| variables.put(v.getSparqlName(),v); |
| } |
| |
| |
| /** |
| * The SQL fragments and triple patterns collected from the query. SQLFragments are LEFT JOINed with each other, |
| * so for each OPTIONAL found in the query, a new fragment is appended here. Inside a fragment, the patterns |
| * are CROSS JOINed with each other. |
| * |
| * @see org.apache.marmotta.kiwi.sparql.builder.collect.PatternCollector |
| */ |
| private List<SQLFragment> fragments; |
| |
| /** |
| * Contains the names those variables for which the value is needed instead of the ID, because the value is used |
| * somewhere in a condition, function or other kind of value expression. |
| * |
| * @see org.apache.marmotta.kiwi.sparql.builder.collect.ConditionFinder |
| */ |
| private Set<String> resolveVariables; |
| |
| private TupleExpr query; |
| |
| private BindingSet bindings; |
| |
| private Dataset dataset; |
| |
| private ValueConverter converter; |
| |
| private KiWiDialect dialect; |
| |
| // a prefix for naming table aliases (needed in case this is a subquery) |
| private String prefix; |
| |
| /** |
| * Create a new SQLBuilder for the given query, initial bindings, dataset, and |
| * @param query |
| * @param bindings |
| * @param dataset |
| */ |
| public SQLBuilder(TupleExpr query, BindingSet bindings, Dataset dataset, final KiWiValueFactory valueFactory, KiWiDialect dialect, Set<String> projectedVars) throws UnsatisfiableQueryException { |
| this(query, bindings, dataset, new ValueConverter() { |
| @Override |
| public KiWiNode convert(Value value) { |
| return valueFactory.convert(value); |
| } |
| }, dialect, projectedVars); |
| } |
| |
| |
| public SQLBuilder(TupleExpr query, BindingSet bindings, Dataset dataset, ValueConverter converter, KiWiDialect dialect, Set<String> projectedVars) throws UnsatisfiableQueryException { |
| this(query,bindings, dataset, converter, dialect, "", projectedVars, new HashMap<String, SQLVariable>()); |
| } |
| |
| /** |
| * Create a new SQLBuilder for the given query, initial bindings, dataset, and |
| * @param query |
| * @param bindings |
| * @param dataset |
| */ |
| public SQLBuilder(TupleExpr query, BindingSet bindings, Dataset dataset, ValueConverter converter, KiWiDialect dialect, String prefix, Set<String> projectedVars, Map<String,SQLVariable> variables) throws UnsatisfiableQueryException { |
| this.query = query; |
| this.bindings = bindings; |
| this.dataset = dataset; |
| this.converter = converter; |
| this.dialect = dialect; |
| this.projectedVars = projectedVars; |
| this.prefix = prefix; |
| this.variables = variables; |
| |
| prepareBuilder(); |
| } |
| |
| |
| public Map<String, SQLVariable> getVariables() { |
| return variables; |
| } |
| |
| public ValueConverter getConverter() { |
| return converter; |
| } |
| |
| public KiWiDialect getDialect() { |
| return dialect; |
| } |
| |
| public Dataset getDataset() { |
| return dataset; |
| } |
| |
| public BindingSet getBindings() { |
| return bindings; |
| } |
| |
| public Set<String> getProjectedVars() { |
| return projectedVars; |
| } |
| |
| private void prepareBuilder() throws UnsatisfiableQueryException { |
| Preconditions.checkArgument(query instanceof Projection || query instanceof Union || query instanceof Extension || query instanceof Order || query instanceof Group || query instanceof LeftJoin ||query instanceof Join || query instanceof Filter || query instanceof StatementPattern || query instanceof Distinct || query instanceof Slice || query instanceof Reduced); |
| |
| |
| // collect all patterns in a list, using depth-first search over the join |
| PatternCollector pc = new PatternCollector(query, bindings, dataset, converter, dialect, projectedVars, prefix); |
| |
| fragments = pc.parts; |
| |
| // collect offset and limit from the query if given |
| offset = new LimitFinder(query).offset; |
| limit = new LimitFinder(query).limit; |
| |
| // check if query is distinct |
| distinct = new DistinctFinder(query).distinct; |
| |
| // find the ordering |
| orderby = new OrderFinder(query).elements; |
| |
| // find the grouping |
| GroupFinder gf = new GroupFinder(query); |
| groupLabels = gf.bindings; |
| |
| // find extensions (BIND) |
| extensions = new ExtensionFinder(query).elements; |
| |
| // find variables that need to be resolved |
| resolveVariables = new ConditionFinder(query).neededVariables; |
| |
| int variableCount = 0; |
| |
| // find all variables that have been bound already, even if they do not appear in a pattern |
| for(Var v : new VariableFinder(query).variables) { |
| if(v.hasValue() && !v.getName().startsWith("-const")) { |
| SQLVariable sv = variables.get(v.getName()); |
| if(sv == null) { |
| sv = new SQLVariable("V" + (++variableCount), v.getName()); |
| |
| // select those variables that are really projected and not only needed in a grouping construct |
| if(projectedVars.contains(sv.getSparqlName()) || new SQLProjectionFinder(query,v.getName()).found) { |
| sv.setProjectionType(ValueType.NODE); |
| } |
| |
| sv.getExpressions().add(""+ converter.convert(v.getValue()).getId()); |
| |
| addVariable(sv); |
| } |
| } |
| } |
| |
| // find all variables occurring in the patterns and create a map to map them to |
| // field names in the database query; each variable will have one or several field names, |
| // one for each pattern it occurs in; field names are constructed automatically by a counter |
| // and the pattern name to ensure the name is a valid HQL identifier |
| for(SQLFragment f : fragments) { |
| for (SQLPattern p : f.getPatterns()) { |
| // build pattern |
| Var[] fields = p.getFields(); |
| for (int i = 0; i < fields.length; i++) { |
| if (fields[i] != null && (!fields[i].hasValue() || !fields[i].getName().startsWith("-const"))) { |
| Var v = fields[i]; |
| |
| SQLVariable sv = variables.get(v.getName()); |
| if(sv == null) { |
| sv = new SQLVariable("V" + (++variableCount), v.getName()); |
| |
| // select those variables that are really projected and not only needed in a grouping construct |
| if(projectedVars.contains(sv.getSparqlName()) || new SQLProjectionFinder(query,v.getName()).found) { |
| sv.setProjectionType(ValueType.NODE); |
| } |
| |
| String pName = p.getName(); |
| String vName = sv.getName(); |
| |
| if (sv.getAlias() == null && resolveVariables.contains(v.getName())) { |
| sv.setAlias(pName + "_" + positions[i] + "_" + vName); |
| } |
| |
| addVariable(sv); |
| } |
| } |
| } |
| } |
| |
| // subqueries: look up which variables are bound in the subqueries and add proper aliases |
| for(SQLAbstractSubquery sq : f.getSubqueries()) { |
| for(SQLVariable sq_v : sq.getQueryVariables()) { |
| SQLVariable sv = variables.get(sq_v.getSparqlName()); |
| |
| if(sv == null) { |
| sv = new SQLVariable("V" + (++variableCount), sq_v.getSparqlName()); |
| |
| // select those variables that are really projected and not only needed in a grouping construct |
| if(projectedVars.contains(sv.getSparqlName()) || new SQLProjectionFinder(query,sq_v.getSparqlName()).found) { |
| sv.setProjectionType(sq_v.getProjectionType()); |
| } |
| |
| String sqName = sq.getAlias(); |
| String vName = sv.getName(); |
| |
| if (sv.getAlias() == null && resolveVariables.contains(sq_v.getSparqlName())) { |
| sv.setAlias(sqName + "_" + vName); |
| } |
| |
| addVariable(sv); |
| } |
| |
| } |
| } |
| } |
| |
| |
| // add all extensions to the variable list so they are properly considered in projections and clauses |
| // TODO: order by variable dependency, or otherwise the evaluateExpression might fail |
| List<ExtensionElem> deferredExtensions = new ArrayList<>(); // some extension might need to be evaluated later because their expressions cannot be computed yet |
| for(ExtensionElem ext : extensions) { |
| Var v = new Var(ext.getName()); |
| |
| SQLVariable sv = variables.get(v.getName()); |
| if(!variables.containsKey(v.getName())) { |
| sv = new SQLVariable("V" + (++variableCount), v.getName()); |
| |
| // select those variables that are really projected and not only needed in a grouping construct |
| if(projectedVars.contains(sv.getSparqlName()) || new SQLProjectionFinder(query,v.getName()).found) { |
| sv.setProjectionType(getProjectionType(ext.getExpr())); |
| } |
| |
| // Functions that return a string literal do so with the string literal of the same kind as the first |
| // argument (simple literal, plain literal with same language tag, xsd:string). |
| ValueType type = getProjectionType(ext.getExpr()); |
| if(type == ValueType.STRING) { |
| sv.setLiteralTypeExpression(getLiteralTypeExpression(ext.getExpr())); |
| sv.setLiteralLangExpression(getLiteralLangExpression(ext.getExpr())); |
| // TODO: the following will produce invalid results for aggregation functions |
| /* |
| } else if(type == ProjectionType.INT || type == ProjectionType.DOUBLE || type == ProjectionType.BOOL) { |
| sv.setLiteralTypeExpression(getLiteralTypeExpression(ext.getExpr())); |
| */ |
| } |
| |
| addVariable(sv); |
| } |
| |
| // TODO: ANY as OPType here is dangerous, because the OPType should depends on projection and actual use |
| // of variables in conditions etc |
| if (resolveVariables.contains(v.getName())) { |
| //sv.getAliases().add(evaluateExpression(ext.getExpr(), OPTypes.VALUE)); |
| sv.getBindings().add(ext.getExpr()); |
| } |
| |
| try { |
| sv.getExpressions().add(evaluateExpression(ext.getExpr(), ValueType.NODE)); |
| if(sv.getProjectionType() == ValueType.NODE && getProjectionType(ext.getExpr()) != ValueType.NODE) { |
| sv.setProjectionType(getProjectionType(ext.getExpr())); |
| } |
| } catch(IllegalStateException ex) { |
| deferredExtensions.add(ext); |
| } |
| |
| } |
| |
| |
| // calculate for each variable the SQL expressions representing them and any necessary JOIN conditions |
| for (SQLFragment f : fragments) { |
| for (SQLPattern p : f.getPatterns()) { |
| // build pattern |
| Var[] fields = p.getFields(); |
| for (int i = 0; i < fields.length; i++) { |
| if (fields[i] != null && (!fields[i].hasValue() || !fields[i].getName().startsWith("-const"))) { |
| Var v = fields[i]; |
| |
| SQLVariable sv = variables.get(v.getName()); |
| |
| String pName = p.getName(); |
| |
| // if the variable has been used before, add a join condition to the first occurrence |
| if(sv.getExpressions().size() > 0) { |
| // case distinction: is this variable projected as node or as another value in an extension? |
| // if it is a value, we need to refer to the corresponding typed column of the node, otherwise |
| // to the node ID (field ID is sufficient) |
| switch (sv.getProjectionType()) { |
| case INT: |
| p.getConditions().add(sv.getExpressions().get(0) + " = " + sv.getAlias() + ".ivalue"); |
| break; |
| case DECIMAL: |
| case DOUBLE: |
| p.getConditions().add(sv.getExpressions().get(0) + " = " + sv.getAlias() + ".dvalue"); |
| break; |
| case DATE: |
| case TZDATE: |
| p.getConditions().add(sv.getExpressions().get(0) + " = " + sv.getAlias() + ".tvalue"); |
| break; |
| case BOOL: |
| p.getConditions().add(sv.getExpressions().get(0) + " = " + sv.getAlias() + ".bvalue"); |
| break; |
| case URI: |
| case STRING: |
| p.getConditions().add(sv.getExpressions().get(0) + " = " + sv.getAlias() + ".svalue"); |
| break; |
| |
| default: |
| p.getConditions().add(sv.getExpressions().get(0) + " = " + pName + "." + positions[i]); |
| break; |
| } |
| } |
| |
| sv.getExpressions().add(pName + "." + positions[i]); |
| } |
| } |
| } |
| |
| // subqueries: look up which variables are bound in the subqueries and add proper aliases |
| for (SQLAbstractSubquery sq : f.getSubqueries()) { |
| for (SQLVariable sq_v : sq.getQueryVariables()) { |
| SQLVariable sv = variables.get(sq_v.getSparqlName()); |
| |
| String sqName = sq.getAlias(); |
| |
| // if the variable has been used before, add a join condition to the first occurrence |
| if(sv.getExpressions().size() > 0) { |
| sq.getConditions().add(sv.getExpressions().get(0) + " = " + sqName + "." + sq_v.getName()); |
| } |
| |
| sv.getExpressions().add(sqName + "." + sq_v.getName()); |
| |
| } |
| } |
| } |
| |
| for(ExtensionElem ext : deferredExtensions) { |
| Var v = new Var(ext.getName()); |
| |
| SQLVariable sv = variables.get(v.getName()); |
| sv.getExpressions().add(evaluateExpression(ext.getExpr(), ValueType.NODE)); |
| } |
| |
| // find context restrictions of patterns and match them with potential restrictions given in the |
| // dataset (MARMOTTA-340) |
| for(SQLFragment f : fragments) { |
| for (SQLPattern p : f.getPatterns()) { |
| Resource[] contexts; |
| Value contextValue = p.getSparqlPattern().getContextVar() != null ? p.getSparqlPattern().getContextVar().getValue() : null; |
| |
| Set<URI> graphs = null; |
| boolean emptyGraph = false; |
| |
| if (dataset != null) { |
| if (p.getSparqlPattern().getScope() == StatementPattern.Scope.DEFAULT_CONTEXTS) { |
| graphs = dataset.getDefaultGraphs(); |
| emptyGraph = graphs.isEmpty() && !dataset.getNamedGraphs().isEmpty(); |
| } else { |
| graphs = dataset.getNamedGraphs(); |
| emptyGraph = graphs.isEmpty() && !dataset.getDefaultGraphs().isEmpty(); |
| } |
| } |
| |
| // set the contexts to query according to the following rules: |
| // 1. if the context defined in the dataset does not exist, there will be no result, so set "empty" to true |
| // 2. if no context graphs have been given, use the context from the statement |
| // 3. if context graphs have been given and the statement has a context, check if the statement context is |
| // contained in the context graphs; if no, set "empty" to true as there can be no result |
| // 4. if context graphs have been given and the statement has no context, use the contexts from the |
| // dataset |
| |
| if (emptyGraph) { |
| // Search zero contexts |
| throw new UnsatisfiableQueryException("dataset does not contain any default graphs"); |
| } else if (graphs == null || graphs.isEmpty()) { |
| if (contextValue != null) { |
| contexts = new Resource[]{(Resource) contextValue}; |
| } else { |
| contexts = new Resource[0]; |
| } |
| } else if (contextValue != null) { |
| if (graphs.contains(contextValue)) { |
| contexts = new Resource[]{(Resource) contextValue}; |
| } else { |
| // Statement pattern specifies a context that is not part of |
| // the dataset |
| throw new UnsatisfiableQueryException("default graph does not contain statement context '" + contextValue.stringValue() + "'"); |
| } |
| } else { |
| contexts = new Resource[graphs.size()]; |
| int i = 0; |
| for (URI graph : graphs) { |
| URI context = null; |
| if (!SESAME.NIL.equals(graph)) { |
| context = graph; |
| } |
| contexts[i++] = context; |
| } |
| } |
| |
| |
| // build an OR query for the value of the context variable |
| if (contexts.length > 0) { |
| p.setVariableContexts(Arrays.asList(contexts)); |
| } |
| } |
| } |
| |
| prepareConditions(); |
| } |
| |
| |
| |
| private void prepareConditions() throws UnsatisfiableQueryException { |
| // build the where clause as follows: |
| // 1. iterate over all patterns and for each resource and literal field in subject, |
| // property, object, or context, and set a query condition according to the |
| // nodes given in the pattern |
| // 2. for each variable that has more than one occurrences, add a join condition |
| // 3. for each variable in the initialBindings, add a condition to the where clause |
| |
| |
| // iterate over all fragments and add translate the filter conditions into SQL |
| for(SQLFragment f : fragments) { |
| for(ValueExpr e : f.getFilters()) { |
| f.getConditions().add(evaluateExpression(e, ValueType.NODE)); |
| } |
| } |
| |
| |
| // 1. iterate over all patterns and for each resource and literal field in subject, |
| // property, object, or context, and set a query condition according to the |
| // nodes given in the pattern |
| for(SQLFragment f : fragments) { |
| for (SQLPattern p : f.getPatterns()) { |
| String pName = p.getName(); |
| Var[] fields = p.getFields(); |
| for (int i = 0; i < fields.length; i++) { |
| // find node id of the resource or literal field and use it in the where clause |
| // in this way we can avoid setting too many query parameters |
| long nodeId = -1; |
| if (fields[i] != null && fields[i].hasValue()) { |
| Value v = converter.convert(fields[i].getValue()); |
| if (v instanceof KiWiNode) { |
| nodeId = ((KiWiNode) v).getId(); |
| } else { |
| throw new UnsatisfiableQueryException("the values in this query have not been created by the KiWi value factory"); |
| } |
| |
| if (nodeId >= 0) { |
| String condition = pName + "." + positions[i] + " = " + nodeId; |
| p.getConditions().add(condition); |
| } |
| } |
| } |
| } |
| } |
| |
| |
| // 6. for each context variable with a restricted list of contexts, we add a condition to the where clause |
| // of the form (V.id = R1.id OR V.id = R2.id ...) |
| for(SQLFragment f : fragments) { |
| for (SQLPattern p : f.getPatterns()) { |
| // the variable |
| String varName = p.getName(); |
| |
| if (p.getVariableContexts() != null) { |
| // the string we are building |
| StringBuilder cCond = new StringBuilder(); |
| cCond.append("("); |
| for (Iterator<Resource> it = p.getVariableContexts().iterator(); it.hasNext(); ) { |
| Value v = converter.convert(it.next()); |
| if (v instanceof KiWiNode) { |
| long nodeId = ((KiWiNode) v).getId(); |
| |
| cCond.append(varName).append(".context = ").append(nodeId); |
| |
| if (it.hasNext()) { |
| cCond.append(" OR "); |
| } |
| } else { |
| throw new UnsatisfiableQueryException("the values in this query have not been created by the KiWi value factory"); |
| } |
| |
| } |
| cCond.append(")"); |
| p.getConditions().add(cCond.toString()); |
| } |
| } |
| } |
| |
| // for each pattern, update the joinFields by checking if subject, predicate, object or context are involved |
| // in any filters or functions; in this case, the corresponding field needs to be joined with the NODES table |
| // and we need to mark the pattern accordingly. |
| boolean first = true; |
| Set<String> joined = new HashSet<>(); |
| for(SQLFragment f : fragments) { |
| if(first && f.getConditionPosition() == SQLFragment.ConditionPosition.JOIN) { |
| // the conditions of the first fragment need to be placed in the WHERE part of the query, because |
| // there is not necessarily a JOIN ... ON where we can put it |
| f.setConditionPosition(SQLFragment.ConditionPosition.WHERE); |
| } |
| first = false; |
| |
| for (SQLPattern p : f.getPatterns()) { |
| for(Map.Entry<SQLPattern.TripleColumns, Var> fieldEntry : p.getTripleFields().entrySet()) { |
| if(fieldEntry.getValue() != null && !fieldEntry.getValue().hasValue() && !joined.contains(fieldEntry.getValue().getName()) |
| && resolveVariables.contains(fieldEntry.getValue().getName())) { |
| p.setJoinField(fieldEntry.getKey(), variables.get(fieldEntry.getValue().getName()).getName()); |
| joined.add(fieldEntry.getValue().getName()); |
| } |
| } |
| } |
| |
| for(SQLAbstractSubquery sq : f.getSubqueries()) { |
| for(SQLVariable sq_v : sq.getQueryVariables()) { |
| if(!joined.contains(sq_v.getSparqlName()) && resolveVariables.contains(sq_v.getSparqlName()) && sq_v.getProjectionType() == ValueType.NODE) { |
| // this is needed in case we need to JOIN with the NODES table to retrieve values |
| SQLVariable sv = variables.get(sq_v.getSparqlName()); // fetch the name of the variable in the enclosing query |
| sq.getJoinFields().add(new SQLAbstractSubquery.VariableMapping(sv.getName(), sq_v.getName())); |
| joined.add(sq_v.getSparqlName()); |
| } |
| } |
| } |
| } |
| } |
| |
| |
| private StringBuilder buildSelectClause() { |
| List<String> projections = new ArrayList<>(); |
| |
| // enforce order in SELECT part, we need this for merging UNION subqueries |
| List<SQLVariable> vars = new ArrayList<>(variables.values()); |
| Collections.sort(vars, SQLVariable.sparqlNameComparator); |
| |
| |
| for(SQLVariable v : vars) { |
| if(v.getProjectionType() != ValueType.NONE && (projectedVars.isEmpty() || projectedVars.contains(v.getSparqlName()))) { |
| String projectedName = v.getName(); |
| String fromName = v.getExpressions().get(0); |
| |
| projections.add(fromName + " AS " + projectedName); |
| |
| if(v.getLiteralTypeExpression() != null) { |
| projections.add(v.getLiteralTypeExpression() + " AS " + projectedName + "_TYPE"); |
| } |
| if(v.getLiteralLangExpression() != null) { |
| projections.add(v.getLiteralLangExpression() + " AS " + projectedName + "_LANG"); |
| } |
| } |
| } |
| |
| // SQL enforces ORDER BY variables to occur in the select part in case distinct is set |
| int counter = 0; |
| if(distinct) { |
| for(OrderElem e : orderby) { |
| projections.add(evaluateExpression(e.getExpr(), ValueType.STRING) + " AS _OB" + (++counter)); |
| } |
| } |
| |
| |
| StringBuilder selectClause = new StringBuilder(); |
| |
| if(distinct) { |
| selectClause.append("DISTINCT "); |
| } |
| |
| Joiner.on(", ").appendTo(selectClause, projections); |
| |
| return selectClause; |
| } |
| |
| |
| private StringBuilder buildFromClause() { |
| // build the from-clause of the query; the from clause is constructed as follows: |
| // 1. for each pattern P, there will be a "KiWiTriple P" in the from clause |
| // 2. for each variable V in P occurring in |
| // - subject, there will be a "inner join P.subject as P_S_V" or "left outer join P.subject as P_S_V", |
| // depending on whether the "optional" parameter is false or true |
| // - property, there will be a "inner join P.property as P_P_V" or "left outer join p.property as P_P_V" |
| // - object, there will be a "inner join P.object as P_O_V" or "left outer join p.object as P_O_V" |
| // - context, there will be a "inner join P.context as P_C_V" or "left outer join p.context as P_C_V" |
| StringBuilder fromClause = new StringBuilder(); |
| for(Iterator<SQLFragment> fit = fragments.iterator(); fit.hasNext(); ) { |
| fromClause.append(fit.next().buildFromClause()); |
| if(fit.hasNext()) { |
| fromClause.append("\n LEFT JOIN \n "); |
| } |
| } |
| |
| return fromClause; |
| } |
| |
| |
| private StringBuilder buildWhereClause() { |
| // build the where clause as follows: |
| // 1. iterate over all patterns and for each resource and literal field in subject, |
| // property, object, or context, and set a query condition according to the |
| // nodes given in the pattern |
| // 2. for each variable that has more than one occurrences, add a join condition |
| // 3. for each variable in the initialBindings, add a condition to the where clause |
| |
| // list of where conditions that will later be connected by AND |
| List<String> whereConditions = new LinkedList<String>(); |
| |
| // 1. for the first pattern of the first fragment, we add the conditions to the WHERE clause |
| |
| for(SQLFragment fragment : fragments) { |
| if(fragment.getConditionPosition() != SQLFragment.ConditionPosition.JOIN) { |
| whereConditions.add(fragment.buildConditionClause()); |
| } |
| } |
| |
| // 3. for each variable in the initialBindings, add a condition to the where clause setting it |
| // to the node given as binding |
| if(bindings != null) { |
| for(String v : bindings.getBindingNames()) { |
| SQLVariable sv = variables.get(v); |
| |
| if(sv != null && !sv.getExpressions().isEmpty()) { |
| List<String> vNames = sv.getExpressions(); |
| String vName = vNames.get(0); |
| Value binding = converter.convert(bindings.getValue(v)); |
| if(binding instanceof KiWiNode) { |
| whereConditions.add(vName+" = "+((KiWiNode)binding).getId()); |
| } else { |
| throw new IllegalStateException("the values in this binding have not been created by the KiWi value factory"); |
| } |
| |
| } |
| } |
| } |
| |
| // construct the where clause |
| StringBuilder whereClause = new StringBuilder(); |
| for(Iterator<String> it = whereConditions.iterator(); it.hasNext(); ) { |
| String condition = it.next(); |
| if(condition.length() > 0) { |
| whereClause.append(condition); |
| whereClause.append("\n "); |
| if (it.hasNext()) { |
| whereClause.append("AND "); |
| } |
| } |
| } |
| return whereClause; |
| } |
| |
| private StringBuilder buildHavingClause() { |
| |
| // list of where conditions that will later be connected by AND |
| List<CharSequence> havingConditions = new LinkedList<CharSequence>(); |
| |
| // 1. for the first pattern of the first fragment, we add the conditions to the WHERE clause |
| |
| for(SQLFragment fragment : fragments) { |
| if(fragment.getConditionPosition() == SQLFragment.ConditionPosition.HAVING) { |
| StringBuilder conditionClause = new StringBuilder(); |
| for(Iterator<String> cit = fragment.getConditions().iterator(); cit.hasNext(); ) { |
| if(conditionClause.length() > 0) { |
| conditionClause.append("\n AND "); |
| } |
| conditionClause.append(cit.next()); |
| } |
| |
| havingConditions.add(conditionClause); |
| } |
| } |
| |
| // construct the having clause |
| StringBuilder havingClause = new StringBuilder(); |
| for(Iterator<CharSequence> it = havingConditions.iterator(); it.hasNext(); ) { |
| CharSequence condition = it.next(); |
| if(condition.length() > 0) { |
| havingClause.append(condition); |
| havingClause.append("\n "); |
| if (it.hasNext()) { |
| havingClause.append("AND "); |
| } |
| } |
| } |
| return havingClause; |
| } |
| |
| |
| private StringBuilder buildOrderClause() { |
| StringBuilder orderClause = new StringBuilder(); |
| if(orderby.size() > 0) { |
| for(Iterator<OrderElem> it = orderby.iterator(); it.hasNext(); ) { |
| OrderElem elem = it.next(); |
| orderClause.append(evaluateExpression(elem.getExpr(), ValueType.STRING)); |
| if(elem.isAscending()) { |
| orderClause.append(" ASC"); |
| } else { |
| orderClause.append(" DESC"); |
| } |
| if(it.hasNext()) { |
| orderClause.append(", "); |
| } |
| } |
| orderClause.append(" \n"); |
| } |
| |
| return orderClause; |
| } |
| |
| private StringBuilder buildGroupClause() { |
| StringBuilder groupClause = new StringBuilder(); |
| if(groupLabels.size() > 0) { |
| for(Iterator<String> it = groupLabels.iterator(); it.hasNext(); ) { |
| SQLVariable sv = variables.get(it.next()); |
| |
| if(sv != null) { |
| Iterator<String> lit = sv.getExpressions().iterator(); |
| while (lit.hasNext()) { |
| groupClause.append(lit.next()); |
| if(lit.hasNext()) { |
| groupClause.append(", "); |
| } |
| } |
| } |
| |
| if(it.hasNext()) { |
| groupClause.append(", "); |
| } |
| } |
| groupClause.append(" \n"); |
| } |
| |
| return groupClause; |
| } |
| |
| |
| private StringBuilder buildLimitClause() { |
| // construct limit and offset |
| StringBuilder limitClause = new StringBuilder(); |
| if(limit > 0) { |
| limitClause |
| .append("LIMIT ") |
| .append(limit) |
| .append(" "); |
| } |
| if(offset >= 0) { |
| limitClause |
| .append("OFFSET ") |
| .append(offset) |
| .append(" "); |
| } |
| return limitClause; |
| } |
| |
| |
| private String evaluateExpression(ValueExpr expr, final ValueType optype) { |
| return new ValueExpressionEvaluator(expr, this, optype).build(); |
| } |
| |
| |
| protected ValueType getProjectionType(ValueExpr expr) { |
| if(expr instanceof BNodeGenerator) { |
| return ValueType.BNODE; |
| } else if(expr instanceof IRIFunction) { |
| return ValueType.URI; |
| } else if(expr instanceof FunctionCall) { |
| return functionRegistry.get(((FunctionCall) expr).getURI()).getReturnType(); |
| } else if(expr instanceof NAryValueOperator) { |
| return getProjectionType(((NAryValueOperator) expr).getArguments().get(0)); |
| } else if(expr instanceof ValueConstant) { |
| return ValueType.NODE; |
| /* |
| if (((ValueConstant) expr).getValue() instanceof URI) { |
| return ProjectionType.URI; |
| } else if (((ValueConstant) expr).getValue() instanceof Literal) { |
| Literal l = (Literal) ((ValueConstant) expr).getValue(); |
| if (XSD.Integer.equals(l.getDatatype()) || XSD.Int.equals(l.getDatatype())) { |
| return ProjectionType.INT; |
| } else if (XSD.Double.equals(l.getDatatype()) || XSD.Float.equals(l.getDatatype())) { |
| return ProjectionType.DOUBLE; |
| } else { |
| return ProjectionType.STRING; |
| } |
| |
| } else { |
| return ProjectionType.STRING; |
| } |
| */ |
| } else if(expr instanceof Var) { |
| return ValueType.NODE; |
| } else if(expr instanceof MathExpr) { |
| MathExpr cmp = (MathExpr) expr; |
| |
| return new OPTypeFinder(cmp).coerce(); |
| } else if(expr instanceof Count) { |
| return ValueType.INT; |
| } else if(expr instanceof Sum) { |
| return ValueType.DOUBLE; |
| } else if(expr instanceof Avg) { |
| return ValueType.DOUBLE; |
| } else if(expr instanceof Compare) { |
| return ValueType.BOOL; |
| } else if(expr instanceof If) { |
| return getProjectionType(((If) expr).getResult()); |
| } else { |
| return ValueType.STRING; |
| } |
| |
| } |
| |
| |
| private String getLiteralLangExpression(ValueExpr expr) { |
| Var langVar = new LiteralTypeExpressionFinder(expr).expr; |
| |
| if(langVar != null) { |
| SQLVariable sqlVar = variables.get(langVar.getName()); |
| if(sqlVar != null) { |
| return sqlVar.getAlias() + ".lang"; |
| } |
| } |
| return null; |
| |
| } |
| |
| private String getLiteralTypeExpression(ValueExpr expr) { |
| Var typeVar = new LiteralTypeExpressionFinder(expr).expr; |
| |
| if(typeVar != null) { |
| SQLVariable sqlVar = variables.get(typeVar.getName()); |
| if(sqlVar != null) { |
| return sqlVar.getAlias() + ".ltype"; |
| } |
| } |
| return null; |
| } |
| |
| |
| /** |
| * Construct the SQL query for the given SPARQL query part. |
| * |
| * @return |
| */ |
| public StringBuilder build() { |
| StringBuilder selectClause = buildSelectClause(); |
| StringBuilder fromClause = buildFromClause(); |
| StringBuilder whereClause = buildWhereClause(); |
| StringBuilder orderClause = buildOrderClause(); |
| StringBuilder groupClause = buildGroupClause(); |
| StringBuilder havingClause = buildHavingClause(); |
| StringBuilder limitClause = buildLimitClause(); |
| |
| |
| StringBuilder queryString = new StringBuilder(); |
| queryString.append("SELECT "); |
| |
| if(selectClause.length() > 0) { |
| queryString.append(selectClause).append("\n "); |
| } else { |
| queryString.append("* \n"); |
| } |
| |
| if(fromClause.length() > 0) { |
| queryString.append("FROM ").append(fromClause).append("\n "); |
| } |
| |
| if(whereClause.length() > 0) { |
| queryString.append("WHERE ").append(whereClause).append("\n "); |
| } |
| |
| if(groupClause.length() > 0) { |
| queryString.append("GROUP BY ").append(groupClause).append("\n "); |
| } |
| |
| if(havingClause.length() > 9) { |
| queryString.append("HAVING ").append(havingClause).append("\n "); |
| } |
| |
| if(orderClause.length() > 0) { |
| queryString.append("ORDER BY ").append(orderClause).append("\n "); |
| } |
| |
| queryString.append(limitClause); |
| |
| |
| log.debug("original SPARQL syntax tree:\n {}", query); |
| log.debug("constructed SQL query string:\n {}",queryString); |
| log.debug("SPARQL -> SQL node variable mappings:\n {}", variables); |
| log.debug("projected variables:\n {}", projectedVars); |
| |
| return queryString; |
| } |
| |
| } |