@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(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(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(And node) throws RuntimeException {
   builder.append("(");
   node.getLeftArg().visit(this);
   builder.append(" AND ");
   node.getRightArg().visit(this);
   builder.append(")");
 }
 @Override
 public void meet(Sum node) throws RuntimeException {
   builder.append("SUM(");
   optypes.push(ValueType.DOUBLE);
   node.getArg().visit(this);
   optypes.pop();
   builder.append(")");
 }
  @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(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(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(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(ValueType.NODE);
      arg.visit(this);
      optypes.pop();
      builder.append(" IS NOT NULL)");
    }
  }
  @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(If node) throws RuntimeException {
    builder.append("CASE WHEN ");

    optypes.push(ValueType.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(Regex re) throws RuntimeException {
   builder.append(
       optimizeRegexp(
           new ValueExpressionEvaluator(re.getArg(), parent, ValueType.STRING).build(),
           new ValueExpressionEvaluator(re.getPatternArg(), parent, ValueType.STRING).build(),
           re.getFlagsArg()));
 }
 @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(SameTerm cmp) throws RuntimeException {
   // covered by value binding in variables
   optypes.push(ValueType.TERM);
   cmp.getLeftArg().visit(this);
   builder.append(" = ");
   cmp.getRightArg().visit(this);
   optypes.pop();
 }
  @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(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()));
   }
 }
  @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(BNodeGenerator gen) throws RuntimeException {
   if (gen.getNodeIdExpr() != null) {
     // get value of argument and express it as string
     optypes.push(ValueType.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(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(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();
    }
  }
 @Override
 public void meet(Not node) throws RuntimeException {
   builder.append("NOT (");
   node.getArg().visit(this);
   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() != 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;
        }
      }
    }
  }