@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(ValueType.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(ValueType.INT); fc.getArgs().get(0).visit(this); optypes.pop(); } else if (XMLSchema.BOOLEAN.toString().equals(fc.getURI()) && fc.getArgs().size() == 1) { optypes.push(ValueType.BOOL); fc.getArgs().get(0).visit(this); optypes.pop(); } else if (XMLSchema.DATE.toString().equals(fc.getURI()) && fc.getArgs().size() == 1) { optypes.push(ValueType.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(ValueConstant node) throws RuntimeException { String val = node.getValue().stringValue(); switch (optypes.peek()) { case STRING: case URI: builder.append("'").append(val).append("'"); break; case INT: builder.append(Integer.parseInt(val)); break; case DECIMAL: 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 NODE: KiWiNode n = parent.getConverter().convert(node.getValue()); builder.append(n.getId()); break; default: throw new IllegalArgumentException("unsupported value type: " + optypes.peek()); } }
@Override public void meet(MathExpr expr) throws RuntimeException { ValueType ot = new OPTypeFinder(expr).coerce(); if (ot == ValueType.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 == ValueType.NODE || ot == ValueType.TERM) { ot = ValueType.DOUBLE; } optypes.push(ot); expr.getLeftArg().visit(this); builder.append(getSQLOperator(expr.getOperator())); expr.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() == ValueType.NONE) { Preconditions.checkState( v.getExpressions().size() > 0, "no expressions available for variable"); countVariables.add(v.getExpressions().get(0)); } } builder.append("ARRAY["); Joiner.on(',').appendTo(builder, countVariables); builder.append("]"); } else { optypes.push(ValueType.NODE); 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, ValueType.STRING).build(), null, node.isDistinct())); } else { builder.append( parent .getDialect() .getGroupConcat( new ValueExpressionEvaluator(node.getArg(), parent, ValueType.STRING).build(), new ValueExpressionEvaluator(node.getSeparator(), parent, ValueType.STRING) .build(), node.isDistinct())); } }
private String castExpression(String arg, ValueType type) { if (type == null) { return arg; } switch (type) { case DECIMAL: return functionRegistry.get(XMLSchema.DECIMAL).getNative(parent.getDialect(), arg); 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 STRING: return arg; case NODE: return arg; default: return arg; } }
@Override public void meet(Like node) throws RuntimeException { if (node.isCaseSensitive()) { optypes.push(ValueType.STRING); node.getArg().visit(this); optypes.pop(); builder.append(" LIKE "); node.getPattern(); } else { builder.append( parent .getDialect() .getILike( new ValueExpressionEvaluator(node.getArg(), parent, ValueType.STRING).build(), node.getOpPattern())); } }
@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(IRIFunction fun) throws RuntimeException { if (fun.getBaseURI() != null) { String ex = new ValueExpressionEvaluator(fun.getArg(), parent, ValueType.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(ValueType.STRING); fun.getArg().visit(this); optypes.pop(); } }
/** * 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 + "'"; } } } }
private String getVariableAlias(String varName) { return parent.getVariables().get(varName).getAlias(); }
private String getVariableAlias(Var var) { return parent.getVariables().get(var.getName()).getAlias(); }
@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() != ValueType.NODE && sv.getProjectionType() != ValueType.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 DECIMAL: 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 TZDATE: Preconditions.checkState(var != null, "no alias available for variable"); builder.append(parent.getDialect().getDateTimeTZ(var)); break; case URI: Preconditions.checkState(var != null, "no alias available for variable"); builder.append(var).append(".svalue"); break; case TERM: case NODE: 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; } } } }