clintropolis commented on a change in pull request #10370:
URL: https://github.com/apache/druid/pull/10370#discussion_r486756009



##########
File path: core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java
##########
@@ -35,6 +35,19 @@
  */
 abstract class ConstantExpr implements Expr
 {
+  final ExprType outputType;
+
+  protected ConstantExpr(ExprType outputType)
+  {
+    this.outputType = outputType;
+  }
+
+  @Override

Review comment:
       Oops, I didn't actually mean to make this not be `@Nullable`, though I 
guess it is true, but will fix to just be consistent with the interface.

##########
File path: core/src/main/java/org/apache/druid/math/expr/Expr.java
##########
@@ -116,16 +116,39 @@ default String getBindingIfIdentifier()
   void visit(Visitor visitor);
 
   /**
-   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}.Each 
{@link Expr} is responsible for
+   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}. 
Each {@link Expr} is responsible for
    * ensuring the {@link Shuttle} can visit all of its {@link Expr} children, 
as well as updating its children
    * {@link Expr} with the results from the {@link Shuttle}, before finally 
visiting an updated form of itself.
    */
   Expr visit(Shuttle shuttle);
 
   /**
-   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingDetails}
+   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingAnalysis}
    */
-  BindingDetails analyzeInputs();
+  BindingAnalysis analyzeInputs();
+
+  /**
+   * Given an {@link InputBindingTypes}, compute what the output {@link 
ExprType} will be for this expression. A return
+   * value of null indicates that the given type information was not enough to 
resolve the output type, so the
+   * expression must be evaluated using default {@link #eval} handling where 
types are only known after evaluation,
+   * through {@link ExprEval#type}.
+   */
+  @Nullable
+  default ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    return null;
+  }
+
+  /**
+   * Mechanism to supply input types for the bindings which will back {@link 
IdentifierExpr}, to use in the aid of
+   * inferring the output type of an expression with {@link #getOutputType}. A 
null value means that either the binding

Review comment:
       not long term probably, but currently this will be backed by 
`ColumnCapabilities` from a `ColumnInspector`, which does not currently 
distinguish between those 2 things for all implementations.

##########
File path: core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java
##########
@@ -163,4 +171,15 @@ public String toString()
   {
     return StringUtils.format("!%s", expr);
   }
+
+  @Nullable
+  @Override
+  public ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    ExprType implicitCast = super.getOutputType(inputTypes);
+    if (ExprType.STRING.equals(implicitCast)) {
+      return ExprType.LONG;
+    }

Review comment:
       I don't know off the top of my head why it does that, I was just trying 
to model its output. The binary logic expressions do that too:
   ```
     @Override
     protected ExprEval evalString(@Nullable String left, @Nullable String 
right)
     {
       return 
ExprEval.ofLongBoolean(Comparators.<String>naturalNullsFirst().compare(left, 
right) < 0);
     }
   ```
   so it is just being consistent with those operators I think.
   
   I'm moderately in the camp of wondering why these logical operators return 
anything other than long for any input type, but I haven't thought fully 
through on the implications of changing that yet, so maybe there is a reason 
that the type is preserved for doubles in these operators.

##########
File path: 
processing/src/main/java/org/apache/druid/query/expression/TimestampCeilExprMacro.java
##########
@@ -93,6 +95,13 @@ public Expr visit(Shuttle shuttle)
       return shuttle.visit(new TimestampCeilExpr(newArgs));
     }
 
+    @Nullable
+    @Override
+    public ExprType getOutputType(InputBindingTypes inputTypes)
+    {
+      return ExprType.LONG;

Review comment:
       Hmm, so numeric null handling should be done currently be done using 
`ExprEval.isNumericNull`, which will return true even if the type is a null 
string from a `StringExprEval`. So i think 'string' is technically correct, but 
I don't think spiritually or functionally quite true, nor that useful. We 
probably should modify these `ExprEval.of(null)` to make type correct 
`ExprEval` to make `eval` match exactly what `getOutputType` says, but I don't 
know if this PR is the correct place to do that.

##########
File path: 
server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java
##########
@@ -518,7 +518,7 @@ private void initializeLookupsConfigWatcher()
         configManager.set(
             LOOKUP_CONFIG_KEY,
             converted,
-            new AuditInfo("autoConversion", "autoConversion", "127.0.0.1")
+            new AuditInfo("autoTypeConversion", "autoTypeConversion", 
"127.0.0.1")

Review comment:
       oops, intellij got a bit aggro

##########
File path: core/src/main/java/org/apache/druid/math/expr/ExprType.java
##########
@@ -29,5 +35,116 @@
   STRING,
   DOUBLE_ARRAY,
   LONG_ARRAY,
-  STRING_ARRAY
+  STRING_ARRAY;
+
+  public boolean isNumeric()
+  {
+    return isNumeric(this);
+  }
+
+  /**
+   * The expression system does not distinguish between {@link 
ValueType#FLOAT} and {@link ValueType#DOUBLE}, and
+   * cannot currently handle {@link ValueType#COMPLEX} inputs. This method 
will convert {@link ValueType#FLOAT} to
+   * {@link #DOUBLE}, or throw an exception if a {@link ValueType#COMPLEX} is 
encountered.
+   *
+   * @throws IllegalStateException
+   */
+  public static ExprType fromValueType(@Nullable ValueType valueType)
+  {
+    if (valueType == null) {
+      throw new IllegalStateException("Unsupported unknown value type");
+    }
+    switch (valueType) {
+      case LONG:
+        return LONG;
+      case LONG_ARRAY:
+        return LONG_ARRAY;
+      case FLOAT:
+      case DOUBLE:
+        return DOUBLE;
+      case DOUBLE_ARRAY:
+        return DOUBLE_ARRAY;
+      case STRING:
+        return STRING;
+      case STRING_ARRAY:
+        return STRING_ARRAY;
+      case COMPLEX:
+      default:
+        throw new ISE("Unsupported value type[%s]", valueType);
+    }
+  }
+
+  public static boolean isNumeric(ExprType type)
+  {
+    return LONG.equals(type) || DOUBLE.equals(type);
+  }
+
+  public static boolean isArray(@Nullable ExprType type)
+  {
+    return LONG_ARRAY.equals(type) || DOUBLE_ARRAY.equals(type) || 
STRING_ARRAY.equals(type);
+  }
+
+  @Nullable
+  public static ExprType elementType(@Nullable ExprType type)
+  {
+    if (type != null && isArray(type)) {
+      switch (type) {
+        case STRING_ARRAY:
+          return STRING;
+        case LONG_ARRAY:
+          return LONG;
+        case DOUBLE_ARRAY:
+          return DOUBLE;
+      }
+    }
+    return type;
+  }
+
+  @Nullable
+  public static ExprType asArrayType(@Nullable ExprType elementType)
+  {
+    if (elementType != null && !isArray(elementType)) {
+      switch (elementType) {
+        case STRING:
+          return STRING_ARRAY;
+        case LONG:
+          return LONG_ARRAY;
+        case DOUBLE:
+          return DOUBLE_ARRAY;
+      }
+    }
+    return elementType;
+  }
+
+  @Nullable
+  public static ExprType implicitCast(@Nullable ExprType type, @Nullable 
ExprType other)
+  {
+    if (type == null || other == null) {
+      // cannot implicitly cast unknown types
+      return null;
+    }
+    // arrays cannot be implicitly cast
+    if (isArray(type)) {
+      if (!type.equals(other)) {
+        throw new IAE("Cannot implicitly cast %s to %s", type, other);
+      }
+      return type;
+    }
+    // if either argument is a string, type becomes a string
+    if (STRING.equals(type) || STRING.equals(other)) {
+      return STRING;
+    }
+
+    if (isNumeric(type) && isNumeric(other)) {
+      // all numbers win over longs
+      if (LONG.equals(type)) {
+        return other;
+      }
+      // floats vs longs would be handled here, but we currently only support 
doubles...
+      return type;
+    }
+
+    // unhandled is unknown
+    return null;

Review comment:
       >As you said the code is volatile so chances of this path being hit are 
even higher.
   
   I mean, it's totally not possible to hit this null because there isn't a 
`ExprType` combination of input arguments that ends up on this line. By 
volatile I meant that this enum and file is likely going to go away and maybe 
this function migrated into `ValueType`. This line is essentially a placeholder 
for `ValueType.COMPLEX` which doesn't exist in `ExprType`, so I was shaping 
this method to be straightforward to work off of that enum instead of this one 
someday. 
   
   I can throw an exception with the messaging 'impossible' if you would 
prefer, but there isn't a way to actually check it with a test.

##########
File path: core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java
##########
@@ -35,6 +35,19 @@
  */
 abstract class ConstantExpr implements Expr
 {
+  final ExprType outputType;
+
+  protected ConstantExpr(ExprType outputType)
+  {
+    this.outputType = outputType;
+  }
+
+  @Override

Review comment:
       Oops, I didn't actually mean to make this not be `@Nullable`, though I 
guess it is true, but will fix to just be consistent with the interface.

##########
File path: core/src/main/java/org/apache/druid/math/expr/Expr.java
##########
@@ -116,16 +116,39 @@ default String getBindingIfIdentifier()
   void visit(Visitor visitor);
 
   /**
-   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}.Each 
{@link Expr} is responsible for
+   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}. 
Each {@link Expr} is responsible for
    * ensuring the {@link Shuttle} can visit all of its {@link Expr} children, 
as well as updating its children
    * {@link Expr} with the results from the {@link Shuttle}, before finally 
visiting an updated form of itself.
    */
   Expr visit(Shuttle shuttle);
 
   /**
-   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingDetails}
+   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingAnalysis}
    */
-  BindingDetails analyzeInputs();
+  BindingAnalysis analyzeInputs();
+
+  /**
+   * Given an {@link InputBindingTypes}, compute what the output {@link 
ExprType} will be for this expression. A return
+   * value of null indicates that the given type information was not enough to 
resolve the output type, so the
+   * expression must be evaluated using default {@link #eval} handling where 
types are only known after evaluation,
+   * through {@link ExprEval#type}.
+   */
+  @Nullable
+  default ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    return null;
+  }
+
+  /**
+   * Mechanism to supply input types for the bindings which will back {@link 
IdentifierExpr}, to use in the aid of
+   * inferring the output type of an expression with {@link #getOutputType}. A 
null value means that either the binding

Review comment:
       not long term probably, but currently this will be backed by 
`ColumnCapabilities` from a `ColumnInspector`, which does not currently 
distinguish between those 2 things for all implementations.

##########
File path: core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java
##########
@@ -163,4 +171,15 @@ public String toString()
   {
     return StringUtils.format("!%s", expr);
   }
+
+  @Nullable
+  @Override
+  public ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    ExprType implicitCast = super.getOutputType(inputTypes);
+    if (ExprType.STRING.equals(implicitCast)) {
+      return ExprType.LONG;
+    }

Review comment:
       I don't know off the top of my head why it does that, I was just trying 
to model its output. The binary logic expressions do that too:
   ```
     @Override
     protected ExprEval evalString(@Nullable String left, @Nullable String 
right)
     {
       return 
ExprEval.ofLongBoolean(Comparators.<String>naturalNullsFirst().compare(left, 
right) < 0);
     }
   ```
   so it is just being consistent with those operators I think.
   
   I'm moderately in the camp of wondering why these logical operators return 
anything other than long for any input type, but I haven't thought fully 
through on the implications of changing that yet, so maybe there is a reason 
that the type is preserved for doubles in these operators.

##########
File path: 
processing/src/main/java/org/apache/druid/query/expression/TimestampCeilExprMacro.java
##########
@@ -93,6 +95,13 @@ public Expr visit(Shuttle shuttle)
       return shuttle.visit(new TimestampCeilExpr(newArgs));
     }
 
+    @Nullable
+    @Override
+    public ExprType getOutputType(InputBindingTypes inputTypes)
+    {
+      return ExprType.LONG;

Review comment:
       Hmm, so numeric null handling should be done currently be done using 
`ExprEval.isNumericNull`, which will return true even if the type is a null 
string from a `StringExprEval`. So i think 'string' is technically correct, but 
I don't think spiritually or functionally quite true, nor that useful. We 
probably should modify these `ExprEval.of(null)` to make type correct 
`ExprEval` to make `eval` match exactly what `getOutputType` says, but I don't 
know if this PR is the correct place to do that.

##########
File path: 
server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java
##########
@@ -518,7 +518,7 @@ private void initializeLookupsConfigWatcher()
         configManager.set(
             LOOKUP_CONFIG_KEY,
             converted,
-            new AuditInfo("autoConversion", "autoConversion", "127.0.0.1")
+            new AuditInfo("autoTypeConversion", "autoTypeConversion", 
"127.0.0.1")

Review comment:
       oops, intellij got a bit aggro

##########
File path: core/src/main/java/org/apache/druid/math/expr/ExprType.java
##########
@@ -29,5 +35,116 @@
   STRING,
   DOUBLE_ARRAY,
   LONG_ARRAY,
-  STRING_ARRAY
+  STRING_ARRAY;
+
+  public boolean isNumeric()
+  {
+    return isNumeric(this);
+  }
+
+  /**
+   * The expression system does not distinguish between {@link 
ValueType#FLOAT} and {@link ValueType#DOUBLE}, and
+   * cannot currently handle {@link ValueType#COMPLEX} inputs. This method 
will convert {@link ValueType#FLOAT} to
+   * {@link #DOUBLE}, or throw an exception if a {@link ValueType#COMPLEX} is 
encountered.
+   *
+   * @throws IllegalStateException
+   */
+  public static ExprType fromValueType(@Nullable ValueType valueType)
+  {
+    if (valueType == null) {
+      throw new IllegalStateException("Unsupported unknown value type");
+    }
+    switch (valueType) {
+      case LONG:
+        return LONG;
+      case LONG_ARRAY:
+        return LONG_ARRAY;
+      case FLOAT:
+      case DOUBLE:
+        return DOUBLE;
+      case DOUBLE_ARRAY:
+        return DOUBLE_ARRAY;
+      case STRING:
+        return STRING;
+      case STRING_ARRAY:
+        return STRING_ARRAY;
+      case COMPLEX:
+      default:
+        throw new ISE("Unsupported value type[%s]", valueType);
+    }
+  }
+
+  public static boolean isNumeric(ExprType type)
+  {
+    return LONG.equals(type) || DOUBLE.equals(type);
+  }
+
+  public static boolean isArray(@Nullable ExprType type)
+  {
+    return LONG_ARRAY.equals(type) || DOUBLE_ARRAY.equals(type) || 
STRING_ARRAY.equals(type);
+  }
+
+  @Nullable
+  public static ExprType elementType(@Nullable ExprType type)
+  {
+    if (type != null && isArray(type)) {
+      switch (type) {
+        case STRING_ARRAY:
+          return STRING;
+        case LONG_ARRAY:
+          return LONG;
+        case DOUBLE_ARRAY:
+          return DOUBLE;
+      }
+    }
+    return type;
+  }
+
+  @Nullable
+  public static ExprType asArrayType(@Nullable ExprType elementType)
+  {
+    if (elementType != null && !isArray(elementType)) {
+      switch (elementType) {
+        case STRING:
+          return STRING_ARRAY;
+        case LONG:
+          return LONG_ARRAY;
+        case DOUBLE:
+          return DOUBLE_ARRAY;
+      }
+    }
+    return elementType;
+  }
+
+  @Nullable
+  public static ExprType implicitCast(@Nullable ExprType type, @Nullable 
ExprType other)
+  {
+    if (type == null || other == null) {
+      // cannot implicitly cast unknown types
+      return null;
+    }
+    // arrays cannot be implicitly cast
+    if (isArray(type)) {
+      if (!type.equals(other)) {
+        throw new IAE("Cannot implicitly cast %s to %s", type, other);
+      }
+      return type;
+    }
+    // if either argument is a string, type becomes a string
+    if (STRING.equals(type) || STRING.equals(other)) {
+      return STRING;
+    }
+
+    if (isNumeric(type) && isNumeric(other)) {
+      // all numbers win over longs
+      if (LONG.equals(type)) {
+        return other;
+      }
+      // floats vs longs would be handled here, but we currently only support 
doubles...
+      return type;
+    }
+
+    // unhandled is unknown
+    return null;

Review comment:
       >As you said the code is volatile so chances of this path being hit are 
even higher.
   
   I mean, it's totally not possible to hit this null because there isn't a 
`ExprType` combination of input arguments that ends up on this line. By 
volatile I meant that this enum and file is likely going to go away and maybe 
this function migrated into `ValueType`. This line is essentially a placeholder 
for `ValueType.COMPLEX` which doesn't exist in `ExprType`, so I was shaping 
this method to be straightforward to work off of that enum instead of this one 
someday. 
   
   I can throw an exception with the messaging 'impossible' if you would 
prefer, but there isn't a way to actually check it with a test.

##########
File path: core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java
##########
@@ -35,6 +35,19 @@
  */
 abstract class ConstantExpr implements Expr
 {
+  final ExprType outputType;
+
+  protected ConstantExpr(ExprType outputType)
+  {
+    this.outputType = outputType;
+  }
+
+  @Override

Review comment:
       Oops, I didn't actually mean to make this not be `@Nullable`, though I 
guess it is true, but will fix to just be consistent with the interface.

##########
File path: core/src/main/java/org/apache/druid/math/expr/Expr.java
##########
@@ -116,16 +116,39 @@ default String getBindingIfIdentifier()
   void visit(Visitor visitor);
 
   /**
-   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}.Each 
{@link Expr} is responsible for
+   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}. 
Each {@link Expr} is responsible for
    * ensuring the {@link Shuttle} can visit all of its {@link Expr} children, 
as well as updating its children
    * {@link Expr} with the results from the {@link Shuttle}, before finally 
visiting an updated form of itself.
    */
   Expr visit(Shuttle shuttle);
 
   /**
-   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingDetails}
+   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingAnalysis}
    */
-  BindingDetails analyzeInputs();
+  BindingAnalysis analyzeInputs();
+
+  /**
+   * Given an {@link InputBindingTypes}, compute what the output {@link 
ExprType} will be for this expression. A return
+   * value of null indicates that the given type information was not enough to 
resolve the output type, so the
+   * expression must be evaluated using default {@link #eval} handling where 
types are only known after evaluation,
+   * through {@link ExprEval#type}.
+   */
+  @Nullable
+  default ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    return null;
+  }
+
+  /**
+   * Mechanism to supply input types for the bindings which will back {@link 
IdentifierExpr}, to use in the aid of
+   * inferring the output type of an expression with {@link #getOutputType}. A 
null value means that either the binding

Review comment:
       not long term probably, but currently this will be backed by 
`ColumnCapabilities` from a `ColumnInspector`, which does not currently 
distinguish between those 2 things for all implementations.

##########
File path: core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java
##########
@@ -163,4 +171,15 @@ public String toString()
   {
     return StringUtils.format("!%s", expr);
   }
+
+  @Nullable
+  @Override
+  public ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    ExprType implicitCast = super.getOutputType(inputTypes);
+    if (ExprType.STRING.equals(implicitCast)) {
+      return ExprType.LONG;
+    }

Review comment:
       I don't know off the top of my head why it does that, I was just trying 
to model its output. The binary logic expressions do that too:
   ```
     @Override
     protected ExprEval evalString(@Nullable String left, @Nullable String 
right)
     {
       return 
ExprEval.ofLongBoolean(Comparators.<String>naturalNullsFirst().compare(left, 
right) < 0);
     }
   ```
   so it is just being consistent with those operators I think.
   
   I'm moderately in the camp of wondering why these logical operators return 
anything other than long for any input type, but I haven't thought fully 
through on the implications of changing that yet, so maybe there is a reason 
that the type is preserved for doubles in these operators.

##########
File path: 
processing/src/main/java/org/apache/druid/query/expression/TimestampCeilExprMacro.java
##########
@@ -93,6 +95,13 @@ public Expr visit(Shuttle shuttle)
       return shuttle.visit(new TimestampCeilExpr(newArgs));
     }
 
+    @Nullable
+    @Override
+    public ExprType getOutputType(InputBindingTypes inputTypes)
+    {
+      return ExprType.LONG;

Review comment:
       Hmm, so numeric null handling should be done currently be done using 
`ExprEval.isNumericNull`, which will return true even if the type is a null 
string from a `StringExprEval`. So i think 'string' is technically correct, but 
I don't think spiritually or functionally quite true, nor that useful. We 
probably should modify these `ExprEval.of(null)` to make type correct 
`ExprEval` to make `eval` match exactly what `getOutputType` says, but I don't 
know if this PR is the correct place to do that.

##########
File path: 
server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java
##########
@@ -518,7 +518,7 @@ private void initializeLookupsConfigWatcher()
         configManager.set(
             LOOKUP_CONFIG_KEY,
             converted,
-            new AuditInfo("autoConversion", "autoConversion", "127.0.0.1")
+            new AuditInfo("autoTypeConversion", "autoTypeConversion", 
"127.0.0.1")

Review comment:
       oops, intellij got a bit aggro

##########
File path: core/src/main/java/org/apache/druid/math/expr/ExprType.java
##########
@@ -29,5 +35,116 @@
   STRING,
   DOUBLE_ARRAY,
   LONG_ARRAY,
-  STRING_ARRAY
+  STRING_ARRAY;
+
+  public boolean isNumeric()
+  {
+    return isNumeric(this);
+  }
+
+  /**
+   * The expression system does not distinguish between {@link 
ValueType#FLOAT} and {@link ValueType#DOUBLE}, and
+   * cannot currently handle {@link ValueType#COMPLEX} inputs. This method 
will convert {@link ValueType#FLOAT} to
+   * {@link #DOUBLE}, or throw an exception if a {@link ValueType#COMPLEX} is 
encountered.
+   *
+   * @throws IllegalStateException
+   */
+  public static ExprType fromValueType(@Nullable ValueType valueType)
+  {
+    if (valueType == null) {
+      throw new IllegalStateException("Unsupported unknown value type");
+    }
+    switch (valueType) {
+      case LONG:
+        return LONG;
+      case LONG_ARRAY:
+        return LONG_ARRAY;
+      case FLOAT:
+      case DOUBLE:
+        return DOUBLE;
+      case DOUBLE_ARRAY:
+        return DOUBLE_ARRAY;
+      case STRING:
+        return STRING;
+      case STRING_ARRAY:
+        return STRING_ARRAY;
+      case COMPLEX:
+      default:
+        throw new ISE("Unsupported value type[%s]", valueType);
+    }
+  }
+
+  public static boolean isNumeric(ExprType type)
+  {
+    return LONG.equals(type) || DOUBLE.equals(type);
+  }
+
+  public static boolean isArray(@Nullable ExprType type)
+  {
+    return LONG_ARRAY.equals(type) || DOUBLE_ARRAY.equals(type) || 
STRING_ARRAY.equals(type);
+  }
+
+  @Nullable
+  public static ExprType elementType(@Nullable ExprType type)
+  {
+    if (type != null && isArray(type)) {
+      switch (type) {
+        case STRING_ARRAY:
+          return STRING;
+        case LONG_ARRAY:
+          return LONG;
+        case DOUBLE_ARRAY:
+          return DOUBLE;
+      }
+    }
+    return type;
+  }
+
+  @Nullable
+  public static ExprType asArrayType(@Nullable ExprType elementType)
+  {
+    if (elementType != null && !isArray(elementType)) {
+      switch (elementType) {
+        case STRING:
+          return STRING_ARRAY;
+        case LONG:
+          return LONG_ARRAY;
+        case DOUBLE:
+          return DOUBLE_ARRAY;
+      }
+    }
+    return elementType;
+  }
+
+  @Nullable
+  public static ExprType implicitCast(@Nullable ExprType type, @Nullable 
ExprType other)
+  {
+    if (type == null || other == null) {
+      // cannot implicitly cast unknown types
+      return null;
+    }
+    // arrays cannot be implicitly cast
+    if (isArray(type)) {
+      if (!type.equals(other)) {
+        throw new IAE("Cannot implicitly cast %s to %s", type, other);
+      }
+      return type;
+    }
+    // if either argument is a string, type becomes a string
+    if (STRING.equals(type) || STRING.equals(other)) {
+      return STRING;
+    }
+
+    if (isNumeric(type) && isNumeric(other)) {
+      // all numbers win over longs
+      if (LONG.equals(type)) {
+        return other;
+      }
+      // floats vs longs would be handled here, but we currently only support 
doubles...
+      return type;
+    }
+
+    // unhandled is unknown
+    return null;

Review comment:
       >As you said the code is volatile so chances of this path being hit are 
even higher.
   
   I mean, it's totally not possible to hit this null because there isn't a 
`ExprType` combination of input arguments that ends up on this line. By 
volatile I meant that this enum and file is likely going to go away and maybe 
this function migrated into `ValueType`. This line is essentially a placeholder 
for `ValueType.COMPLEX` which doesn't exist in `ExprType`, so I was shaping 
this method to be straightforward to work off of that enum instead of this one 
someday. 
   
   I can throw an exception with the messaging 'impossible' if you would 
prefer, but there isn't a way to actually check it with a test.

##########
File path: core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java
##########
@@ -35,6 +35,19 @@
  */
 abstract class ConstantExpr implements Expr
 {
+  final ExprType outputType;
+
+  protected ConstantExpr(ExprType outputType)
+  {
+    this.outputType = outputType;
+  }
+
+  @Override

Review comment:
       Oops, I didn't actually mean to make this not be `@Nullable`, though I 
guess it is true, but will fix to just be consistent with the interface.

##########
File path: core/src/main/java/org/apache/druid/math/expr/Expr.java
##########
@@ -116,16 +116,39 @@ default String getBindingIfIdentifier()
   void visit(Visitor visitor);
 
   /**
-   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}.Each 
{@link Expr} is responsible for
+   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}. 
Each {@link Expr} is responsible for
    * ensuring the {@link Shuttle} can visit all of its {@link Expr} children, 
as well as updating its children
    * {@link Expr} with the results from the {@link Shuttle}, before finally 
visiting an updated form of itself.
    */
   Expr visit(Shuttle shuttle);
 
   /**
-   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingDetails}
+   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingAnalysis}
    */
-  BindingDetails analyzeInputs();
+  BindingAnalysis analyzeInputs();
+
+  /**
+   * Given an {@link InputBindingTypes}, compute what the output {@link 
ExprType} will be for this expression. A return
+   * value of null indicates that the given type information was not enough to 
resolve the output type, so the
+   * expression must be evaluated using default {@link #eval} handling where 
types are only known after evaluation,
+   * through {@link ExprEval#type}.
+   */
+  @Nullable
+  default ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    return null;
+  }
+
+  /**
+   * Mechanism to supply input types for the bindings which will back {@link 
IdentifierExpr}, to use in the aid of
+   * inferring the output type of an expression with {@link #getOutputType}. A 
null value means that either the binding

Review comment:
       not long term probably, but currently this will be backed by 
`ColumnCapabilities` from a `ColumnInspector`, which does not currently 
distinguish between those 2 things for all implementations.

##########
File path: core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java
##########
@@ -163,4 +171,15 @@ public String toString()
   {
     return StringUtils.format("!%s", expr);
   }
+
+  @Nullable
+  @Override
+  public ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    ExprType implicitCast = super.getOutputType(inputTypes);
+    if (ExprType.STRING.equals(implicitCast)) {
+      return ExprType.LONG;
+    }

Review comment:
       I don't know off the top of my head why it does that, I was just trying 
to model its output. The binary logic expressions do that too:
   ```
     @Override
     protected ExprEval evalString(@Nullable String left, @Nullable String 
right)
     {
       return 
ExprEval.ofLongBoolean(Comparators.<String>naturalNullsFirst().compare(left, 
right) < 0);
     }
   ```
   so it is just being consistent with those operators I think.
   
   I'm moderately in the camp of wondering why these logical operators return 
anything other than long for any input type, but I haven't thought fully 
through on the implications of changing that yet, so maybe there is a reason 
that the type is preserved for doubles in these operators.

##########
File path: 
processing/src/main/java/org/apache/druid/query/expression/TimestampCeilExprMacro.java
##########
@@ -93,6 +95,13 @@ public Expr visit(Shuttle shuttle)
       return shuttle.visit(new TimestampCeilExpr(newArgs));
     }
 
+    @Nullable
+    @Override
+    public ExprType getOutputType(InputBindingTypes inputTypes)
+    {
+      return ExprType.LONG;

Review comment:
       Hmm, so numeric null handling should be done currently be done using 
`ExprEval.isNumericNull`, which will return true even if the type is a null 
string from a `StringExprEval`. So i think 'string' is technically correct, but 
I don't think spiritually or functionally quite true, nor that useful. We 
probably should modify these `ExprEval.of(null)` to make type correct 
`ExprEval` to make `eval` match exactly what `getOutputType` says, but I don't 
know if this PR is the correct place to do that.

##########
File path: 
server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java
##########
@@ -518,7 +518,7 @@ private void initializeLookupsConfigWatcher()
         configManager.set(
             LOOKUP_CONFIG_KEY,
             converted,
-            new AuditInfo("autoConversion", "autoConversion", "127.0.0.1")
+            new AuditInfo("autoTypeConversion", "autoTypeConversion", 
"127.0.0.1")

Review comment:
       oops, intellij got a bit aggro

##########
File path: core/src/main/java/org/apache/druid/math/expr/ExprType.java
##########
@@ -29,5 +35,116 @@
   STRING,
   DOUBLE_ARRAY,
   LONG_ARRAY,
-  STRING_ARRAY
+  STRING_ARRAY;
+
+  public boolean isNumeric()
+  {
+    return isNumeric(this);
+  }
+
+  /**
+   * The expression system does not distinguish between {@link 
ValueType#FLOAT} and {@link ValueType#DOUBLE}, and
+   * cannot currently handle {@link ValueType#COMPLEX} inputs. This method 
will convert {@link ValueType#FLOAT} to
+   * {@link #DOUBLE}, or throw an exception if a {@link ValueType#COMPLEX} is 
encountered.
+   *
+   * @throws IllegalStateException
+   */
+  public static ExprType fromValueType(@Nullable ValueType valueType)
+  {
+    if (valueType == null) {
+      throw new IllegalStateException("Unsupported unknown value type");
+    }
+    switch (valueType) {
+      case LONG:
+        return LONG;
+      case LONG_ARRAY:
+        return LONG_ARRAY;
+      case FLOAT:
+      case DOUBLE:
+        return DOUBLE;
+      case DOUBLE_ARRAY:
+        return DOUBLE_ARRAY;
+      case STRING:
+        return STRING;
+      case STRING_ARRAY:
+        return STRING_ARRAY;
+      case COMPLEX:
+      default:
+        throw new ISE("Unsupported value type[%s]", valueType);
+    }
+  }
+
+  public static boolean isNumeric(ExprType type)
+  {
+    return LONG.equals(type) || DOUBLE.equals(type);
+  }
+
+  public static boolean isArray(@Nullable ExprType type)
+  {
+    return LONG_ARRAY.equals(type) || DOUBLE_ARRAY.equals(type) || 
STRING_ARRAY.equals(type);
+  }
+
+  @Nullable
+  public static ExprType elementType(@Nullable ExprType type)
+  {
+    if (type != null && isArray(type)) {
+      switch (type) {
+        case STRING_ARRAY:
+          return STRING;
+        case LONG_ARRAY:
+          return LONG;
+        case DOUBLE_ARRAY:
+          return DOUBLE;
+      }
+    }
+    return type;
+  }
+
+  @Nullable
+  public static ExprType asArrayType(@Nullable ExprType elementType)
+  {
+    if (elementType != null && !isArray(elementType)) {
+      switch (elementType) {
+        case STRING:
+          return STRING_ARRAY;
+        case LONG:
+          return LONG_ARRAY;
+        case DOUBLE:
+          return DOUBLE_ARRAY;
+      }
+    }
+    return elementType;
+  }
+
+  @Nullable
+  public static ExprType implicitCast(@Nullable ExprType type, @Nullable 
ExprType other)
+  {
+    if (type == null || other == null) {
+      // cannot implicitly cast unknown types
+      return null;
+    }
+    // arrays cannot be implicitly cast
+    if (isArray(type)) {
+      if (!type.equals(other)) {
+        throw new IAE("Cannot implicitly cast %s to %s", type, other);
+      }
+      return type;
+    }
+    // if either argument is a string, type becomes a string
+    if (STRING.equals(type) || STRING.equals(other)) {
+      return STRING;
+    }
+
+    if (isNumeric(type) && isNumeric(other)) {
+      // all numbers win over longs
+      if (LONG.equals(type)) {
+        return other;
+      }
+      // floats vs longs would be handled here, but we currently only support 
doubles...
+      return type;
+    }
+
+    // unhandled is unknown
+    return null;

Review comment:
       >As you said the code is volatile so chances of this path being hit are 
even higher.
   
   I mean, it's totally not possible to hit this null because there isn't a 
`ExprType` combination of input arguments that ends up on this line. By 
volatile I meant that this enum and file is likely going to go away and maybe 
this function migrated into `ValueType`. This line is essentially a placeholder 
for `ValueType.COMPLEX` which doesn't exist in `ExprType`, so I was shaping 
this method to be straightforward to work off of that enum instead of this one 
someday. 
   
   I can throw an exception with the messaging 'impossible' if you would 
prefer, but there isn't a way to actually check it with a test.

##########
File path: core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java
##########
@@ -35,6 +35,19 @@
  */
 abstract class ConstantExpr implements Expr
 {
+  final ExprType outputType;
+
+  protected ConstantExpr(ExprType outputType)
+  {
+    this.outputType = outputType;
+  }
+
+  @Override

Review comment:
       Oops, I didn't actually mean to make this not be `@Nullable`, though I 
guess it is true, but will fix to just be consistent with the interface.

##########
File path: core/src/main/java/org/apache/druid/math/expr/Expr.java
##########
@@ -116,16 +116,39 @@ default String getBindingIfIdentifier()
   void visit(Visitor visitor);
 
   /**
-   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}.Each 
{@link Expr} is responsible for
+   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}. 
Each {@link Expr} is responsible for
    * ensuring the {@link Shuttle} can visit all of its {@link Expr} children, 
as well as updating its children
    * {@link Expr} with the results from the {@link Shuttle}, before finally 
visiting an updated form of itself.
    */
   Expr visit(Shuttle shuttle);
 
   /**
-   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingDetails}
+   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingAnalysis}
    */
-  BindingDetails analyzeInputs();
+  BindingAnalysis analyzeInputs();
+
+  /**
+   * Given an {@link InputBindingTypes}, compute what the output {@link 
ExprType} will be for this expression. A return
+   * value of null indicates that the given type information was not enough to 
resolve the output type, so the
+   * expression must be evaluated using default {@link #eval} handling where 
types are only known after evaluation,
+   * through {@link ExprEval#type}.
+   */
+  @Nullable
+  default ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    return null;
+  }
+
+  /**
+   * Mechanism to supply input types for the bindings which will back {@link 
IdentifierExpr}, to use in the aid of
+   * inferring the output type of an expression with {@link #getOutputType}. A 
null value means that either the binding

Review comment:
       not long term probably, but currently this will be backed by 
`ColumnCapabilities` from a `ColumnInspector`, which does not currently 
distinguish between those 2 things for all implementations.

##########
File path: core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java
##########
@@ -163,4 +171,15 @@ public String toString()
   {
     return StringUtils.format("!%s", expr);
   }
+
+  @Nullable
+  @Override
+  public ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    ExprType implicitCast = super.getOutputType(inputTypes);
+    if (ExprType.STRING.equals(implicitCast)) {
+      return ExprType.LONG;
+    }

Review comment:
       I don't know off the top of my head why it does that, I was just trying 
to model its output. The binary logic expressions do that too:
   ```
     @Override
     protected ExprEval evalString(@Nullable String left, @Nullable String 
right)
     {
       return 
ExprEval.ofLongBoolean(Comparators.<String>naturalNullsFirst().compare(left, 
right) < 0);
     }
   ```
   so it is just being consistent with those operators I think.
   
   I'm moderately in the camp of wondering why these logical operators return 
anything other than long for any input type, but I haven't thought fully 
through on the implications of changing that yet, so maybe there is a reason 
that the type is preserved for doubles in these operators.

##########
File path: 
processing/src/main/java/org/apache/druid/query/expression/TimestampCeilExprMacro.java
##########
@@ -93,6 +95,13 @@ public Expr visit(Shuttle shuttle)
       return shuttle.visit(new TimestampCeilExpr(newArgs));
     }
 
+    @Nullable
+    @Override
+    public ExprType getOutputType(InputBindingTypes inputTypes)
+    {
+      return ExprType.LONG;

Review comment:
       Hmm, so numeric null handling should be done currently be done using 
`ExprEval.isNumericNull`, which will return true even if the type is a null 
string from a `StringExprEval`. So i think 'string' is technically correct, but 
I don't think spiritually or functionally quite true, nor that useful. We 
probably should modify these `ExprEval.of(null)` to make type correct 
`ExprEval` to make `eval` match exactly what `getOutputType` says, but I don't 
know if this PR is the correct place to do that.

##########
File path: 
server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java
##########
@@ -518,7 +518,7 @@ private void initializeLookupsConfigWatcher()
         configManager.set(
             LOOKUP_CONFIG_KEY,
             converted,
-            new AuditInfo("autoConversion", "autoConversion", "127.0.0.1")
+            new AuditInfo("autoTypeConversion", "autoTypeConversion", 
"127.0.0.1")

Review comment:
       oops, intellij got a bit aggro

##########
File path: core/src/main/java/org/apache/druid/math/expr/ExprType.java
##########
@@ -29,5 +35,116 @@
   STRING,
   DOUBLE_ARRAY,
   LONG_ARRAY,
-  STRING_ARRAY
+  STRING_ARRAY;
+
+  public boolean isNumeric()
+  {
+    return isNumeric(this);
+  }
+
+  /**
+   * The expression system does not distinguish between {@link 
ValueType#FLOAT} and {@link ValueType#DOUBLE}, and
+   * cannot currently handle {@link ValueType#COMPLEX} inputs. This method 
will convert {@link ValueType#FLOAT} to
+   * {@link #DOUBLE}, or throw an exception if a {@link ValueType#COMPLEX} is 
encountered.
+   *
+   * @throws IllegalStateException
+   */
+  public static ExprType fromValueType(@Nullable ValueType valueType)
+  {
+    if (valueType == null) {
+      throw new IllegalStateException("Unsupported unknown value type");
+    }
+    switch (valueType) {
+      case LONG:
+        return LONG;
+      case LONG_ARRAY:
+        return LONG_ARRAY;
+      case FLOAT:
+      case DOUBLE:
+        return DOUBLE;
+      case DOUBLE_ARRAY:
+        return DOUBLE_ARRAY;
+      case STRING:
+        return STRING;
+      case STRING_ARRAY:
+        return STRING_ARRAY;
+      case COMPLEX:
+      default:
+        throw new ISE("Unsupported value type[%s]", valueType);
+    }
+  }
+
+  public static boolean isNumeric(ExprType type)
+  {
+    return LONG.equals(type) || DOUBLE.equals(type);
+  }
+
+  public static boolean isArray(@Nullable ExprType type)
+  {
+    return LONG_ARRAY.equals(type) || DOUBLE_ARRAY.equals(type) || 
STRING_ARRAY.equals(type);
+  }
+
+  @Nullable
+  public static ExprType elementType(@Nullable ExprType type)
+  {
+    if (type != null && isArray(type)) {
+      switch (type) {
+        case STRING_ARRAY:
+          return STRING;
+        case LONG_ARRAY:
+          return LONG;
+        case DOUBLE_ARRAY:
+          return DOUBLE;
+      }
+    }
+    return type;
+  }
+
+  @Nullable
+  public static ExprType asArrayType(@Nullable ExprType elementType)
+  {
+    if (elementType != null && !isArray(elementType)) {
+      switch (elementType) {
+        case STRING:
+          return STRING_ARRAY;
+        case LONG:
+          return LONG_ARRAY;
+        case DOUBLE:
+          return DOUBLE_ARRAY;
+      }
+    }
+    return elementType;
+  }
+
+  @Nullable
+  public static ExprType implicitCast(@Nullable ExprType type, @Nullable 
ExprType other)
+  {
+    if (type == null || other == null) {
+      // cannot implicitly cast unknown types
+      return null;
+    }
+    // arrays cannot be implicitly cast
+    if (isArray(type)) {
+      if (!type.equals(other)) {
+        throw new IAE("Cannot implicitly cast %s to %s", type, other);
+      }
+      return type;
+    }
+    // if either argument is a string, type becomes a string
+    if (STRING.equals(type) || STRING.equals(other)) {
+      return STRING;
+    }
+
+    if (isNumeric(type) && isNumeric(other)) {
+      // all numbers win over longs
+      if (LONG.equals(type)) {
+        return other;
+      }
+      // floats vs longs would be handled here, but we currently only support 
doubles...
+      return type;
+    }
+
+    // unhandled is unknown
+    return null;

Review comment:
       >As you said the code is volatile so chances of this path being hit are 
even higher.
   
   I mean, it's totally not possible to hit this null because there isn't a 
`ExprType` combination of input arguments that ends up on this line. By 
volatile I meant that this enum and file is likely going to go away and maybe 
this function migrated into `ValueType`. This line is essentially a placeholder 
for `ValueType.COMPLEX` which doesn't exist in `ExprType`, so I was shaping 
this method to be straightforward to work off of that enum instead of this one 
someday. 
   
   I can throw an exception with the messaging 'impossible' if you would 
prefer, but there isn't a way to actually check it with a test.

##########
File path: core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java
##########
@@ -35,6 +35,19 @@
  */
 abstract class ConstantExpr implements Expr
 {
+  final ExprType outputType;
+
+  protected ConstantExpr(ExprType outputType)
+  {
+    this.outputType = outputType;
+  }
+
+  @Override

Review comment:
       Oops, I didn't actually mean to make this not be `@Nullable`, though I 
guess it is true, but will fix to just be consistent with the interface.

##########
File path: core/src/main/java/org/apache/druid/math/expr/Expr.java
##########
@@ -116,16 +116,39 @@ default String getBindingIfIdentifier()
   void visit(Visitor visitor);
 
   /**
-   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}.Each 
{@link Expr} is responsible for
+   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}. 
Each {@link Expr} is responsible for
    * ensuring the {@link Shuttle} can visit all of its {@link Expr} children, 
as well as updating its children
    * {@link Expr} with the results from the {@link Shuttle}, before finally 
visiting an updated form of itself.
    */
   Expr visit(Shuttle shuttle);
 
   /**
-   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingDetails}
+   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingAnalysis}
    */
-  BindingDetails analyzeInputs();
+  BindingAnalysis analyzeInputs();
+
+  /**
+   * Given an {@link InputBindingTypes}, compute what the output {@link 
ExprType} will be for this expression. A return
+   * value of null indicates that the given type information was not enough to 
resolve the output type, so the
+   * expression must be evaluated using default {@link #eval} handling where 
types are only known after evaluation,
+   * through {@link ExprEval#type}.
+   */
+  @Nullable
+  default ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    return null;
+  }
+
+  /**
+   * Mechanism to supply input types for the bindings which will back {@link 
IdentifierExpr}, to use in the aid of
+   * inferring the output type of an expression with {@link #getOutputType}. A 
null value means that either the binding

Review comment:
       not long term probably, but currently this will be backed by 
`ColumnCapabilities` from a `ColumnInspector`, which does not currently 
distinguish between those 2 things for all implementations.

##########
File path: core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java
##########
@@ -163,4 +171,15 @@ public String toString()
   {
     return StringUtils.format("!%s", expr);
   }
+
+  @Nullable
+  @Override
+  public ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    ExprType implicitCast = super.getOutputType(inputTypes);
+    if (ExprType.STRING.equals(implicitCast)) {
+      return ExprType.LONG;
+    }

Review comment:
       I don't know off the top of my head why it does that, I was just trying 
to model its output. The binary logic expressions do that too:
   ```
     @Override
     protected ExprEval evalString(@Nullable String left, @Nullable String 
right)
     {
       return 
ExprEval.ofLongBoolean(Comparators.<String>naturalNullsFirst().compare(left, 
right) < 0);
     }
   ```
   so it is just being consistent with those operators I think.
   
   I'm moderately in the camp of wondering why these logical operators return 
anything other than long for any input type, but I haven't thought fully 
through on the implications of changing that yet, so maybe there is a reason 
that the type is preserved for doubles in these operators.

##########
File path: 
processing/src/main/java/org/apache/druid/query/expression/TimestampCeilExprMacro.java
##########
@@ -93,6 +95,13 @@ public Expr visit(Shuttle shuttle)
       return shuttle.visit(new TimestampCeilExpr(newArgs));
     }
 
+    @Nullable
+    @Override
+    public ExprType getOutputType(InputBindingTypes inputTypes)
+    {
+      return ExprType.LONG;

Review comment:
       Hmm, so numeric null handling should be done currently be done using 
`ExprEval.isNumericNull`, which will return true even if the type is a null 
string from a `StringExprEval`. So i think 'string' is technically correct, but 
I don't think spiritually or functionally quite true, nor that useful. We 
probably should modify these `ExprEval.of(null)` to make type correct 
`ExprEval` to make `eval` match exactly what `getOutputType` says, but I don't 
know if this PR is the correct place to do that.

##########
File path: 
server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java
##########
@@ -518,7 +518,7 @@ private void initializeLookupsConfigWatcher()
         configManager.set(
             LOOKUP_CONFIG_KEY,
             converted,
-            new AuditInfo("autoConversion", "autoConversion", "127.0.0.1")
+            new AuditInfo("autoTypeConversion", "autoTypeConversion", 
"127.0.0.1")

Review comment:
       oops, intellij got a bit aggro

##########
File path: core/src/main/java/org/apache/druid/math/expr/ExprType.java
##########
@@ -29,5 +35,116 @@
   STRING,
   DOUBLE_ARRAY,
   LONG_ARRAY,
-  STRING_ARRAY
+  STRING_ARRAY;
+
+  public boolean isNumeric()
+  {
+    return isNumeric(this);
+  }
+
+  /**
+   * The expression system does not distinguish between {@link 
ValueType#FLOAT} and {@link ValueType#DOUBLE}, and
+   * cannot currently handle {@link ValueType#COMPLEX} inputs. This method 
will convert {@link ValueType#FLOAT} to
+   * {@link #DOUBLE}, or throw an exception if a {@link ValueType#COMPLEX} is 
encountered.
+   *
+   * @throws IllegalStateException
+   */
+  public static ExprType fromValueType(@Nullable ValueType valueType)
+  {
+    if (valueType == null) {
+      throw new IllegalStateException("Unsupported unknown value type");
+    }
+    switch (valueType) {
+      case LONG:
+        return LONG;
+      case LONG_ARRAY:
+        return LONG_ARRAY;
+      case FLOAT:
+      case DOUBLE:
+        return DOUBLE;
+      case DOUBLE_ARRAY:
+        return DOUBLE_ARRAY;
+      case STRING:
+        return STRING;
+      case STRING_ARRAY:
+        return STRING_ARRAY;
+      case COMPLEX:
+      default:
+        throw new ISE("Unsupported value type[%s]", valueType);
+    }
+  }
+
+  public static boolean isNumeric(ExprType type)
+  {
+    return LONG.equals(type) || DOUBLE.equals(type);
+  }
+
+  public static boolean isArray(@Nullable ExprType type)
+  {
+    return LONG_ARRAY.equals(type) || DOUBLE_ARRAY.equals(type) || 
STRING_ARRAY.equals(type);
+  }
+
+  @Nullable
+  public static ExprType elementType(@Nullable ExprType type)
+  {
+    if (type != null && isArray(type)) {
+      switch (type) {
+        case STRING_ARRAY:
+          return STRING;
+        case LONG_ARRAY:
+          return LONG;
+        case DOUBLE_ARRAY:
+          return DOUBLE;
+      }
+    }
+    return type;
+  }
+
+  @Nullable
+  public static ExprType asArrayType(@Nullable ExprType elementType)
+  {
+    if (elementType != null && !isArray(elementType)) {
+      switch (elementType) {
+        case STRING:
+          return STRING_ARRAY;
+        case LONG:
+          return LONG_ARRAY;
+        case DOUBLE:
+          return DOUBLE_ARRAY;
+      }
+    }
+    return elementType;
+  }
+
+  @Nullable
+  public static ExprType implicitCast(@Nullable ExprType type, @Nullable 
ExprType other)
+  {
+    if (type == null || other == null) {
+      // cannot implicitly cast unknown types
+      return null;
+    }
+    // arrays cannot be implicitly cast
+    if (isArray(type)) {
+      if (!type.equals(other)) {
+        throw new IAE("Cannot implicitly cast %s to %s", type, other);
+      }
+      return type;
+    }
+    // if either argument is a string, type becomes a string
+    if (STRING.equals(type) || STRING.equals(other)) {
+      return STRING;
+    }
+
+    if (isNumeric(type) && isNumeric(other)) {
+      // all numbers win over longs
+      if (LONG.equals(type)) {
+        return other;
+      }
+      // floats vs longs would be handled here, but we currently only support 
doubles...
+      return type;
+    }
+
+    // unhandled is unknown
+    return null;

Review comment:
       >As you said the code is volatile so chances of this path being hit are 
even higher.
   
   I mean, it's totally not possible to hit this null because there isn't a 
`ExprType` combination of input arguments that ends up on this line. By 
volatile I meant that this enum and file is likely going to go away and maybe 
this function migrated into `ValueType`. This line is essentially a placeholder 
for `ValueType.COMPLEX` which doesn't exist in `ExprType`, so I was shaping 
this method to be straightforward to work off of that enum instead of this one 
someday. 
   
   I can throw an exception with the messaging 'impossible' if you would 
prefer, but there isn't a way to actually check it with a test.

##########
File path: core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java
##########
@@ -35,6 +35,19 @@
  */
 abstract class ConstantExpr implements Expr
 {
+  final ExprType outputType;
+
+  protected ConstantExpr(ExprType outputType)
+  {
+    this.outputType = outputType;
+  }
+
+  @Override

Review comment:
       Oops, I didn't actually mean to make this not be `@Nullable`, though I 
guess it is true, but will fix to just be consistent with the interface.

##########
File path: core/src/main/java/org/apache/druid/math/expr/Expr.java
##########
@@ -116,16 +116,39 @@ default String getBindingIfIdentifier()
   void visit(Visitor visitor);
 
   /**
-   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}.Each 
{@link Expr} is responsible for
+   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}. 
Each {@link Expr} is responsible for
    * ensuring the {@link Shuttle} can visit all of its {@link Expr} children, 
as well as updating its children
    * {@link Expr} with the results from the {@link Shuttle}, before finally 
visiting an updated form of itself.
    */
   Expr visit(Shuttle shuttle);
 
   /**
-   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingDetails}
+   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingAnalysis}
    */
-  BindingDetails analyzeInputs();
+  BindingAnalysis analyzeInputs();
+
+  /**
+   * Given an {@link InputBindingTypes}, compute what the output {@link 
ExprType} will be for this expression. A return
+   * value of null indicates that the given type information was not enough to 
resolve the output type, so the
+   * expression must be evaluated using default {@link #eval} handling where 
types are only known after evaluation,
+   * through {@link ExprEval#type}.
+   */
+  @Nullable
+  default ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    return null;
+  }
+
+  /**
+   * Mechanism to supply input types for the bindings which will back {@link 
IdentifierExpr}, to use in the aid of
+   * inferring the output type of an expression with {@link #getOutputType}. A 
null value means that either the binding

Review comment:
       not long term probably, but currently this will be backed by 
`ColumnCapabilities` from a `ColumnInspector`, which does not currently 
distinguish between those 2 things for all implementations.

##########
File path: core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java
##########
@@ -163,4 +171,15 @@ public String toString()
   {
     return StringUtils.format("!%s", expr);
   }
+
+  @Nullable
+  @Override
+  public ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    ExprType implicitCast = super.getOutputType(inputTypes);
+    if (ExprType.STRING.equals(implicitCast)) {
+      return ExprType.LONG;
+    }

Review comment:
       I don't know off the top of my head why it does that, I was just trying 
to model its output. The binary logic expressions do that too:
   ```
     @Override
     protected ExprEval evalString(@Nullable String left, @Nullable String 
right)
     {
       return 
ExprEval.ofLongBoolean(Comparators.<String>naturalNullsFirst().compare(left, 
right) < 0);
     }
   ```
   so it is just being consistent with those operators I think.
   
   I'm moderately in the camp of wondering why these logical operators return 
anything other than long for any input type, but I haven't thought fully 
through on the implications of changing that yet, so maybe there is a reason 
that the type is preserved for doubles in these operators.

##########
File path: 
processing/src/main/java/org/apache/druid/query/expression/TimestampCeilExprMacro.java
##########
@@ -93,6 +95,13 @@ public Expr visit(Shuttle shuttle)
       return shuttle.visit(new TimestampCeilExpr(newArgs));
     }
 
+    @Nullable
+    @Override
+    public ExprType getOutputType(InputBindingTypes inputTypes)
+    {
+      return ExprType.LONG;

Review comment:
       Hmm, so numeric null handling should be done currently be done using 
`ExprEval.isNumericNull`, which will return true even if the type is a null 
string from a `StringExprEval`. So i think 'string' is technically correct, but 
I don't think spiritually or functionally quite true, nor that useful. We 
probably should modify these `ExprEval.of(null)` to make type correct 
`ExprEval` to make `eval` match exactly what `getOutputType` says, but I don't 
know if this PR is the correct place to do that.

##########
File path: 
server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java
##########
@@ -518,7 +518,7 @@ private void initializeLookupsConfigWatcher()
         configManager.set(
             LOOKUP_CONFIG_KEY,
             converted,
-            new AuditInfo("autoConversion", "autoConversion", "127.0.0.1")
+            new AuditInfo("autoTypeConversion", "autoTypeConversion", 
"127.0.0.1")

Review comment:
       oops, intellij got a bit aggro

##########
File path: core/src/main/java/org/apache/druid/math/expr/ExprType.java
##########
@@ -29,5 +35,116 @@
   STRING,
   DOUBLE_ARRAY,
   LONG_ARRAY,
-  STRING_ARRAY
+  STRING_ARRAY;
+
+  public boolean isNumeric()
+  {
+    return isNumeric(this);
+  }
+
+  /**
+   * The expression system does not distinguish between {@link 
ValueType#FLOAT} and {@link ValueType#DOUBLE}, and
+   * cannot currently handle {@link ValueType#COMPLEX} inputs. This method 
will convert {@link ValueType#FLOAT} to
+   * {@link #DOUBLE}, or throw an exception if a {@link ValueType#COMPLEX} is 
encountered.
+   *
+   * @throws IllegalStateException
+   */
+  public static ExprType fromValueType(@Nullable ValueType valueType)
+  {
+    if (valueType == null) {
+      throw new IllegalStateException("Unsupported unknown value type");
+    }
+    switch (valueType) {
+      case LONG:
+        return LONG;
+      case LONG_ARRAY:
+        return LONG_ARRAY;
+      case FLOAT:
+      case DOUBLE:
+        return DOUBLE;
+      case DOUBLE_ARRAY:
+        return DOUBLE_ARRAY;
+      case STRING:
+        return STRING;
+      case STRING_ARRAY:
+        return STRING_ARRAY;
+      case COMPLEX:
+      default:
+        throw new ISE("Unsupported value type[%s]", valueType);
+    }
+  }
+
+  public static boolean isNumeric(ExprType type)
+  {
+    return LONG.equals(type) || DOUBLE.equals(type);
+  }
+
+  public static boolean isArray(@Nullable ExprType type)
+  {
+    return LONG_ARRAY.equals(type) || DOUBLE_ARRAY.equals(type) || 
STRING_ARRAY.equals(type);
+  }
+
+  @Nullable
+  public static ExprType elementType(@Nullable ExprType type)
+  {
+    if (type != null && isArray(type)) {
+      switch (type) {
+        case STRING_ARRAY:
+          return STRING;
+        case LONG_ARRAY:
+          return LONG;
+        case DOUBLE_ARRAY:
+          return DOUBLE;
+      }
+    }
+    return type;
+  }
+
+  @Nullable
+  public static ExprType asArrayType(@Nullable ExprType elementType)
+  {
+    if (elementType != null && !isArray(elementType)) {
+      switch (elementType) {
+        case STRING:
+          return STRING_ARRAY;
+        case LONG:
+          return LONG_ARRAY;
+        case DOUBLE:
+          return DOUBLE_ARRAY;
+      }
+    }
+    return elementType;
+  }
+
+  @Nullable
+  public static ExprType implicitCast(@Nullable ExprType type, @Nullable 
ExprType other)
+  {
+    if (type == null || other == null) {
+      // cannot implicitly cast unknown types
+      return null;
+    }
+    // arrays cannot be implicitly cast
+    if (isArray(type)) {
+      if (!type.equals(other)) {
+        throw new IAE("Cannot implicitly cast %s to %s", type, other);
+      }
+      return type;
+    }
+    // if either argument is a string, type becomes a string
+    if (STRING.equals(type) || STRING.equals(other)) {
+      return STRING;
+    }
+
+    if (isNumeric(type) && isNumeric(other)) {
+      // all numbers win over longs
+      if (LONG.equals(type)) {
+        return other;
+      }
+      // floats vs longs would be handled here, but we currently only support 
doubles...
+      return type;
+    }
+
+    // unhandled is unknown
+    return null;

Review comment:
       >As you said the code is volatile so chances of this path being hit are 
even higher.
   
   I mean, it's totally not possible to hit this null because there isn't a 
`ExprType` combination of input arguments that ends up on this line. By 
volatile I meant that this enum and file is likely going to go away and maybe 
this function migrated into `ValueType`. This line is essentially a placeholder 
for `ValueType.COMPLEX` which doesn't exist in `ExprType`, so I was shaping 
this method to be straightforward to work off of that enum instead of this one 
someday. 
   
   I can throw an exception with the messaging 'impossible' if you would 
prefer, but there isn't a way to actually check it with a test.

##########
File path: core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java
##########
@@ -35,6 +35,19 @@
  */
 abstract class ConstantExpr implements Expr
 {
+  final ExprType outputType;
+
+  protected ConstantExpr(ExprType outputType)
+  {
+    this.outputType = outputType;
+  }
+
+  @Override

Review comment:
       Oops, I didn't actually mean to make this not be `@Nullable`, though I 
guess it is true, but will fix to just be consistent with the interface.

##########
File path: core/src/main/java/org/apache/druid/math/expr/Expr.java
##########
@@ -116,16 +116,39 @@ default String getBindingIfIdentifier()
   void visit(Visitor visitor);
 
   /**
-   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}.Each 
{@link Expr} is responsible for
+   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}. 
Each {@link Expr} is responsible for
    * ensuring the {@link Shuttle} can visit all of its {@link Expr} children, 
as well as updating its children
    * {@link Expr} with the results from the {@link Shuttle}, before finally 
visiting an updated form of itself.
    */
   Expr visit(Shuttle shuttle);
 
   /**
-   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingDetails}
+   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingAnalysis}
    */
-  BindingDetails analyzeInputs();
+  BindingAnalysis analyzeInputs();
+
+  /**
+   * Given an {@link InputBindingTypes}, compute what the output {@link 
ExprType} will be for this expression. A return
+   * value of null indicates that the given type information was not enough to 
resolve the output type, so the
+   * expression must be evaluated using default {@link #eval} handling where 
types are only known after evaluation,
+   * through {@link ExprEval#type}.
+   */
+  @Nullable
+  default ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    return null;
+  }
+
+  /**
+   * Mechanism to supply input types for the bindings which will back {@link 
IdentifierExpr}, to use in the aid of
+   * inferring the output type of an expression with {@link #getOutputType}. A 
null value means that either the binding

Review comment:
       not long term probably, but currently this will be backed by 
`ColumnCapabilities` from a `ColumnInspector`, which does not currently 
distinguish between those 2 things for all implementations.

##########
File path: core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java
##########
@@ -163,4 +171,15 @@ public String toString()
   {
     return StringUtils.format("!%s", expr);
   }
+
+  @Nullable
+  @Override
+  public ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    ExprType implicitCast = super.getOutputType(inputTypes);
+    if (ExprType.STRING.equals(implicitCast)) {
+      return ExprType.LONG;
+    }

Review comment:
       I don't know off the top of my head why it does that, I was just trying 
to model its output. The binary logic expressions do that too:
   ```
     @Override
     protected ExprEval evalString(@Nullable String left, @Nullable String 
right)
     {
       return 
ExprEval.ofLongBoolean(Comparators.<String>naturalNullsFirst().compare(left, 
right) < 0);
     }
   ```
   so it is just being consistent with those operators I think.
   
   I'm moderately in the camp of wondering why these logical operators return 
anything other than long for any input type, but I haven't thought fully 
through on the implications of changing that yet, so maybe there is a reason 
that the type is preserved for doubles in these operators.

##########
File path: 
processing/src/main/java/org/apache/druid/query/expression/TimestampCeilExprMacro.java
##########
@@ -93,6 +95,13 @@ public Expr visit(Shuttle shuttle)
       return shuttle.visit(new TimestampCeilExpr(newArgs));
     }
 
+    @Nullable
+    @Override
+    public ExprType getOutputType(InputBindingTypes inputTypes)
+    {
+      return ExprType.LONG;

Review comment:
       Hmm, so numeric null handling should be done currently be done using 
`ExprEval.isNumericNull`, which will return true even if the type is a null 
string from a `StringExprEval`. So i think 'string' is technically correct, but 
I don't think spiritually or functionally quite true, nor that useful. We 
probably should modify these `ExprEval.of(null)` to make type correct 
`ExprEval` to make `eval` match exactly what `getOutputType` says, but I don't 
know if this PR is the correct place to do that.

##########
File path: 
server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java
##########
@@ -518,7 +518,7 @@ private void initializeLookupsConfigWatcher()
         configManager.set(
             LOOKUP_CONFIG_KEY,
             converted,
-            new AuditInfo("autoConversion", "autoConversion", "127.0.0.1")
+            new AuditInfo("autoTypeConversion", "autoTypeConversion", 
"127.0.0.1")

Review comment:
       oops, intellij got a bit aggro

##########
File path: core/src/main/java/org/apache/druid/math/expr/ConstantExpr.java
##########
@@ -35,6 +35,19 @@
  */
 abstract class ConstantExpr implements Expr
 {
+  final ExprType outputType;
+
+  protected ConstantExpr(ExprType outputType)
+  {
+    this.outputType = outputType;
+  }
+
+  @Override

Review comment:
       Oops, I didn't actually mean to make this not be `@Nullable`, though I 
guess it is true, but will fix to just be consistent with the interface.

##########
File path: core/src/main/java/org/apache/druid/math/expr/Expr.java
##########
@@ -116,16 +116,39 @@ default String getBindingIfIdentifier()
   void visit(Visitor visitor);
 
   /**
-   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}.Each 
{@link Expr} is responsible for
+   * Programatically rewrite the {@link Expr} tree with a {@link Shuttle}. 
Each {@link Expr} is responsible for
    * ensuring the {@link Shuttle} can visit all of its {@link Expr} children, 
as well as updating its children
    * {@link Expr} with the results from the {@link Shuttle}, before finally 
visiting an updated form of itself.
    */
   Expr visit(Shuttle shuttle);
 
   /**
-   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingDetails}
+   * Examine the usage of {@link IdentifierExpr} children of an {@link Expr}, 
constructing a {@link BindingAnalysis}
    */
-  BindingDetails analyzeInputs();
+  BindingAnalysis analyzeInputs();
+
+  /**
+   * Given an {@link InputBindingTypes}, compute what the output {@link 
ExprType} will be for this expression. A return
+   * value of null indicates that the given type information was not enough to 
resolve the output type, so the
+   * expression must be evaluated using default {@link #eval} handling where 
types are only known after evaluation,
+   * through {@link ExprEval#type}.
+   */
+  @Nullable
+  default ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    return null;
+  }
+
+  /**
+   * Mechanism to supply input types for the bindings which will back {@link 
IdentifierExpr}, to use in the aid of
+   * inferring the output type of an expression with {@link #getOutputType}. A 
null value means that either the binding

Review comment:
       not long term probably, but currently this will be backed by 
`ColumnCapabilities` from a `ColumnInspector`, which does not currently 
distinguish between those 2 things for all implementations.

##########
File path: core/src/main/java/org/apache/druid/math/expr/UnaryOperatorExpr.java
##########
@@ -163,4 +171,15 @@ public String toString()
   {
     return StringUtils.format("!%s", expr);
   }
+
+  @Nullable
+  @Override
+  public ExprType getOutputType(InputBindingTypes inputTypes)
+  {
+    ExprType implicitCast = super.getOutputType(inputTypes);
+    if (ExprType.STRING.equals(implicitCast)) {
+      return ExprType.LONG;
+    }

Review comment:
       I don't know off the top of my head why it does that, I was just trying 
to model its output. The binary logic expressions do that too:
   ```
     @Override
     protected ExprEval evalString(@Nullable String left, @Nullable String 
right)
     {
       return 
ExprEval.ofLongBoolean(Comparators.<String>naturalNullsFirst().compare(left, 
right) < 0);
     }
   ```
   so it is just being consistent with those operators I think.
   
   I'm moderately in the camp of wondering why these logical operators return 
anything other than long for any input type, but I haven't thought fully 
through on the implications of changing that yet, so maybe there is a reason 
that the type is preserved for doubles in these operators.

##########
File path: 
processing/src/main/java/org/apache/druid/query/expression/TimestampCeilExprMacro.java
##########
@@ -93,6 +95,13 @@ public Expr visit(Shuttle shuttle)
       return shuttle.visit(new TimestampCeilExpr(newArgs));
     }
 
+    @Nullable
+    @Override
+    public ExprType getOutputType(InputBindingTypes inputTypes)
+    {
+      return ExprType.LONG;

Review comment:
       Hmm, so numeric null handling should be done currently be done using 
`ExprEval.isNumericNull`, which will return true even if the type is a null 
string from a `StringExprEval`. So i think 'string' is technically correct, but 
I don't think spiritually or functionally quite true, nor that useful. We 
probably should modify these `ExprEval.of(null)` to make type correct 
`ExprEval` to make `eval` match exactly what `getOutputType` says, but I don't 
know if this PR is the correct place to do that.

##########
File path: 
server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java
##########
@@ -518,7 +518,7 @@ private void initializeLookupsConfigWatcher()
         configManager.set(
             LOOKUP_CONFIG_KEY,
             converted,
-            new AuditInfo("autoConversion", "autoConversion", "127.0.0.1")
+            new AuditInfo("autoTypeConversion", "autoTypeConversion", 
"127.0.0.1")

Review comment:
       oops, intellij got a bit aggro

##########
File path: core/src/main/java/org/apache/druid/math/expr/ExprType.java
##########
@@ -29,5 +35,116 @@
   STRING,
   DOUBLE_ARRAY,
   LONG_ARRAY,
-  STRING_ARRAY
+  STRING_ARRAY;
+
+  public boolean isNumeric()
+  {
+    return isNumeric(this);
+  }
+
+  /**
+   * The expression system does not distinguish between {@link 
ValueType#FLOAT} and {@link ValueType#DOUBLE}, and
+   * cannot currently handle {@link ValueType#COMPLEX} inputs. This method 
will convert {@link ValueType#FLOAT} to
+   * {@link #DOUBLE}, or throw an exception if a {@link ValueType#COMPLEX} is 
encountered.
+   *
+   * @throws IllegalStateException
+   */
+  public static ExprType fromValueType(@Nullable ValueType valueType)
+  {
+    if (valueType == null) {
+      throw new IllegalStateException("Unsupported unknown value type");
+    }
+    switch (valueType) {
+      case LONG:
+        return LONG;
+      case LONG_ARRAY:
+        return LONG_ARRAY;
+      case FLOAT:
+      case DOUBLE:
+        return DOUBLE;
+      case DOUBLE_ARRAY:
+        return DOUBLE_ARRAY;
+      case STRING:
+        return STRING;
+      case STRING_ARRAY:
+        return STRING_ARRAY;
+      case COMPLEX:
+      default:
+        throw new ISE("Unsupported value type[%s]", valueType);
+    }
+  }
+
+  public static boolean isNumeric(ExprType type)
+  {
+    return LONG.equals(type) || DOUBLE.equals(type);
+  }
+
+  public static boolean isArray(@Nullable ExprType type)
+  {
+    return LONG_ARRAY.equals(type) || DOUBLE_ARRAY.equals(type) || 
STRING_ARRAY.equals(type);
+  }
+
+  @Nullable
+  public static ExprType elementType(@Nullable ExprType type)
+  {
+    if (type != null && isArray(type)) {
+      switch (type) {
+        case STRING_ARRAY:
+          return STRING;
+        case LONG_ARRAY:
+          return LONG;
+        case DOUBLE_ARRAY:
+          return DOUBLE;
+      }
+    }
+    return type;
+  }
+
+  @Nullable
+  public static ExprType asArrayType(@Nullable ExprType elementType)
+  {
+    if (elementType != null && !isArray(elementType)) {
+      switch (elementType) {
+        case STRING:
+          return STRING_ARRAY;
+        case LONG:
+          return LONG_ARRAY;
+        case DOUBLE:
+          return DOUBLE_ARRAY;
+      }
+    }
+    return elementType;
+  }
+
+  @Nullable
+  public static ExprType implicitCast(@Nullable ExprType type, @Nullable 
ExprType other)
+  {
+    if (type == null || other == null) {
+      // cannot implicitly cast unknown types
+      return null;
+    }
+    // arrays cannot be implicitly cast
+    if (isArray(type)) {
+      if (!type.equals(other)) {
+        throw new IAE("Cannot implicitly cast %s to %s", type, other);
+      }
+      return type;
+    }
+    // if either argument is a string, type becomes a string
+    if (STRING.equals(type) || STRING.equals(other)) {
+      return STRING;
+    }
+
+    if (isNumeric(type) && isNumeric(other)) {
+      // all numbers win over longs
+      if (LONG.equals(type)) {
+        return other;
+      }
+      // floats vs longs would be handled here, but we currently only support 
doubles...
+      return type;
+    }
+
+    // unhandled is unknown
+    return null;

Review comment:
       >As you said the code is volatile so chances of this path being hit are 
even higher.
   
   I mean, it's totally not possible to hit this null because there isn't a 
`ExprType` combination of input arguments that ends up on this line. By 
volatile I meant that this enum and file is likely going to go away and maybe 
this function migrated into `ValueType`. This line is essentially a placeholder 
for `ValueType.COMPLEX` which doesn't exist in `ExprType`, so I was shaping 
this method to be straightforward to work off of that enum instead of this one 
someday. 
   
   I can throw an exception with the messaging 'impossible' if you would 
prefer, but there isn't a way to actually check it with a test.

##########
File path: core/src/main/java/org/apache/druid/math/expr/ExprType.java
##########
@@ -29,5 +35,116 @@
   STRING,
   DOUBLE_ARRAY,
   LONG_ARRAY,
-  STRING_ARRAY
+  STRING_ARRAY;
+
+  public boolean isNumeric()
+  {
+    return isNumeric(this);
+  }
+
+  /**
+   * The expression system does not distinguish between {@link 
ValueType#FLOAT} and {@link ValueType#DOUBLE}, and
+   * cannot currently handle {@link ValueType#COMPLEX} inputs. This method 
will convert {@link ValueType#FLOAT} to
+   * {@link #DOUBLE}, or throw an exception if a {@link ValueType#COMPLEX} is 
encountered.
+   *
+   * @throws IllegalStateException
+   */
+  public static ExprType fromValueType(@Nullable ValueType valueType)
+  {
+    if (valueType == null) {
+      throw new IllegalStateException("Unsupported unknown value type");
+    }
+    switch (valueType) {
+      case LONG:
+        return LONG;
+      case LONG_ARRAY:
+        return LONG_ARRAY;
+      case FLOAT:
+      case DOUBLE:
+        return DOUBLE;
+      case DOUBLE_ARRAY:
+        return DOUBLE_ARRAY;
+      case STRING:
+        return STRING;
+      case STRING_ARRAY:
+        return STRING_ARRAY;
+      case COMPLEX:
+      default:
+        throw new ISE("Unsupported value type[%s]", valueType);
+    }
+  }
+
+  public static boolean isNumeric(ExprType type)
+  {
+    return LONG.equals(type) || DOUBLE.equals(type);
+  }
+
+  public static boolean isArray(@Nullable ExprType type)
+  {
+    return LONG_ARRAY.equals(type) || DOUBLE_ARRAY.equals(type) || 
STRING_ARRAY.equals(type);
+  }
+
+  @Nullable
+  public static ExprType elementType(@Nullable ExprType type)
+  {
+    if (type != null && isArray(type)) {
+      switch (type) {
+        case STRING_ARRAY:
+          return STRING;
+        case LONG_ARRAY:
+          return LONG;
+        case DOUBLE_ARRAY:
+          return DOUBLE;
+      }
+    }
+    return type;
+  }
+
+  @Nullable
+  public static ExprType asArrayType(@Nullable ExprType elementType)
+  {
+    if (elementType != null && !isArray(elementType)) {
+      switch (elementType) {
+        case STRING:
+          return STRING_ARRAY;
+        case LONG:
+          return LONG_ARRAY;
+        case DOUBLE:
+          return DOUBLE_ARRAY;
+      }
+    }
+    return elementType;
+  }
+
+  @Nullable
+  public static ExprType implicitCast(@Nullable ExprType type, @Nullable 
ExprType other)
+  {
+    if (type == null || other == null) {
+      // cannot implicitly cast unknown types
+      return null;
+    }
+    // arrays cannot be implicitly cast
+    if (isArray(type)) {
+      if (!type.equals(other)) {
+        throw new IAE("Cannot implicitly cast %s to %s", type, other);
+      }
+      return type;
+    }
+    // if either argument is a string, type becomes a string
+    if (STRING.equals(type) || STRING.equals(other)) {
+      return STRING;
+    }
+
+    if (isNumeric(type) && isNumeric(other)) {
+      // all numbers win over longs
+      if (LONG.equals(type)) {
+        return other;
+      }
+      // floats vs longs would be handled here, but we currently only support 
doubles...
+      return type;
+    }
+
+    // unhandled is unknown
+    return null;

Review comment:
       >As you said the code is volatile so chances of this path being hit are 
even higher.
   
   I mean, it's totally not possible to hit this null because there isn't a 
`ExprType` combination of input arguments that ends up on this line. By 
volatile I meant that this enum and file is likely going to go away and maybe 
this function migrated into `ValueType`. This line is essentially a placeholder 
for `ValueType.COMPLEX` which doesn't exist in `ExprType`, so I was shaping 
this method to be straightforward to work off of that enum instead of this one 
someday. 
   
   I can throw an exception with the messaging 'impossible' if you would 
prefer, but there isn't a way to actually check it with a test.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
[email protected]



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to