diff --git a/core/src/main/java/org/apache/druid/math/expr/BinaryLogicalOperatorExpr.java b/core/src/main/java/org/apache/druid/math/expr/BinaryLogicalOperatorExpr.java new file mode 100644 index 00000000000..dad35f30560 --- /dev/null +++ b/core/src/main/java/org/apache/druid/math/expr/BinaryLogicalOperatorExpr.java @@ -0,0 +1,266 @@ +/* + * 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.druid.math.expr; + +import org.apache.druid.java.util.common.guava.Comparators; + +import javax.annotation.Nullable; +import java.util.Objects; + +// logical operators live here + +class BinLtExpr extends BinaryEvalOpExprBase +{ + BinLtExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinLtExpr(op, left, right); + } + + @Override + protected ExprEval evalString(@Nullable String left, @Nullable String right) + { + return ExprEval.of(Comparators.naturalNullsFirst().compare(left, right) < 0, ExprType.LONG); + } + + @Override + protected final long evalLong(long left, long right) + { + return Evals.asLong(left < right); + } + + @Override + protected final double evalDouble(double left, double right) + { + // Use Double.compare for more consistent NaN handling. + return Evals.asDouble(Double.compare(left, right) < 0); + } +} + +class BinLeqExpr extends BinaryEvalOpExprBase +{ + BinLeqExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinLeqExpr(op, left, right); + } + + @Override + protected ExprEval evalString(@Nullable String left, @Nullable String right) + { + return ExprEval.of(Comparators.naturalNullsFirst().compare(left, right) <= 0, ExprType.LONG); + } + + @Override + protected final long evalLong(long left, long right) + { + return Evals.asLong(left <= right); + } + + @Override + protected final double evalDouble(double left, double right) + { + // Use Double.compare for more consistent NaN handling. + return Evals.asDouble(Double.compare(left, right) <= 0); + } +} + +class BinGtExpr extends BinaryEvalOpExprBase +{ + BinGtExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinGtExpr(op, left, right); + } + + @Override + protected ExprEval evalString(@Nullable String left, @Nullable String right) + { + return ExprEval.of(Comparators.naturalNullsFirst().compare(left, right) > 0, ExprType.LONG); + } + + @Override + protected final long evalLong(long left, long right) + { + return Evals.asLong(left > right); + } + + @Override + protected final double evalDouble(double left, double right) + { + // Use Double.compare for more consistent NaN handling. + return Evals.asDouble(Double.compare(left, right) > 0); + } +} + +class BinGeqExpr extends BinaryEvalOpExprBase +{ + BinGeqExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinGeqExpr(op, left, right); + } + + @Override + protected ExprEval evalString(@Nullable String left, @Nullable String right) + { + return ExprEval.of(Comparators.naturalNullsFirst().compare(left, right) >= 0, ExprType.LONG); + } + + @Override + protected final long evalLong(long left, long right) + { + return Evals.asLong(left >= right); + } + + @Override + protected final double evalDouble(double left, double right) + { + // Use Double.compare for more consistent NaN handling. + return Evals.asDouble(Double.compare(left, right) >= 0); + } +} + +class BinEqExpr extends BinaryEvalOpExprBase +{ + BinEqExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinEqExpr(op, left, right); + } + + @Override + protected ExprEval evalString(@Nullable String left, @Nullable String right) + { + return ExprEval.of(Objects.equals(left, right), ExprType.LONG); + } + + @Override + protected final long evalLong(long left, long right) + { + return Evals.asLong(left == right); + } + + @Override + protected final double evalDouble(double left, double right) + { + return Evals.asDouble(left == right); + } +} + +class BinNeqExpr extends BinaryEvalOpExprBase +{ + BinNeqExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinNeqExpr(op, left, right); + } + + @Override + protected ExprEval evalString(@Nullable String left, @Nullable String right) + { + return ExprEval.of(!Objects.equals(left, right), ExprType.LONG); + } + + @Override + protected final long evalLong(long left, long right) + { + return Evals.asLong(left != right); + } + + @Override + protected final double evalDouble(double left, double right) + { + return Evals.asDouble(left != right); + } +} + +class BinAndExpr extends BinaryOpExprBase +{ + BinAndExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinAndExpr(op, left, right); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + ExprEval leftVal = left.eval(bindings); + return leftVal.asBoolean() ? right.eval(bindings) : leftVal; + } +} + +class BinOrExpr extends BinaryOpExprBase +{ + BinOrExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinOrExpr(op, left, right); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + ExprEval leftVal = left.eval(bindings); + return leftVal.asBoolean() ? leftVal : right.eval(bindings); + } + +} diff --git a/core/src/main/java/org/apache/druid/math/expr/BinaryMathOperatorExpr.java b/core/src/main/java/org/apache/druid/math/expr/BinaryMathOperatorExpr.java new file mode 100644 index 00000000000..21fadd49032 --- /dev/null +++ b/core/src/main/java/org/apache/druid/math/expr/BinaryMathOperatorExpr.java @@ -0,0 +1,191 @@ +/* + * 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.druid.math.expr; + +import com.google.common.math.LongMath; +import com.google.common.primitives.Ints; +import org.apache.druid.common.config.NullHandling; + +import javax.annotation.Nullable; + +// math operators live here + +class BinPlusExpr extends BinaryEvalOpExprBase +{ + BinPlusExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinPlusExpr(op, left, right); + } + + @Override + protected ExprEval evalString(@Nullable String left, @Nullable String right) + { + return ExprEval.of(NullHandling.nullToEmptyIfNeeded(left) + + NullHandling.nullToEmptyIfNeeded(right)); + } + + @Override + protected final long evalLong(long left, long right) + { + return left + right; + } + + @Override + protected final double evalDouble(double left, double right) + { + return left + right; + } +} + +class BinMinusExpr extends BinaryEvalOpExprBase +{ + BinMinusExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinMinusExpr(op, left, right); + } + + @Override + protected final long evalLong(long left, long right) + { + return left - right; + } + + @Override + protected final double evalDouble(double left, double right) + { + return left - right; + } +} + +class BinMulExpr extends BinaryEvalOpExprBase +{ + BinMulExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinMulExpr(op, left, right); + } + + @Override + protected final long evalLong(long left, long right) + { + return left * right; + } + + @Override + protected final double evalDouble(double left, double right) + { + return left * right; + } +} + +class BinDivExpr extends BinaryEvalOpExprBase +{ + BinDivExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinDivExpr(op, left, right); + } + + @Override + protected final long evalLong(long left, long right) + { + return left / right; + } + + @Override + protected final double evalDouble(double left, double right) + { + return left / right; + } +} + +class BinPowExpr extends BinaryEvalOpExprBase +{ + BinPowExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinPowExpr(op, left, right); + } + + @Override + protected final long evalLong(long left, long right) + { + return LongMath.pow(left, Ints.checkedCast(right)); + } + + @Override + protected final double evalDouble(double left, double right) + { + return Math.pow(left, right); + } +} + +class BinModuloExpr extends BinaryEvalOpExprBase +{ + BinModuloExpr(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + protected BinaryOpExprBase copy(Expr left, Expr right) + { + return new BinModuloExpr(op, left, right); + } + + @Override + protected final long evalLong(long left, long right) + { + return left % right; + } + + @Override + protected final double evalDouble(double left, double right) + { + return left % right; + } +} diff --git a/core/src/main/java/org/apache/druid/math/expr/BinaryOperatorExpr.java b/core/src/main/java/org/apache/druid/math/expr/BinaryOperatorExpr.java new file mode 100644 index 00000000000..9c390587bd4 --- /dev/null +++ b/core/src/main/java/org/apache/druid/math/expr/BinaryOperatorExpr.java @@ -0,0 +1,158 @@ +/* + * 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.druid.math.expr; + +import com.google.common.collect.ImmutableSet; +import org.apache.druid.common.config.NullHandling; +import org.apache.druid.java.util.common.StringUtils; + +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * Base type for all binary operators, this {@link Expr} has two children {@link Expr} for the left and right side + * operands. + * + * Note: all concrete subclass of this should have constructor with the form of (String, Expr, Expr) + * if it's not possible, just be sure Evals.binaryOp() can handle that + */ +abstract class BinaryOpExprBase implements Expr +{ + protected final String op; + protected final Expr left; + protected final Expr right; + + BinaryOpExprBase(String op, Expr left, Expr right) + { + this.op = op; + this.left = left; + this.right = right; + } + + @Override + public void visit(Visitor visitor) + { + left.visit(visitor); + right.visit(visitor); + visitor.visit(this); + } + + @Override + public Expr visit(Shuttle shuttle) + { + Expr newLeft = left.visit(shuttle); + Expr newRight = right.visit(shuttle); + //noinspection ObjectEquality (checking for object equality here is intentional) + if (left != newLeft || right != newRight) { + return shuttle.visit(copy(newLeft, newRight)); + } + return shuttle.visit(this); + } + + @Override + public String toString() + { + return StringUtils.format("(%s %s %s)", op, left, right); + } + + @Override + public String stringify() + { + return StringUtils.format("(%s %s %s)", left.stringify(), op, right.stringify()); + } + + protected abstract BinaryOpExprBase copy(Expr left, Expr right); + + @Override + public BindingDetails analyzeInputs() + { + // currently all binary operators operate on scalar inputs + return left.analyzeInputs().with(right).withScalarArguments(ImmutableSet.of(left, right)); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BinaryOpExprBase that = (BinaryOpExprBase) o; + return Objects.equals(op, that.op) && + Objects.equals(left, that.left) && + Objects.equals(right, that.right); + } + + @Override + public int hashCode() + { + return Objects.hash(op, left, right); + } +} + +/** + * Base class for numerical binary operators, with additional methods defined to evaluate primitive values directly + * instead of wrapped with {@link ExprEval} + */ +abstract class BinaryEvalOpExprBase extends BinaryOpExprBase +{ + BinaryEvalOpExprBase(String op, Expr left, Expr right) + { + super(op, left, right); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + ExprEval leftVal = left.eval(bindings); + ExprEval rightVal = right.eval(bindings); + + // Result of any Binary expressions is null if any of the argument is null. + // e.g "select null * 2 as c;" or "select null + 1 as c;" will return null as per Standard SQL spec. + if (NullHandling.sqlCompatible() && (leftVal.value() == null || rightVal.value() == null)) { + return ExprEval.of(null); + } + + if (leftVal.type() == ExprType.STRING && rightVal.type() == ExprType.STRING) { + return evalString(leftVal.asString(), rightVal.asString()); + } else if (leftVal.type() == ExprType.LONG && rightVal.type() == ExprType.LONG) { + if (NullHandling.sqlCompatible() && (leftVal.isNumericNull() || rightVal.isNumericNull())) { + return ExprEval.of(null); + } + return ExprEval.of(evalLong(leftVal.asLong(), rightVal.asLong())); + } else { + if (NullHandling.sqlCompatible() && (leftVal.isNumericNull() || rightVal.isNumericNull())) { + return ExprEval.of(null); + } + return ExprEval.of(evalDouble(leftVal.asDouble(), rightVal.asDouble())); + } + } + + protected ExprEval evalString(@Nullable String left, @Nullable String right) + { + throw new IllegalArgumentException("unsupported type " + ExprType.STRING); + } + + protected abstract long evalLong(long left, long right); + + protected abstract double evalDouble(double left, double right); +} diff --git a/core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java b/core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java new file mode 100644 index 00000000000..4f6099cf366 --- /dev/null +++ b/core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java @@ -0,0 +1,457 @@ +/* + * 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.druid.math.expr; + +import com.google.common.base.Preconditions; +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.druid.common.config.NullHandling; +import org.apache.druid.java.util.common.StringUtils; + +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Objects; + +/** + * Base type for all constant expressions. {@link ConstantExpr} allow for direct value extraction without evaluating + * {@link Expr.ObjectBinding}. {@link ConstantExpr} are terminal nodes of an expression tree, and have no children + * {@link Expr}. + */ +abstract class ConstantExpr implements Expr +{ + @Override + public boolean isLiteral() + { + return true; + } + + @Override + public void visit(Visitor visitor) + { + visitor.visit(this); + } + + @Override + public Expr visit(Shuttle shuttle) + { + return shuttle.visit(this); + } + + @Override + public BindingDetails analyzeInputs() + { + return new BindingDetails(); + } + + @Override + public String stringify() + { + return toString(); + } +} + +/** + * Base class for typed 'null' value constants (or default value, depending on {@link NullHandling#sqlCompatible}) + */ +abstract class NullNumericConstantExpr extends ConstantExpr +{ + @Override + public Object getLiteralValue() + { + return null; + } + + @Override + public String toString() + { + return NULL_LITERAL; + } +} + +class LongExpr extends ConstantExpr +{ + private final Long value; + + LongExpr(Long value) + { + this.value = Preconditions.checkNotNull(value, "value"); + } + + @Override + public Object getLiteralValue() + { + return value; + } + + @Override + public String toString() + { + return String.valueOf(value); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + return ExprEval.ofLong(value); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LongExpr longExpr = (LongExpr) o; + return Objects.equals(value, longExpr.value); + } + + @Override + public int hashCode() + { + return Objects.hash(value); + } +} + +class NullLongExpr extends NullNumericConstantExpr +{ + @Override + public ExprEval eval(ObjectBinding bindings) + { + return ExprEval.ofLong(null); + } + + @Override + public final int hashCode() + { + return NullLongExpr.class.hashCode(); + } + + @Override + public final boolean equals(Object obj) + { + return obj instanceof NullLongExpr; + } +} + +class LongArrayExpr extends ConstantExpr +{ + private final Long[] value; + + LongArrayExpr(Long[] value) + { + this.value = Preconditions.checkNotNull(value, "value"); + } + + @Override + public Object getLiteralValue() + { + return value; + } + + @Override + public String toString() + { + return Arrays.toString(value); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + return ExprEval.ofLongArray(value); + } + + @Override + public String stringify() + { + if (value.length == 0) { + return "[]"; + } + return StringUtils.format("%s", toString()); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LongArrayExpr that = (LongArrayExpr) o; + return Arrays.equals(value, that.value); + } + + @Override + public int hashCode() + { + return Arrays.hashCode(value); + } +} + +class StringExpr extends ConstantExpr +{ + @Nullable + private final String value; + + StringExpr(@Nullable String value) + { + this.value = NullHandling.emptyToNullIfNeeded(value); + } + + @Nullable + @Override + public Object getLiteralValue() + { + return value; + } + + @Override + public String toString() + { + return value; + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + return ExprEval.of(value); + } + + @Override + public String stringify() + { + // escape as javascript string since string literals are wrapped in single quotes + return value == null ? NULL_LITERAL : StringUtils.format("'%s'", StringEscapeUtils.escapeJavaScript(value)); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StringExpr that = (StringExpr) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() + { + return Objects.hash(value); + } +} + +class StringArrayExpr extends ConstantExpr +{ + private final String[] value; + + StringArrayExpr(String[] value) + { + this.value = Preconditions.checkNotNull(value, "value"); + } + + @Override + public Object getLiteralValue() + { + return value; + } + + @Override + public String toString() + { + return Arrays.toString(value); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + return ExprEval.ofStringArray(value); + } + + @Override + public String stringify() + { + if (value.length == 0) { + return "[]"; + } + + return StringUtils.format( + "[%s]", + ARG_JOINER.join( + Arrays.stream(value) + .map(s -> s == null + ? NULL_LITERAL + // escape as javascript string since string literals are wrapped in single quotes + : StringUtils.format("'%s'", StringEscapeUtils.escapeJavaScript(s)) + ) + .iterator() + ) + ); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StringArrayExpr that = (StringArrayExpr) o; + return Arrays.equals(value, that.value); + } + + @Override + public int hashCode() + { + return Arrays.hashCode(value); + } +} + +class DoubleExpr extends ConstantExpr +{ + private final Double value; + + DoubleExpr(Double value) + { + this.value = Preconditions.checkNotNull(value, "value"); + } + + @Override + public Object getLiteralValue() + { + return value; + } + + @Override + public String toString() + { + return String.valueOf(value); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + return ExprEval.ofDouble(value); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DoubleExpr that = (DoubleExpr) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() + { + return Objects.hash(value); + } +} + +class NullDoubleExpr extends NullNumericConstantExpr +{ + @Override + public ExprEval eval(ObjectBinding bindings) + { + return ExprEval.ofDouble(null); + } + + @Override + public final int hashCode() + { + return NullDoubleExpr.class.hashCode(); + } + + @Override + public final boolean equals(Object obj) + { + return obj instanceof NullDoubleExpr; + } +} + +class DoubleArrayExpr extends ConstantExpr +{ + private final Double[] value; + + DoubleArrayExpr(Double[] value) + { + this.value = Preconditions.checkNotNull(value, "value"); + } + + @Override + public Object getLiteralValue() + { + return value; + } + + @Override + public String toString() + { + return Arrays.toString(value); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + return ExprEval.ofDoubleArray(value); + } + + @Override + public String stringify() + { + if (value.length == 0) { + return "[]"; + } + return StringUtils.format("%s", toString()); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DoubleArrayExpr that = (DoubleArrayExpr) o; + return Arrays.equals(value, that.value); + } + + @Override + public int hashCode() + { + return Arrays.hashCode(value); + } +} diff --git a/core/src/main/java/org/apache/druid/math/expr/Expr.java b/core/src/main/java/org/apache/druid/math/expr/Expr.java index 1cd7d6253ff..e0a1525c7df 100644 --- a/core/src/main/java/org/apache/druid/math/expr/Expr.java +++ b/core/src/main/java/org/apache/druid/math/expr/Expr.java @@ -20,28 +20,16 @@ package org.apache.druid.math.expr; import com.google.common.base.Joiner; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; -import com.google.common.math.LongMath; -import com.google.common.primitives.Ints; -import org.apache.commons.lang.StringEscapeUtils; import org.apache.druid.annotations.SubclassesMustOverrideEqualsAndHashCode; -import org.apache.druid.common.config.NullHandling; -import org.apache.druid.java.util.common.IAE; import org.apache.druid.java.util.common.ISE; -import org.apache.druid.java.util.common.StringUtils; -import org.apache.druid.java.util.common.guava.Comparators; import javax.annotation.Nullable; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; /** * Base interface of Druid expression language abstract syntax tree nodes. All {@link Expr} implementations are @@ -444,1533 +432,3 @@ public interface Expr } } } - -/** - * Base type for all constant expressions. {@link ConstantExpr} allow for direct value extraction without evaluating - * {@link Expr.ObjectBinding}. {@link ConstantExpr} are terminal nodes of an expression tree, and have no children - * {@link Expr}. - */ -abstract class ConstantExpr implements Expr -{ - @Override - public boolean isLiteral() - { - return true; - } - - @Override - public void visit(Visitor visitor) - { - visitor.visit(this); - } - - @Override - public Expr visit(Shuttle shuttle) - { - return shuttle.visit(this); - } - - @Override - public BindingDetails analyzeInputs() - { - return new BindingDetails(); - } - - @Override - public String stringify() - { - return toString(); - } -} - -abstract class NullNumericConstantExpr extends ConstantExpr -{ - @Override - public Object getLiteralValue() - { - return null; - } - - @Override - public String toString() - { - return NULL_LITERAL; - } -} - -class LongExpr extends ConstantExpr -{ - private final Long value; - - LongExpr(Long value) - { - this.value = Preconditions.checkNotNull(value, "value"); - } - - @Override - public Object getLiteralValue() - { - return value; - } - - @Override - public String toString() - { - return String.valueOf(value); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - return ExprEval.ofLong(value); - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - LongExpr longExpr = (LongExpr) o; - return Objects.equals(value, longExpr.value); - } - - @Override - public int hashCode() - { - return Objects.hash(value); - } -} - -class NullLongExpr extends NullNumericConstantExpr -{ - @Override - public ExprEval eval(ObjectBinding bindings) - { - return ExprEval.ofLong(null); - } - - @Override - public final int hashCode() - { - return NullLongExpr.class.hashCode(); - } - - @Override - public final boolean equals(Object obj) - { - return obj instanceof NullLongExpr; - } -} - - -class LongArrayExpr extends ConstantExpr -{ - private final Long[] value; - - LongArrayExpr(Long[] value) - { - this.value = Preconditions.checkNotNull(value, "value"); - } - - @Override - public Object getLiteralValue() - { - return value; - } - - @Override - public String toString() - { - return Arrays.toString(value); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - return ExprEval.ofLongArray(value); - } - - @Override - public String stringify() - { - if (value.length == 0) { - return "[]"; - } - return StringUtils.format("%s", toString()); - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - LongArrayExpr that = (LongArrayExpr) o; - return Arrays.equals(value, that.value); - } - - @Override - public int hashCode() - { - return Arrays.hashCode(value); - } -} - -class StringExpr extends ConstantExpr -{ - @Nullable - private final String value; - - StringExpr(@Nullable String value) - { - this.value = NullHandling.emptyToNullIfNeeded(value); - } - - @Nullable - @Override - public Object getLiteralValue() - { - return value; - } - - @Override - public String toString() - { - return value; - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - return ExprEval.of(value); - } - - @Override - public String stringify() - { - // escape as javascript string since string literals are wrapped in single quotes - return value == null ? NULL_LITERAL : StringUtils.format("'%s'", StringEscapeUtils.escapeJavaScript(value)); - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - StringExpr that = (StringExpr) o; - return Objects.equals(value, that.value); - } - - @Override - public int hashCode() - { - return Objects.hash(value); - } -} - -class StringArrayExpr extends ConstantExpr -{ - private final String[] value; - - StringArrayExpr(String[] value) - { - this.value = Preconditions.checkNotNull(value, "value"); - } - - @Override - public Object getLiteralValue() - { - return value; - } - - @Override - public String toString() - { - return Arrays.toString(value); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - return ExprEval.ofStringArray(value); - } - - @Override - public String stringify() - { - if (value.length == 0) { - return "[]"; - } - - return StringUtils.format( - "[%s]", - ARG_JOINER.join( - Arrays.stream(value) - .map(s -> s == null - ? NULL_LITERAL - // escape as javascript string since string literals are wrapped in single quotes - : StringUtils.format("'%s'", StringEscapeUtils.escapeJavaScript(s)) - ) - .iterator() - ) - ); - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - StringArrayExpr that = (StringArrayExpr) o; - return Arrays.equals(value, that.value); - } - - @Override - public int hashCode() - { - return Arrays.hashCode(value); - } -} - -class DoubleExpr extends ConstantExpr -{ - private final Double value; - - DoubleExpr(Double value) - { - this.value = Preconditions.checkNotNull(value, "value"); - } - - @Override - public Object getLiteralValue() - { - return value; - } - - @Override - public String toString() - { - return String.valueOf(value); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - return ExprEval.ofDouble(value); - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DoubleExpr that = (DoubleExpr) o; - return Objects.equals(value, that.value); - } - - @Override - public int hashCode() - { - return Objects.hash(value); - } -} - -class NullDoubleExpr extends NullNumericConstantExpr -{ - @Override - public ExprEval eval(ObjectBinding bindings) - { - return ExprEval.ofDouble(null); - } - - @Override - public final int hashCode() - { - return NullDoubleExpr.class.hashCode(); - } - - @Override - public final boolean equals(Object obj) - { - return obj instanceof NullDoubleExpr; - } -} - -class DoubleArrayExpr extends ConstantExpr -{ - private final Double[] value; - - DoubleArrayExpr(Double[] value) - { - this.value = Preconditions.checkNotNull(value, "value"); - } - - @Override - public Object getLiteralValue() - { - return value; - } - - @Override - public String toString() - { - return Arrays.toString(value); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - return ExprEval.ofDoubleArray(value); - } - - @Override - public String stringify() - { - if (value.length == 0) { - return "[]"; - } - return StringUtils.format("%s", toString()); - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DoubleArrayExpr that = (DoubleArrayExpr) o; - return Arrays.equals(value, that.value); - } - - @Override - public int hashCode() - { - return Arrays.hashCode(value); - } -} - -/** - * This {@link Expr} node is used to represent a variable in the expression language. At evaluation time, the string - * identifier will be used to retrieve the runtime value for the variable from {@link Expr.ObjectBinding}. - * {@link IdentifierExpr} are terminal nodes of an expression tree, and have no children {@link Expr}. - */ -class IdentifierExpr implements Expr -{ - private final String identifier; - private final String binding; - - /** - * Construct a identifier expression for a {@link LambdaExpr}, where the {@link #identifier} is equal to - * {@link #binding} - */ - IdentifierExpr(String value) - { - this.identifier = value; - this.binding = value; - } - - /** - * Construct a normal identifier expression, where {@link #binding} is the key to fetch the backing value from - * {@link Expr.ObjectBinding} and the {@link #identifier} is a unique string that identifies this usage of the - * binding. - */ - IdentifierExpr(String identifier, String binding) - { - this.identifier = identifier; - this.binding = binding; - } - - @Override - public String toString() - { - return binding; - } - - /** - * Unique identifier for the binding - */ - @Nullable - public String getIdentifier() - { - return identifier; - } - - /** - * Value binding, key to retrieve value from {@link Expr.ObjectBinding#get(String)} - */ - @Nullable - public String getBinding() - { - return binding; - } - - @Nullable - @Override - public String getIdentifierIfIdentifier() - { - return identifier; - } - - @Nullable - @Override - public String getBindingIfIdentifier() - { - return binding; - } - - @Nullable - @Override - public IdentifierExpr getIdentifierExprIfIdentifierExpr() - { - return this; - } - - @Override - public BindingDetails analyzeInputs() - { - return new BindingDetails(this); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - return ExprEval.bestEffortOf(bindings.get(binding)); - } - - @Override - public String stringify() - { - // escape as java strings since identifiers are wrapped in double quotes - return StringUtils.format("\"%s\"", StringEscapeUtils.escapeJava(binding)); - } - - @Override - public void visit(Visitor visitor) - { - visitor.visit(this); - } - - @Override - public Expr visit(Shuttle shuttle) - { - return shuttle.visit(this); - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - IdentifierExpr that = (IdentifierExpr) o; - return Objects.equals(identifier, that.identifier); - } - - @Override - public int hashCode() - { - return Objects.hash(identifier); - } -} - -class LambdaExpr implements Expr -{ - private final ImmutableList args; - private final Expr expr; - - LambdaExpr(List args, Expr expr) - { - this.args = ImmutableList.copyOf(args); - this.expr = expr; - } - - @Override - public String toString() - { - return StringUtils.format("(%s -> %s)", args, expr); - } - - int identifierCount() - { - return args.size(); - } - - @Nullable - public String getIdentifier() - { - Preconditions.checkState(args.size() < 2, "LambdaExpr has multiple arguments"); - if (args.size() == 1) { - return args.get(0).toString(); - } - return null; - } - - public List getIdentifiers() - { - return args.stream().map(IdentifierExpr::toString).collect(Collectors.toList()); - } - - ImmutableList getIdentifierExprs() - { - return args; - } - - public Expr getExpr() - { - return expr; - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - return expr.eval(bindings); - } - - @Override - public String stringify() - { - return StringUtils.format("(%s) -> %s", ARG_JOINER.join(getIdentifiers()), expr.stringify()); - } - - @Override - public void visit(Visitor visitor) - { - expr.visit(visitor); - visitor.visit(this); - } - - @Override - public Expr visit(Shuttle shuttle) - { - List newArgs = - args.stream().map(arg -> (IdentifierExpr) shuttle.visit(arg)).collect(Collectors.toList()); - Expr newBody = expr.visit(shuttle); - return shuttle.visit(new LambdaExpr(newArgs, newBody)); - } - - @Override - public BindingDetails analyzeInputs() - { - final Set lambdaArgs = args.stream().map(IdentifierExpr::toString).collect(Collectors.toSet()); - BindingDetails bodyDetails = expr.analyzeInputs(); - return bodyDetails.removeLambdaArguments(lambdaArgs); - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - LambdaExpr that = (LambdaExpr) o; - return Objects.equals(args, that.args) && - Objects.equals(expr, that.expr); - } - - @Override - public int hashCode() - { - return Objects.hash(args, expr); - } -} - -/** - * {@link Expr} node for a {@link Function} call. {@link FunctionExpr} has children {@link Expr} in the form of the - * list of arguments that are passed to the {@link Function} along with the {@link Expr.ObjectBinding} when it is - * evaluated. - */ -class FunctionExpr implements Expr -{ - final Function function; - final ImmutableList args; - private final String name; - - FunctionExpr(Function function, String name, List args) - { - this.function = function; - this.name = name; - this.args = ImmutableList.copyOf(args); - function.validateArguments(args); - } - - @Override - public String toString() - { - return StringUtils.format("(%s %s)", name, args); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - return function.apply(args, bindings); - } - - @Override - public String stringify() - { - return StringUtils.format("%s(%s)", name, ARG_JOINER.join(args.stream().map(Expr::stringify).iterator())); - } - - @Override - public void visit(Visitor visitor) - { - for (Expr child : args) { - child.visit(visitor); - } - visitor.visit(this); - } - - @Override - public Expr visit(Shuttle shuttle) - { - List newArgs = args.stream().map(shuttle::visit).collect(Collectors.toList()); - return shuttle.visit(new FunctionExpr(function, name, newArgs)); - } - - @Override - public BindingDetails analyzeInputs() - { - BindingDetails accumulator = new BindingDetails(); - - for (Expr arg : args) { - accumulator = accumulator.with(arg); - } - return accumulator.withScalarArguments(function.getScalarInputs(args)) - .withArrayArguments(function.getArrayInputs(args)) - .withArrayInputs(function.hasArrayInputs()) - .withArrayOutput(function.hasArrayOutput()); - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - FunctionExpr that = (FunctionExpr) o; - return args.equals(that.args) && - name.equals(that.name); - } - - @Override - public int hashCode() - { - return Objects.hash(args, name); - } -} - -/** - * This {@link Expr} node is representative of an {@link ApplyFunction}, and has children in the form of a - * {@link LambdaExpr} and the list of {@link Expr} arguments that are combined with {@link Expr.ObjectBinding} to - * evaluate the {@link LambdaExpr}. - */ -class ApplyFunctionExpr implements Expr -{ - final ApplyFunction function; - final String name; - final LambdaExpr lambdaExpr; - final ImmutableList argsExpr; - final BindingDetails bindingDetails; - final BindingDetails lambdaBindingDetails; - final ImmutableList argsBindingDetails; - - ApplyFunctionExpr(ApplyFunction function, String name, LambdaExpr expr, List args) - { - this.function = function; - this.name = name; - this.argsExpr = ImmutableList.copyOf(args); - this.lambdaExpr = expr; - - function.validateArguments(expr, args); - - // apply function expressions are examined during expression selector creation, so precompute and cache the - // binding details of children - ImmutableList.Builder argBindingDetailsBuilder = ImmutableList.builder(); - BindingDetails accumulator = new BindingDetails(); - for (Expr arg : argsExpr) { - BindingDetails argDetails = arg.analyzeInputs(); - argBindingDetailsBuilder.add(argDetails); - accumulator = accumulator.with(argDetails); - } - - lambdaBindingDetails = lambdaExpr.analyzeInputs(); - - bindingDetails = accumulator.with(lambdaBindingDetails) - .withArrayArguments(function.getArrayInputs(argsExpr)) - .withArrayInputs(true) - .withArrayOutput(function.hasArrayOutput(lambdaExpr)); - argsBindingDetails = argBindingDetailsBuilder.build(); - } - - @Override - public String toString() - { - return StringUtils.format("(%s %s, %s)", name, lambdaExpr, argsExpr); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - return function.apply(lambdaExpr, argsExpr, bindings); - } - - @Override - public String stringify() - { - return StringUtils.format( - "%s(%s, %s)", - name, - lambdaExpr.stringify(), - ARG_JOINER.join(argsExpr.stream().map(Expr::stringify).iterator()) - ); - } - - @Override - public void visit(Visitor visitor) - { - lambdaExpr.visit(visitor); - for (Expr arg : argsExpr) { - arg.visit(visitor); - } - visitor.visit(this); - } - - @Override - public Expr visit(Shuttle shuttle) - { - LambdaExpr newLambda = (LambdaExpr) lambdaExpr.visit(shuttle); - List newArgs = argsExpr.stream().map(shuttle::visit).collect(Collectors.toList()); - return shuttle.visit(new ApplyFunctionExpr(function, name, newLambda, newArgs)); - } - - @Override - public BindingDetails analyzeInputs() - { - return bindingDetails; - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ApplyFunctionExpr that = (ApplyFunctionExpr) o; - return name.equals(that.name) && - lambdaExpr.equals(that.lambdaExpr) && - argsExpr.equals(that.argsExpr); - } - - @Override - public int hashCode() - { - return Objects.hash(name, lambdaExpr, argsExpr); - } -} - -/** - * Base type for all single argument operators, with a single {@link Expr} child for the operand. - */ -abstract class UnaryExpr implements Expr -{ - final Expr expr; - - UnaryExpr(Expr expr) - { - this.expr = expr; - } - - abstract UnaryExpr copy(Expr expr); - - @Override - public void visit(Visitor visitor) - { - expr.visit(visitor); - visitor.visit(this); - } - - @Override - public Expr visit(Shuttle shuttle) - { - Expr newExpr = expr.visit(shuttle); - //noinspection ObjectEquality (checking for object equality here is intentional) - if (newExpr != expr) { - return shuttle.visit(copy(newExpr)); - } - return shuttle.visit(this); - } - - @Override - public BindingDetails analyzeInputs() - { - // currently all unary operators only operate on scalar inputs - return expr.analyzeInputs().withScalarArguments(ImmutableSet.of(expr)); - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - UnaryExpr unaryExpr = (UnaryExpr) o; - return Objects.equals(expr, unaryExpr.expr); - } - - @Override - public int hashCode() - { - return Objects.hash(expr); - } -} - -class UnaryMinusExpr extends UnaryExpr -{ - UnaryMinusExpr(Expr expr) - { - super(expr); - } - - @Override - UnaryExpr copy(Expr expr) - { - return new UnaryMinusExpr(expr); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - ExprEval ret = expr.eval(bindings); - if (NullHandling.sqlCompatible() && (ret.value() == null)) { - return ExprEval.of(null); - } - if (ret.type() == ExprType.LONG) { - return ExprEval.of(-ret.asLong()); - } - if (ret.type() == ExprType.DOUBLE) { - return ExprEval.of(-ret.asDouble()); - } - throw new IAE("unsupported type " + ret.type()); - } - - @Override - public String stringify() - { - return StringUtils.format("-%s", expr.stringify()); - } - - @Override - public String toString() - { - return StringUtils.format("-%s", expr); - } -} - -class UnaryNotExpr extends UnaryExpr -{ - UnaryNotExpr(Expr expr) - { - super(expr); - } - - @Override - UnaryExpr copy(Expr expr) - { - return new UnaryNotExpr(expr); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - ExprEval ret = expr.eval(bindings); - if (NullHandling.sqlCompatible() && (ret.value() == null)) { - return ExprEval.of(null); - } - // conforming to other boolean-returning binary operators - ExprType retType = ret.type() == ExprType.DOUBLE ? ExprType.DOUBLE : ExprType.LONG; - return ExprEval.of(!ret.asBoolean(), retType); - } - - @Override - public String stringify() - { - return StringUtils.format("!%s", expr.stringify()); - } - - @Override - public String toString() - { - return StringUtils.format("!%s", expr); - } -} - -/** - * Base type for all binary operators, this {@link Expr} has two children {@link Expr} for the left and right side - * operands. - * - * Note: all concrete subclass of this should have constructor with the form of (String, Expr, Expr) - * if it's not possible, just be sure Evals.binaryOp() can handle that - */ -abstract class BinaryOpExprBase implements Expr -{ - protected final String op; - protected final Expr left; - protected final Expr right; - - BinaryOpExprBase(String op, Expr left, Expr right) - { - this.op = op; - this.left = left; - this.right = right; - } - - @Override - public void visit(Visitor visitor) - { - left.visit(visitor); - right.visit(visitor); - visitor.visit(this); - } - - @Override - public Expr visit(Shuttle shuttle) - { - Expr newLeft = left.visit(shuttle); - Expr newRight = right.visit(shuttle); - //noinspection ObjectEquality (checking for object equality here is intentional) - if (left != newLeft || right != newRight) { - return shuttle.visit(copy(newLeft, newRight)); - } - return shuttle.visit(this); - } - - @Override - public String toString() - { - return StringUtils.format("(%s %s %s)", op, left, right); - } - - @Override - public String stringify() - { - return StringUtils.format("(%s %s %s)", left.stringify(), op, right.stringify()); - } - - protected abstract BinaryOpExprBase copy(Expr left, Expr right); - - @Override - public BindingDetails analyzeInputs() - { - // currently all binary operators operate on scalar inputs - return left.analyzeInputs().with(right).withScalarArguments(ImmutableSet.of(left, right)); - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - BinaryOpExprBase that = (BinaryOpExprBase) o; - return Objects.equals(op, that.op) && - Objects.equals(left, that.left) && - Objects.equals(right, that.right); - } - - @Override - public int hashCode() - { - return Objects.hash(op, left, right); - } -} - -/** - * Base class for numerical binary operators, with additional methods defined to evaluate primitive values directly - * instead of wrapped with {@link ExprEval} - */ -abstract class BinaryEvalOpExprBase extends BinaryOpExprBase -{ - BinaryEvalOpExprBase(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - ExprEval leftVal = left.eval(bindings); - ExprEval rightVal = right.eval(bindings); - - // Result of any Binary expressions is null if any of the argument is null. - // e.g "select null * 2 as c;" or "select null + 1 as c;" will return null as per Standard SQL spec. - if (NullHandling.sqlCompatible() && (leftVal.value() == null || rightVal.value() == null)) { - return ExprEval.of(null); - } - - if (leftVal.type() == ExprType.STRING && rightVal.type() == ExprType.STRING) { - return evalString(leftVal.asString(), rightVal.asString()); - } else if (leftVal.type() == ExprType.LONG && rightVal.type() == ExprType.LONG) { - if (NullHandling.sqlCompatible() && (leftVal.isNumericNull() || rightVal.isNumericNull())) { - return ExprEval.of(null); - } - return ExprEval.of(evalLong(leftVal.asLong(), rightVal.asLong())); - } else { - if (NullHandling.sqlCompatible() && (leftVal.isNumericNull() || rightVal.isNumericNull())) { - return ExprEval.of(null); - } - return ExprEval.of(evalDouble(leftVal.asDouble(), rightVal.asDouble())); - } - } - - protected ExprEval evalString(@Nullable String left, @Nullable String right) - { - throw new IllegalArgumentException("unsupported type " + ExprType.STRING); - } - - protected abstract long evalLong(long left, long right); - - protected abstract double evalDouble(double left, double right); -} - -class BinMinusExpr extends BinaryEvalOpExprBase -{ - BinMinusExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinMinusExpr(op, left, right); - } - - @Override - protected final long evalLong(long left, long right) - { - return left - right; - } - - @Override - protected final double evalDouble(double left, double right) - { - return left - right; - } -} - -class BinPowExpr extends BinaryEvalOpExprBase -{ - BinPowExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinPowExpr(op, left, right); - } - - @Override - protected final long evalLong(long left, long right) - { - return LongMath.pow(left, Ints.checkedCast(right)); - } - - @Override - protected final double evalDouble(double left, double right) - { - return Math.pow(left, right); - } -} - -class BinMulExpr extends BinaryEvalOpExprBase -{ - BinMulExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinMulExpr(op, left, right); - } - - @Override - protected final long evalLong(long left, long right) - { - return left * right; - } - - @Override - protected final double evalDouble(double left, double right) - { - return left * right; - } -} - -class BinDivExpr extends BinaryEvalOpExprBase -{ - BinDivExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinDivExpr(op, left, right); - } - - @Override - protected final long evalLong(long left, long right) - { - return left / right; - } - - @Override - protected final double evalDouble(double left, double right) - { - return left / right; - } -} - -class BinModuloExpr extends BinaryEvalOpExprBase -{ - BinModuloExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinModuloExpr(op, left, right); - } - - @Override - protected final long evalLong(long left, long right) - { - return left % right; - } - - @Override - protected final double evalDouble(double left, double right) - { - return left % right; - } -} - -class BinPlusExpr extends BinaryEvalOpExprBase -{ - BinPlusExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinPlusExpr(op, left, right); - } - - @Override - protected ExprEval evalString(@Nullable String left, @Nullable String right) - { - return ExprEval.of(NullHandling.nullToEmptyIfNeeded(left) - + NullHandling.nullToEmptyIfNeeded(right)); - } - - @Override - protected final long evalLong(long left, long right) - { - return left + right; - } - - @Override - protected final double evalDouble(double left, double right) - { - return left + right; - } -} - -class BinLtExpr extends BinaryEvalOpExprBase -{ - BinLtExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinLtExpr(op, left, right); - } - - @Override - protected ExprEval evalString(@Nullable String left, @Nullable String right) - { - return ExprEval.of(Comparators.naturalNullsFirst().compare(left, right) < 0, ExprType.LONG); - } - - @Override - protected final long evalLong(long left, long right) - { - return Evals.asLong(left < right); - } - - @Override - protected final double evalDouble(double left, double right) - { - // Use Double.compare for more consistent NaN handling. - return Evals.asDouble(Double.compare(left, right) < 0); - } -} - -class BinLeqExpr extends BinaryEvalOpExprBase -{ - BinLeqExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinLeqExpr(op, left, right); - } - - @Override - protected ExprEval evalString(@Nullable String left, @Nullable String right) - { - return ExprEval.of(Comparators.naturalNullsFirst().compare(left, right) <= 0, ExprType.LONG); - } - - @Override - protected final long evalLong(long left, long right) - { - return Evals.asLong(left <= right); - } - - @Override - protected final double evalDouble(double left, double right) - { - // Use Double.compare for more consistent NaN handling. - return Evals.asDouble(Double.compare(left, right) <= 0); - } -} - -class BinGtExpr extends BinaryEvalOpExprBase -{ - BinGtExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinGtExpr(op, left, right); - } - - @Override - protected ExprEval evalString(@Nullable String left, @Nullable String right) - { - return ExprEval.of(Comparators.naturalNullsFirst().compare(left, right) > 0, ExprType.LONG); - } - - @Override - protected final long evalLong(long left, long right) - { - return Evals.asLong(left > right); - } - - @Override - protected final double evalDouble(double left, double right) - { - // Use Double.compare for more consistent NaN handling. - return Evals.asDouble(Double.compare(left, right) > 0); - } -} - -class BinGeqExpr extends BinaryEvalOpExprBase -{ - BinGeqExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinGeqExpr(op, left, right); - } - - @Override - protected ExprEval evalString(@Nullable String left, @Nullable String right) - { - return ExprEval.of(Comparators.naturalNullsFirst().compare(left, right) >= 0, ExprType.LONG); - } - - @Override - protected final long evalLong(long left, long right) - { - return Evals.asLong(left >= right); - } - - @Override - protected final double evalDouble(double left, double right) - { - // Use Double.compare for more consistent NaN handling. - return Evals.asDouble(Double.compare(left, right) >= 0); - } -} - -class BinEqExpr extends BinaryEvalOpExprBase -{ - BinEqExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinEqExpr(op, left, right); - } - - @Override - protected ExprEval evalString(@Nullable String left, @Nullable String right) - { - return ExprEval.of(Objects.equals(left, right), ExprType.LONG); - } - - @Override - protected final long evalLong(long left, long right) - { - return Evals.asLong(left == right); - } - - @Override - protected final double evalDouble(double left, double right) - { - return Evals.asDouble(left == right); - } -} - -class BinNeqExpr extends BinaryEvalOpExprBase -{ - BinNeqExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinNeqExpr(op, left, right); - } - - @Override - protected ExprEval evalString(@Nullable String left, @Nullable String right) - { - return ExprEval.of(!Objects.equals(left, right), ExprType.LONG); - } - - @Override - protected final long evalLong(long left, long right) - { - return Evals.asLong(left != right); - } - - @Override - protected final double evalDouble(double left, double right) - { - return Evals.asDouble(left != right); - } -} - -class BinAndExpr extends BinaryOpExprBase -{ - BinAndExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinAndExpr(op, left, right); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - ExprEval leftVal = left.eval(bindings); - return leftVal.asBoolean() ? right.eval(bindings) : leftVal; - } -} - -class BinOrExpr extends BinaryOpExprBase -{ - BinOrExpr(String op, Expr left, Expr right) - { - super(op, left, right); - } - - @Override - protected BinaryOpExprBase copy(Expr left, Expr right) - { - return new BinOrExpr(op, left, right); - } - - @Override - public ExprEval eval(ObjectBinding bindings) - { - ExprEval leftVal = left.eval(bindings); - return leftVal.asBoolean() ? leftVal : right.eval(bindings); - } - -} - diff --git a/core/src/main/java/org/apache/druid/math/expr/FunctionalExpr.java b/core/src/main/java/org/apache/druid/math/expr/FunctionalExpr.java new file mode 100644 index 00000000000..2b3474a6c82 --- /dev/null +++ b/core/src/main/java/org/apache/druid/math/expr/FunctionalExpr.java @@ -0,0 +1,334 @@ +/* + * 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.druid.math.expr; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import org.apache.druid.java.util.common.StringUtils; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +class LambdaExpr implements Expr +{ + private final ImmutableList args; + private final Expr expr; + + LambdaExpr(List args, Expr expr) + { + this.args = ImmutableList.copyOf(args); + this.expr = expr; + } + + @Override + public String toString() + { + return StringUtils.format("(%s -> %s)", args, expr); + } + + int identifierCount() + { + return args.size(); + } + + @Nullable + public String getIdentifier() + { + Preconditions.checkState(args.size() < 2, "LambdaExpr has multiple arguments"); + if (args.size() == 1) { + return args.get(0).toString(); + } + return null; + } + + public List getIdentifiers() + { + return args.stream().map(IdentifierExpr::toString).collect(Collectors.toList()); + } + + ImmutableList getIdentifierExprs() + { + return args; + } + + public Expr getExpr() + { + return expr; + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + return expr.eval(bindings); + } + + @Override + public String stringify() + { + return StringUtils.format("(%s) -> %s", ARG_JOINER.join(getIdentifiers()), expr.stringify()); + } + + @Override + public void visit(Visitor visitor) + { + expr.visit(visitor); + visitor.visit(this); + } + + @Override + public Expr visit(Shuttle shuttle) + { + List newArgs = + args.stream().map(arg -> (IdentifierExpr) shuttle.visit(arg)).collect(Collectors.toList()); + Expr newBody = expr.visit(shuttle); + return shuttle.visit(new LambdaExpr(newArgs, newBody)); + } + + @Override + public BindingDetails analyzeInputs() + { + final Set lambdaArgs = args.stream().map(IdentifierExpr::toString).collect(Collectors.toSet()); + BindingDetails bodyDetails = expr.analyzeInputs(); + return bodyDetails.removeLambdaArguments(lambdaArgs); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LambdaExpr that = (LambdaExpr) o; + return Objects.equals(args, that.args) && + Objects.equals(expr, that.expr); + } + + @Override + public int hashCode() + { + return Objects.hash(args, expr); + } +} + +/** + * {@link Expr} node for a {@link Function} call. {@link FunctionExpr} has children {@link Expr} in the form of the + * list of arguments that are passed to the {@link Function} along with the {@link Expr.ObjectBinding} when it is + * evaluated. + */ +class FunctionExpr implements Expr +{ + final Function function; + final ImmutableList args; + private final String name; + + FunctionExpr(Function function, String name, List args) + { + this.function = function; + this.name = name; + this.args = ImmutableList.copyOf(args); + function.validateArguments(args); + } + + @Override + public String toString() + { + return StringUtils.format("(%s %s)", name, args); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + return function.apply(args, bindings); + } + + @Override + public String stringify() + { + return StringUtils.format("%s(%s)", name, ARG_JOINER.join(args.stream().map(Expr::stringify).iterator())); + } + + @Override + public void visit(Visitor visitor) + { + for (Expr child : args) { + child.visit(visitor); + } + visitor.visit(this); + } + + @Override + public Expr visit(Shuttle shuttle) + { + List newArgs = args.stream().map(shuttle::visit).collect(Collectors.toList()); + return shuttle.visit(new FunctionExpr(function, name, newArgs)); + } + + @Override + public BindingDetails analyzeInputs() + { + BindingDetails accumulator = new BindingDetails(); + + for (Expr arg : args) { + accumulator = accumulator.with(arg); + } + return accumulator.withScalarArguments(function.getScalarInputs(args)) + .withArrayArguments(function.getArrayInputs(args)) + .withArrayInputs(function.hasArrayInputs()) + .withArrayOutput(function.hasArrayOutput()); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FunctionExpr that = (FunctionExpr) o; + return args.equals(that.args) && + name.equals(that.name); + } + + @Override + public int hashCode() + { + return Objects.hash(args, name); + } +} + +/** + * This {@link Expr} node is representative of an {@link ApplyFunction}, and has children in the form of a + * {@link LambdaExpr} and the list of {@link Expr} arguments that are combined with {@link Expr.ObjectBinding} to + * evaluate the {@link LambdaExpr}. + */ +class ApplyFunctionExpr implements Expr +{ + final ApplyFunction function; + final String name; + final LambdaExpr lambdaExpr; + final ImmutableList argsExpr; + final BindingDetails bindingDetails; + final BindingDetails lambdaBindingDetails; + final ImmutableList argsBindingDetails; + + ApplyFunctionExpr(ApplyFunction function, String name, LambdaExpr expr, List args) + { + this.function = function; + this.name = name; + this.argsExpr = ImmutableList.copyOf(args); + this.lambdaExpr = expr; + + function.validateArguments(expr, args); + + // apply function expressions are examined during expression selector creation, so precompute and cache the + // binding details of children + ImmutableList.Builder argBindingDetailsBuilder = ImmutableList.builder(); + BindingDetails accumulator = new BindingDetails(); + for (Expr arg : argsExpr) { + BindingDetails argDetails = arg.analyzeInputs(); + argBindingDetailsBuilder.add(argDetails); + accumulator = accumulator.with(argDetails); + } + + lambdaBindingDetails = lambdaExpr.analyzeInputs(); + + bindingDetails = accumulator.with(lambdaBindingDetails) + .withArrayArguments(function.getArrayInputs(argsExpr)) + .withArrayInputs(true) + .withArrayOutput(function.hasArrayOutput(lambdaExpr)); + argsBindingDetails = argBindingDetailsBuilder.build(); + } + + @Override + public String toString() + { + return StringUtils.format("(%s %s, %s)", name, lambdaExpr, argsExpr); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + return function.apply(lambdaExpr, argsExpr, bindings); + } + + @Override + public String stringify() + { + return StringUtils.format( + "%s(%s, %s)", + name, + lambdaExpr.stringify(), + ARG_JOINER.join(argsExpr.stream().map(Expr::stringify).iterator()) + ); + } + + @Override + public void visit(Visitor visitor) + { + lambdaExpr.visit(visitor); + for (Expr arg : argsExpr) { + arg.visit(visitor); + } + visitor.visit(this); + } + + @Override + public Expr visit(Shuttle shuttle) + { + LambdaExpr newLambda = (LambdaExpr) lambdaExpr.visit(shuttle); + List newArgs = argsExpr.stream().map(shuttle::visit).collect(Collectors.toList()); + return shuttle.visit(new ApplyFunctionExpr(function, name, newLambda, newArgs)); + } + + @Override + public BindingDetails analyzeInputs() + { + return bindingDetails; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ApplyFunctionExpr that = (ApplyFunctionExpr) o; + return name.equals(that.name) && + lambdaExpr.equals(that.lambdaExpr) && + argsExpr.equals(that.argsExpr); + } + + @Override + public int hashCode() + { + return Objects.hash(name, lambdaExpr, argsExpr); + } +} diff --git a/core/src/main/java/org/apache/druid/math/expr/IdentifierExpr.java b/core/src/main/java/org/apache/druid/math/expr/IdentifierExpr.java new file mode 100644 index 00000000000..d23657a3bd9 --- /dev/null +++ b/core/src/main/java/org/apache/druid/math/expr/IdentifierExpr.java @@ -0,0 +1,153 @@ +/* + * 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.druid.math.expr; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.druid.java.util.common.StringUtils; + +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * This {@link Expr} node is used to represent a variable in the expression language. At evaluation time, the string + * identifier will be used to retrieve the runtime value for the variable from {@link Expr.ObjectBinding}. + * {@link IdentifierExpr} are terminal nodes of an expression tree, and have no children {@link Expr}. + */ +class IdentifierExpr implements Expr +{ + private final String identifier; + private final String binding; + + /** + * Construct a identifier expression for a {@link LambdaExpr}, where the {@link #identifier} is equal to + * {@link #binding} + */ + IdentifierExpr(String value) + { + this.identifier = value; + this.binding = value; + } + + /** + * Construct a normal identifier expression, where {@link #binding} is the key to fetch the backing value from + * {@link Expr.ObjectBinding} and the {@link #identifier} is a unique string that identifies this usage of the + * binding. + */ + IdentifierExpr(String identifier, String binding) + { + this.identifier = identifier; + this.binding = binding; + } + + @Override + public String toString() + { + return binding; + } + + /** + * Unique identifier for the binding + */ + @Nullable + public String getIdentifier() + { + return identifier; + } + + /** + * Value binding, key to retrieve value from {@link Expr.ObjectBinding#get(String)} + */ + @Nullable + public String getBinding() + { + return binding; + } + + @Nullable + @Override + public String getIdentifierIfIdentifier() + { + return identifier; + } + + @Nullable + @Override + public String getBindingIfIdentifier() + { + return binding; + } + + @Nullable + @Override + public IdentifierExpr getIdentifierExprIfIdentifierExpr() + { + return this; + } + + @Override + public BindingDetails analyzeInputs() + { + return new BindingDetails(this); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + return ExprEval.bestEffortOf(bindings.get(binding)); + } + + @Override + public String stringify() + { + // escape as java strings since identifiers are wrapped in double quotes + return StringUtils.format("\"%s\"", StringEscapeUtils.escapeJava(binding)); + } + + @Override + public void visit(Visitor visitor) + { + visitor.visit(this); + } + + @Override + public Expr visit(Shuttle shuttle) + { + return shuttle.visit(this); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + IdentifierExpr that = (IdentifierExpr) o; + return Objects.equals(identifier, that.identifier); + } + + @Override + public int hashCode() + { + return Objects.hash(identifier); + } +} diff --git a/core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java b/core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java new file mode 100644 index 00000000000..5a41e904250 --- /dev/null +++ b/core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java @@ -0,0 +1,166 @@ +/* + * 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.druid.math.expr; + +import com.google.common.collect.ImmutableSet; +import org.apache.druid.common.config.NullHandling; +import org.apache.druid.java.util.common.IAE; +import org.apache.druid.java.util.common.StringUtils; + +import java.util.Objects; + +/** + * Base type for all single argument operators, with a single {@link Expr} child for the operand. + */ +abstract class UnaryExpr implements Expr +{ + final Expr expr; + + UnaryExpr(Expr expr) + { + this.expr = expr; + } + + abstract UnaryExpr copy(Expr expr); + + @Override + public void visit(Visitor visitor) + { + expr.visit(visitor); + visitor.visit(this); + } + + @Override + public Expr visit(Shuttle shuttle) + { + Expr newExpr = expr.visit(shuttle); + //noinspection ObjectEquality (checking for object equality here is intentional) + if (newExpr != expr) { + return shuttle.visit(copy(newExpr)); + } + return shuttle.visit(this); + } + + @Override + public BindingDetails analyzeInputs() + { + // currently all unary operators only operate on scalar inputs + return expr.analyzeInputs().withScalarArguments(ImmutableSet.of(expr)); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UnaryExpr unaryExpr = (UnaryExpr) o; + return Objects.equals(expr, unaryExpr.expr); + } + + @Override + public int hashCode() + { + return Objects.hash(expr); + } +} + +class UnaryMinusExpr extends UnaryExpr +{ + UnaryMinusExpr(Expr expr) + { + super(expr); + } + + @Override + UnaryExpr copy(Expr expr) + { + return new UnaryMinusExpr(expr); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + ExprEval ret = expr.eval(bindings); + if (NullHandling.sqlCompatible() && (ret.value() == null)) { + return ExprEval.of(null); + } + if (ret.type() == ExprType.LONG) { + return ExprEval.of(-ret.asLong()); + } + if (ret.type() == ExprType.DOUBLE) { + return ExprEval.of(-ret.asDouble()); + } + throw new IAE("unsupported type " + ret.type()); + } + + @Override + public String stringify() + { + return StringUtils.format("-%s", expr.stringify()); + } + + @Override + public String toString() + { + return StringUtils.format("-%s", expr); + } +} + +class UnaryNotExpr extends UnaryExpr +{ + UnaryNotExpr(Expr expr) + { + super(expr); + } + + @Override + UnaryExpr copy(Expr expr) + { + return new UnaryNotExpr(expr); + } + + @Override + public ExprEval eval(ObjectBinding bindings) + { + ExprEval ret = expr.eval(bindings); + if (NullHandling.sqlCompatible() && (ret.value() == null)) { + return ExprEval.of(null); + } + // conforming to other boolean-returning binary operators + ExprType retType = ret.type() == ExprType.DOUBLE ? ExprType.DOUBLE : ExprType.LONG; + return ExprEval.of(!ret.asBoolean(), retType); + } + + @Override + public String stringify() + { + return StringUtils.format("!%s", expr.stringify()); + } + + @Override + public String toString() + { + return StringUtils.format("!%s", expr); + } +}