@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;
        }
      }
    }
  }