This is an automated email from the ASF dual-hosted git repository.

mbudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git

commit 87485a90737ded1f7324870e761b94d78a41b5e0
Author: Mihai Budiu <[email protected]>
AuthorDate: Thu Sep 5 23:43:38 2024 -0700

    Implement VARIANT functions TYPEOF, VARIANTNULL; add variant.iq
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../calcite/adapter/enumerable/RexImpTable.java    |   6 +
 .../adapter/enumerable/RexToLixTranslator.java     |  13 +-
 .../java/org/apache/calcite/rex/RexBuilder.java    |   3 +-
 .../java/org/apache/calcite/rex/RexLiteral.java    |   4 +-
 .../org/apache/calcite/runtime/SqlFunctions.java   |   6 +-
 .../{util => runtime}/rtti/BasicSqlTypeRtti.java   |  26 +-
 .../{util => runtime}/rtti/GenericSqlTypeRtti.java |  10 +-
 .../{util => runtime}/rtti/RowSqlTypeRtti.java     |  46 ++-
 .../rtti/RuntimeTypeInformation.java               | 106 +++--
 .../{util => runtime}/rtti/package-info.java       |   2 +-
 .../calcite/runtime/variant/VariantNonNull.java    | 452 +++++++++++++++++++++
 .../calcite/runtime/variant/VariantNull.java       |  64 +++
 .../calcite/runtime/variant/VariantSqlNull.java    |  60 +++
 .../calcite/runtime/variant/VariantSqlValue.java   |  58 +++
 .../calcite/runtime/variant/VariantValue.java      |  44 ++
 .../rtti => runtime/variant}/package-info.java     |   4 +-
 .../calcite/sql/fun/SqlStdOperatorTable.java       |  10 +
 .../org/apache/calcite/sql/type/OperandTypes.java  |   3 +
 .../org/apache/calcite/sql/type/ReturnTypes.java   |   6 +
 .../apache/calcite/sql2rel/ConvertToChecked.java   |   6 +-
 .../org/apache/calcite/util/BuiltInMethod.java     |  12 +-
 .../main/java/org/apache/calcite/util/Variant.java | 181 ---------
 core/src/test/resources/sql/variant.iq             | 197 +++++++++
 site/_docs/reference.md                            |  27 +-
 .../org/apache/calcite/test/SqlOperatorTest.java   |  10 +-
 25 files changed, 1080 insertions(+), 276 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index f766feb402..fd7aa0cf76 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -520,10 +520,12 @@ import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.TRANSLATE;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.TRIM;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.TRUNCATE;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.TUMBLE;
+import static org.apache.calcite.sql.fun.SqlStdOperatorTable.TYPEOF;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.UNARY_MINUS;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.UNARY_PLUS;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.UPPER;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.USER;
+import static org.apache.calcite.sql.fun.SqlStdOperatorTable.VARIANTNULL;
 import static org.apache.calcite.util.ReflectUtil.isStatic;
 
 import static java.util.Objects.requireNonNull;
@@ -866,6 +868,8 @@ public class RexImpTable {
       defineMethod(TRUNC_BIG_QUERY, BuiltInMethod.STRUNCATE.method, 
NullPolicy.STRICT);
       defineMethod(TRUNCATE, BuiltInMethod.STRUNCATE.method, 
NullPolicy.STRICT);
       defineMethod(LOG1P, BuiltInMethod.LOG1P.method, NullPolicy.STRICT);
+      defineMethod(TYPEOF, BuiltInMethod.TYPEOF.method, NullPolicy.STRICT);
+      defineMethod(VARIANTNULL, BuiltInMethod.VARIANTNULL.method, 
NullPolicy.STRICT);
 
       define(SAFE_ADD,
           new SafeArithmeticImplementor(BuiltInMethod.SAFE_ADD.method));
@@ -3825,6 +3829,8 @@ public class RexImpTable {
     // use the general MethodImplementor.
     private AbstractRexCallImplementor getImplementor(SqlTypeName sqlTypeName) 
{
       switch (sqlTypeName) {
+      case VARIANT:
+        return new MethodImplementor(BuiltInMethod.VARIANT_ITEM.method, 
nullPolicy, false);
       case ARRAY:
         return new ArrayItemImplementor();
       case MAP:
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
index 0cebb750e2..d3411654fe 100644
--- 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
+++ 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
@@ -53,6 +53,8 @@ import org.apache.calcite.rex.RexTableInputRef;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.rex.RexVisitor;
 import org.apache.calcite.runtime.SpatialTypeFunctions;
+import org.apache.calcite.runtime.rtti.RuntimeTypeInformation;
+import org.apache.calcite.runtime.variant.VariantValue;
 import org.apache.calcite.schema.FunctionContext;
 import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlOperator;
@@ -66,8 +68,6 @@ import org.apache.calcite.util.BuiltInMethod;
 import org.apache.calcite.util.ControlFlowException;
 import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Util;
-import org.apache.calcite.util.Variant;
-import org.apache.calcite.util.rtti.RuntimeTypeInformation;
 
 import com.google.common.base.CaseFormat;
 import com.google.common.collect.ImmutableList;
@@ -321,10 +321,12 @@ public class RexToLixTranslator implements 
RexVisitor<RexToLixTranslator.Result>
         return defaultExpression.get();
       }
       // Converting a VARIANT to any other type calls the Variant.cast method
+      // First cast operand to a VariantValue (it may be an Object)
+      Expression operandCast = Expressions.convert_(operand, 
VariantValue.class);
       Expression cast =
-          Expressions.call(BuiltInMethod.VARIANT_CAST.method, operand,
+          Expressions.call(operandCast, BuiltInMethod.VARIANT_CAST.method,
               RuntimeTypeInformation.createExpression(targetType));
-      // The cast returns an Object, so we need a convert too
+      // The cast returns an Object, so we need a convert to the expected Java 
type
       RelDataType nullableTarget = 
typeFactory.createTypeWithNullability(targetType, true);
       return Expressions.convert_(cast, 
typeFactory.getJavaClass(nullableTarget));
     }
@@ -333,7 +335,8 @@ public class RexToLixTranslator implements 
RexVisitor<RexToLixTranslator.Result>
     case VARIANT:
       // Converting any type to a VARIANT invokes the Variant constructor
       Expression rtti = RuntimeTypeInformation.createExpression(sourceType);
-      return Expressions.new_(Variant.class, operand, rtti);
+      Expression roundingMode = 
Expressions.constant(typeFactory.getTypeSystem().roundingMode());
+      return Expressions.call(BuiltInMethod.VARIANT_CREATE.method, 
roundingMode, operand, rtti);
     case ANY:
       return operand;
 
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java 
b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
index a3a5248ba5..cb4285e376 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -859,8 +859,7 @@ public class RexBuilder {
       return true;
     }
     final SqlTypeName sqlType = toType.getSqlTypeName();
-    if (sqlType == SqlTypeName.MEASURE
-        || sqlType == SqlTypeName.VARIANT) {
+    if (sqlType == SqlTypeName.MEASURE || sqlType == SqlTypeName.VARIANT) {
       return false;
     }
     if (!RexLiteral.valueMatchesType(value, sqlType, false)) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java 
b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
index 69230a1ec9..d2722719e4 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
@@ -26,6 +26,7 @@ import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.runtime.FlatLists;
 import org.apache.calcite.runtime.SpatialTypeFunctions;
+import org.apache.calcite.runtime.variant.VariantValue;
 import org.apache.calcite.sql.SqlCollation;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlOperator;
@@ -43,7 +44,6 @@ import org.apache.calcite.util.TimeWithTimeZoneString;
 import org.apache.calcite.util.TimestampString;
 import org.apache.calcite.util.TimestampWithTimeZoneString;
 import org.apache.calcite.util.Util;
-import org.apache.calcite.util.Variant;
 
 import com.google.common.collect.ImmutableList;
 
@@ -317,7 +317,7 @@ public class RexLiteral extends RexNode {
     }
     switch (typeName) {
     case VARIANT:
-      return value instanceof Variant;
+      return value instanceof VariantValue;
     case BOOLEAN:
       // Unlike SqlLiteral, we do not allow boolean null.
       return value instanceof Boolean;
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index a5959c3a3a..ba6cf04f73 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -38,6 +38,7 @@ import org.apache.calcite.linq4j.tree.Primitive;
 import org.apache.calcite.rel.type.TimeFrame;
 import org.apache.calcite.rel.type.TimeFrameSet;
 import org.apache.calcite.runtime.FlatLists.ComparableList;
+import org.apache.calcite.runtime.variant.VariantValue;
 import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlUtil;
 import org.apache.calcite.sql.fun.SqlLibraryOperators;
@@ -47,7 +48,6 @@ import org.apache.calcite.util.TimestampWithTimeZoneString;
 import org.apache.calcite.util.TryThreadLocal;
 import org.apache.calcite.util.Unsafe;
 import org.apache.calcite.util.Util;
-import org.apache.calcite.util.Variant;
 import org.apache.calcite.util.format.FormatElement;
 import org.apache.calcite.util.format.FormatModel;
 import org.apache.calcite.util.format.FormatModels;
@@ -5767,8 +5767,8 @@ public class SqlFunctions {
    * known until runtime.
    */
   public static @Nullable Object item(Object object, Object index) {
-    if (object instanceof Variant) {
-      return ((Variant) object).item(index);
+    if (object instanceof VariantValue) {
+      return ((VariantValue) object).item(index);
     }
     if (object instanceof Map) {
       return mapItem((Map) object, index);
diff --git 
a/core/src/main/java/org/apache/calcite/util/rtti/BasicSqlTypeRtti.java 
b/core/src/main/java/org/apache/calcite/runtime/rtti/BasicSqlTypeRtti.java
similarity index 81%
rename from 
core/src/main/java/org/apache/calcite/util/rtti/BasicSqlTypeRtti.java
rename to 
core/src/main/java/org/apache/calcite/runtime/rtti/BasicSqlTypeRtti.java
index 63f7afe60a..cb2281ac68 100644
--- a/core/src/main/java/org/apache/calcite/util/rtti/BasicSqlTypeRtti.java
+++ b/core/src/main/java/org/apache/calcite/runtime/rtti/BasicSqlTypeRtti.java
@@ -14,20 +14,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.calcite.util.rtti;
+package org.apache.calcite.runtime.rtti;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
 
+import java.util.Objects;
+
 /** Runtime type information about a base (primitive) SQL type. */
 public class BasicSqlTypeRtti extends RuntimeTypeInformation {
-  private final int precision;
-  private final int scale;
-
-  public BasicSqlTypeRtti(RuntimeSqlTypeName typeName, int precision, int 
scale) {
+  public BasicSqlTypeRtti(RuntimeSqlTypeName typeName) {
     super(typeName);
     assert typeName.isScalar() : "Base SQL type must be a scalar type " + 
typeName;
-    this.precision = precision;
-    this.scale = scale;
   }
 
   @Override public boolean equals(@Nullable Object o) {
@@ -39,13 +36,11 @@ public class BasicSqlTypeRtti extends 
RuntimeTypeInformation {
     }
 
     BasicSqlTypeRtti that = (BasicSqlTypeRtti) o;
-    return typeName == that.typeName && precision == that.precision && scale 
== that.scale;
+    return typeName == that.typeName;
   }
 
   @Override public int hashCode() {
-    int result = precision;
-    result = 31 * result + scale;
-    return result;
+    return Objects.hashCode(typeName);
   }
 
   @Override public String getTypeString()  {
@@ -62,8 +57,6 @@ public class BasicSqlTypeRtti extends RuntimeTypeInformation {
       return "BIGINT";
     case DECIMAL:
       return "DECIMAL";
-    case FLOAT:
-      return "FLOAT";
     case REAL:
       return "REAL";
     case DOUBLE:
@@ -85,12 +78,8 @@ public class BasicSqlTypeRtti extends RuntimeTypeInformation 
{
     case INTERVAL_LONG:
     case INTERVAL_SHORT:
       return "INTERVAL";
-    case CHAR:
-      return "CHAR";
     case VARCHAR:
       return "VARCHAR";
-    case BINARY:
-      return "BINARY";
     case VARBINARY:
       return "VARBINARY";
     case NULL:
@@ -107,7 +96,6 @@ public class BasicSqlTypeRtti extends RuntimeTypeInformation 
{
   // This method is used to serialize the type in Java code implementations,
   // so it should produce a computation that reconstructs the type at runtime
   @Override public String toString() {
-    return "new BasicSqlTypeRtti("
-        + this.getTypeString() + ", " + this.precision + ", " + this.scale + 
")";
+    return "new BasicSqlTypeRtti(" + this.getTypeString() + ")";
   }
 }
diff --git 
a/core/src/main/java/org/apache/calcite/util/rtti/GenericSqlTypeRtti.java 
b/core/src/main/java/org/apache/calcite/runtime/rtti/GenericSqlTypeRtti.java
similarity index 92%
rename from 
core/src/main/java/org/apache/calcite/util/rtti/GenericSqlTypeRtti.java
rename to 
core/src/main/java/org/apache/calcite/runtime/rtti/GenericSqlTypeRtti.java
index 25f3be33e8..7c8893b3a8 100644
--- a/core/src/main/java/org/apache/calcite/util/rtti/GenericSqlTypeRtti.java
+++ b/core/src/main/java/org/apache/calcite/runtime/rtti/GenericSqlTypeRtti.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.calcite.util.rtti;
+package org.apache.calcite.runtime.rtti;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
 
@@ -72,4 +72,12 @@ public class GenericSqlTypeRtti extends 
RuntimeTypeInformation {
   @Override public int hashCode() {
     return Arrays.hashCode(typeArguments);
   }
+
+  public RuntimeTypeInformation getTypeArgument(int index) {
+    return typeArguments[index];
+  }
+
+  public int getArgumentCount() {
+    return typeArguments.length;
+  }
 }
diff --git 
a/core/src/main/java/org/apache/calcite/util/rtti/RowSqlTypeRtti.java 
b/core/src/main/java/org/apache/calcite/runtime/rtti/RowSqlTypeRtti.java
similarity index 64%
rename from core/src/main/java/org/apache/calcite/util/rtti/RowSqlTypeRtti.java
rename to core/src/main/java/org/apache/calcite/runtime/rtti/RowSqlTypeRtti.java
index d876e7ea76..035c7aeb02 100644
--- a/core/src/main/java/org/apache/calcite/util/rtti/RowSqlTypeRtti.java
+++ b/core/src/main/java/org/apache/calcite/runtime/rtti/RowSqlTypeRtti.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.calcite.util.rtti;
+package org.apache.calcite.runtime.rtti;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
 
@@ -23,12 +23,12 @@ import java.util.Map;
 
 /** Runtime type information for a ROW type. */
 public class RowSqlTypeRtti extends RuntimeTypeInformation {
-  private final Map.Entry<String, RuntimeTypeInformation>[] fieldNames;
+  private final Map.Entry<String, RuntimeTypeInformation>[] fields;
 
   @SafeVarargs
-  public RowSqlTypeRtti(Map.Entry<String, RuntimeTypeInformation>... 
fieldNames) {
+  public RowSqlTypeRtti(Map.Entry<String, RuntimeTypeInformation>... fields) {
     super(RuntimeSqlTypeName.ROW);
-    this.fieldNames = fieldNames;
+    this.fields = fields;
   }
 
   @Override public String getTypeString()  {
@@ -41,7 +41,7 @@ public class RowSqlTypeRtti extends RuntimeTypeInformation {
     StringBuilder builder = new StringBuilder();
     builder.append("new RowSqlTypeRtti(");
     boolean first = true;
-    for (Map.Entry<String, RuntimeTypeInformation> arg : this.fieldNames) {
+    for (Map.Entry<String, RuntimeTypeInformation> arg : this.fields) {
       if (!first) {
         builder.append(", ");
       }
@@ -61,10 +61,42 @@ public class RowSqlTypeRtti extends RuntimeTypeInformation {
     }
 
     RowSqlTypeRtti that = (RowSqlTypeRtti) o;
-    return Arrays.equals(fieldNames, that.fieldNames);
+    return Arrays.equals(fields, that.fields);
   }
 
   @Override public int hashCode() {
-    return Arrays.hashCode(fieldNames);
+    return Arrays.hashCode(fields);
+  }
+
+  /** Get the field with the specified index. */
+  public Map.Entry<String, RuntimeTypeInformation> getField(int index) {
+    return this.fields[index];
+  }
+
+  public int size() {
+    return this.fields.length;
+  }
+
+  /** Return the runtime type information of the associated field,
+   * or null if no such field exists.
+   *
+   * @param index Field index, starting from 0
+   */
+  public @Nullable RuntimeTypeInformation getFieldType(Object index) {
+    if (index instanceof Integer) {
+      int intIndex = (Integer) index;
+      if (intIndex < 0 || intIndex >= this.fields.length) {
+        return null;
+      }
+      return this.fields[intIndex].getValue();
+    } else if (index instanceof String) {
+      String stringIndex = (String) index;
+      for (Map.Entry<String, RuntimeTypeInformation> field : this.fields) {
+        if (field.getKey().equalsIgnoreCase(stringIndex)) {
+          return field.getValue();
+        }
+      }
+    }
+    return null;
   }
 }
diff --git 
a/core/src/main/java/org/apache/calcite/util/rtti/RuntimeTypeInformation.java 
b/core/src/main/java/org/apache/calcite/runtime/rtti/RuntimeTypeInformation.java
similarity index 72%
rename from 
core/src/main/java/org/apache/calcite/util/rtti/RuntimeTypeInformation.java
rename to 
core/src/main/java/org/apache/calcite/runtime/rtti/RuntimeTypeInformation.java
index 73527408d7..2bb9bed6a5 100644
--- 
a/core/src/main/java/org/apache/calcite/util/rtti/RuntimeTypeInformation.java
+++ 
b/core/src/main/java/org/apache/calcite/runtime/rtti/RuntimeTypeInformation.java
@@ -14,21 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.calcite.util.rtti;
+package org.apache.calcite.runtime.rtti;
 
 import org.apache.calcite.linq4j.tree.Expression;
 import org.apache.calcite.linq4j.tree.Expressions;
+import org.apache.calcite.linq4j.tree.Primitive;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
 
+import org.checkerframework.checker.nullness.qual.Nullable;
+
 import java.util.AbstractMap;
 
 import static java.util.Objects.requireNonNull;
 
 /**
- * This class represents the type of a SQL expression at runtime.
+ * The type of a SQL expression at runtime.
  * Normally SQL is a statically-typed language, and there is no need for
- * runtime-type information.  However, the VARIANT data type is actually
+ * runtime-type information. However, the VARIANT data type is actually
  * a dynamically-typed value, and needs this kind of information.
  * We cannot use the very similar RelDataType type since it carries extra
  * baggage, like the type system, which is not available at runtime. */
@@ -41,8 +44,8 @@ public abstract class RuntimeTypeInformation {
     INTEGER(false),
     BIGINT(false),
     DECIMAL(false),
-    FLOAT(false),
     REAL(false),
+    // FLOAT is represented as DOUBLE
     DOUBLE(false),
     DATE(false),
     TIME(false),
@@ -53,9 +56,11 @@ public abstract class RuntimeTypeInformation {
     TIMESTAMP_TZ(false),
     INTERVAL_LONG(false),
     INTERVAL_SHORT(false),
-    CHAR(false),
+    // "Name" is used for structure field names
+    NAME(false),
+    // CHAR is represented as VARCHAR
     VARCHAR(false),
-    BINARY(false),
+    // BINARY is represented as VARBINARY
     VARBINARY(false),
     NULL(false),
     MULTISET(true),
@@ -93,71 +98,92 @@ public abstract class RuntimeTypeInformation {
     return this.typeName.isScalar();
   }
 
+  /** If this type is a Primitive, return it, otherwise return null. */
+  public @Nullable Primitive asPrimitive() {
+    switch (typeName) {
+    case BOOLEAN:
+      return Primitive.BOOLEAN;
+    case TINYINT:
+      return Primitive.BYTE;
+    case SMALLINT:
+      return Primitive.SHORT;
+    case INTEGER:
+      return Primitive.INT;
+    case BIGINT:
+      return Primitive.LONG;
+    case REAL:
+      return Primitive.FLOAT;
+    case DOUBLE:
+      return Primitive.DOUBLE;
+    default:
+      return null;
+    }
+  }
+
+  public GenericSqlTypeRtti asGeneric() {
+    assert this instanceof GenericSqlTypeRtti;
+    return (GenericSqlTypeRtti) this;
+  }
+
   /**
-   * Create and return an expression that creates a runtime type that
+   * Creates and returns an expression that creates a runtime type that
    * reflects the information in the statically-known type 'type'.
    *
    * @param type The static type of an expression.
    */
   public static Expression createExpression(RelDataType type) {
-    final Expression precision = Expressions.constant(type.getPrecision());
-    final Expression scale = Expressions.constant(type.getScale());
     switch (type.getSqlTypeName()) {
     case BOOLEAN:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.BOOLEAN), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.BOOLEAN));
     case TINYINT:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.TINYINT), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.TINYINT));
     case SMALLINT:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.SMALLINT), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.SMALLINT));
     case INTEGER:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.INTEGER), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.INTEGER));
     case BIGINT:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.BIGINT), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.BIGINT));
     case DECIMAL:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.DECIMAL), precision, scale);
-    case FLOAT:
-      return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.FLOAT), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.DECIMAL));
     case REAL:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.REAL), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.REAL));
+    case FLOAT:
     case DOUBLE:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.DOUBLE), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.DOUBLE));
     case DATE:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.DATE), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.DATE));
     case TIME:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.TIME), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.TIME));
     case TIME_WITH_LOCAL_TIME_ZONE:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.TIME_WITH_LOCAL_TIME_ZONE),
-          precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.TIME_WITH_LOCAL_TIME_ZONE));
     case TIME_TZ:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.TIME_TZ), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.TIME_TZ));
     case TIMESTAMP:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.TIMESTAMP), precision, 
scale);
+          Expressions.constant(RuntimeSqlTypeName.TIMESTAMP));
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          
Expressions.constant(RuntimeSqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE),
-          precision, scale);
+          
Expressions.constant(RuntimeSqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE));
     case TIMESTAMP_TZ:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.TIMESTAMP_TZ), precision, 
scale);
+          Expressions.constant(RuntimeSqlTypeName.TIMESTAMP_TZ));
     case INTERVAL_YEAR:
     case INTERVAL_YEAR_MONTH:
     case INTERVAL_MONTH:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.INTERVAL_LONG), precision, 
scale);
+          Expressions.constant(RuntimeSqlTypeName.INTERVAL_LONG));
     case INTERVAL_DAY:
     case INTERVAL_DAY_HOUR:
     case INTERVAL_DAY_MINUTE:
@@ -169,22 +195,18 @@ public abstract class RuntimeTypeInformation {
     case INTERVAL_MINUTE_SECOND:
     case INTERVAL_SECOND:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.INTERVAL_SHORT), precision, 
scale);
+          Expressions.constant(RuntimeSqlTypeName.INTERVAL_SHORT));
     case CHAR:
-      return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.CHAR), precision, scale);
     case VARCHAR:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.VARCHAR), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.VARCHAR));
     case BINARY:
-      return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.BINARY), precision, scale);
     case VARBINARY:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.VARBINARY), precision, 
scale);
+          Expressions.constant(RuntimeSqlTypeName.VARBINARY));
     case NULL:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.NULL), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.NULL));
     case MULTISET: {
       Expression comp = 
createExpression(requireNonNull(type.getComponentType()));
       return Expressions.new_(GenericSqlTypeRtti.class,
@@ -196,8 +218,8 @@ public abstract class RuntimeTypeInformation {
           Expressions.constant(RuntimeSqlTypeName.ARRAY), comp);
     }
     case MAP: {
-      Expression key = createExpression(requireNonNull(type.getValueType()));
-      Expression value = createExpression(requireNonNull(type.getKeyType()));
+      Expression key = createExpression(requireNonNull(type.getKeyType()));
+      Expression value = createExpression(requireNonNull(type.getValueType()));
       return Expressions.new_(GenericSqlTypeRtti.class,
           Expressions.constant(RuntimeSqlTypeName.MAP), key, value);
     }
@@ -217,10 +239,10 @@ public abstract class RuntimeTypeInformation {
     }
     case GEOMETRY:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.GEOMETRY), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.GEOMETRY));
     case VARIANT:
       return Expressions.new_(BasicSqlTypeRtti.class,
-          Expressions.constant(RuntimeSqlTypeName.VARIANT), precision, scale);
+          Expressions.constant(RuntimeSqlTypeName.VARIANT));
     default:
       throw new RuntimeException("Unexpected type " + type);
     }
diff --git a/core/src/main/java/org/apache/calcite/util/rtti/package-info.java 
b/core/src/main/java/org/apache/calcite/runtime/rtti/package-info.java
similarity index 95%
copy from core/src/main/java/org/apache/calcite/util/rtti/package-info.java
copy to core/src/main/java/org/apache/calcite/runtime/rtti/package-info.java
index 7113279765..548b4ba624 100644
--- a/core/src/main/java/org/apache/calcite/util/rtti/package-info.java
+++ b/core/src/main/java/org/apache/calcite/runtime/rtti/package-info.java
@@ -18,4 +18,4 @@
 /**
  * Support for runtime type information.
  */
-package org.apache.calcite.util.rtti;
+package org.apache.calcite.runtime.rtti;
diff --git 
a/core/src/main/java/org/apache/calcite/runtime/variant/VariantNonNull.java 
b/core/src/main/java/org/apache/calcite/runtime/variant/VariantNonNull.java
new file mode 100644
index 0000000000..939785f74f
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantNonNull.java
@@ -0,0 +1,452 @@
+/*
+ * 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.calcite.runtime.variant;
+
+import org.apache.calcite.linq4j.tree.Primitive;
+import org.apache.calcite.runtime.SqlFunctions;
+import org.apache.calcite.runtime.rtti.BasicSqlTypeRtti;
+import org.apache.calcite.runtime.rtti.RowSqlTypeRtti;
+import org.apache.calcite.runtime.rtti.RuntimeTypeInformation;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static 
org.apache.calcite.runtime.rtti.RuntimeTypeInformation.RuntimeSqlTypeName.NAME;
+
+import static java.util.Objects.requireNonNull;
+
+/** A VARIANT value that contains a non-null value. */
+public class VariantNonNull extends VariantSqlValue {
+  final RoundingMode roundingMode;
+  /** Actual value - can have any SQL type. */
+  final Object value;
+
+  VariantNonNull(RoundingMode roundingMode, Object value, 
RuntimeTypeInformation runtimeType) {
+    super(runtimeType.getTypeName());
+    this.roundingMode = roundingMode;
+    // sanity check
+    switch (runtimeType.getTypeName()) {
+    case NAME:
+      assert value instanceof String;
+      this.value = value;
+      break;
+    case BOOLEAN:
+      assert value instanceof Boolean;
+      this.value = value;
+      break;
+    case TINYINT:
+      assert value instanceof Byte;
+      this.value = value;
+      break;
+    case SMALLINT:
+      assert value instanceof Short;
+      this.value = value;
+      break;
+    case INTEGER:
+      assert value instanceof Integer;
+      this.value = value;
+      break;
+    case BIGINT:
+      assert value instanceof Long;
+      this.value = value;
+      break;
+    case DECIMAL:
+      assert value instanceof BigDecimal;
+      this.value = value;
+      break;
+    case REAL:
+      assert value instanceof Float;
+      this.value = value;
+      break;
+    case DOUBLE:
+      assert value instanceof Double;
+      this.value = value;
+      break;
+    case DATE:
+    case TIME:
+    case TIME_WITH_LOCAL_TIME_ZONE:
+    case TIME_TZ:
+    case TIMESTAMP:
+    case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+    case TIMESTAMP_TZ:
+    case INTERVAL_LONG:
+    case INTERVAL_SHORT:
+      this.value = value;
+      break;
+    case VARCHAR:
+      this.value = value;
+      assert value instanceof String;
+      break;
+    case NULL:
+    default:
+      throw new RuntimeException("Unreachable");
+    case VARBINARY:
+    case GEOMETRY:
+    case VARIANT:
+      this.value = value;
+      break;
+    case MAP: {
+      RuntimeTypeInformation keyType = 
runtimeType.asGeneric().getTypeArgument(0);
+      RuntimeTypeInformation valueType = 
runtimeType.asGeneric().getTypeArgument(1);
+      assert value instanceof Map<?, ?>;
+      Map<?, ?> map = (Map<?, ?>) value;
+      LinkedHashMap<VariantValue, VariantValue> converted = new 
LinkedHashMap<>(map.size());
+      for (Map.Entry<?, ?> o : map.entrySet()) {
+        VariantValue key = VariantSqlValue.create(roundingMode, o.getKey(), 
keyType);
+        VariantValue val = VariantSqlValue.create(roundingMode, o.getValue(), 
valueType);
+        converted.put(key, val);
+      }
+      this.value = converted;
+      break;
+    }
+    case ROW: {
+      assert value instanceof Object[];
+      Object[] a = (Object[]) value;
+      assert runtimeType instanceof RowSqlTypeRtti;
+      RowSqlTypeRtti rowType = (RowSqlTypeRtti) runtimeType;
+      LinkedHashMap<VariantValue, VariantValue> converted = new 
LinkedHashMap<>(a.length);
+      RuntimeTypeInformation name = new BasicSqlTypeRtti(NAME);
+      for (int i = 0; i < a.length; i++) {
+        Map.Entry<String, RuntimeTypeInformation> fieldType = 
rowType.getField(i);
+        VariantValue key = VariantSqlValue.create(roundingMode, 
fieldType.getKey(), name);
+        VariantValue val = VariantSqlValue.create(roundingMode, a[i], 
fieldType.getValue());
+        converted.put(key, val);
+      }
+      this.value = converted;
+      break;
+    }
+    case MULTISET:
+    case ARRAY: {
+      RuntimeTypeInformation elementType = 
runtimeType.asGeneric().getTypeArgument(0);
+      assert value instanceof List<?>;
+      List<?> list = (List<?>) value;
+      List<VariantValue> converted = new ArrayList<>(list.size());
+      for (Object o : list) {
+        VariantValue element = VariantSqlValue.create(roundingMode, o, 
elementType);
+        converted.add(element);
+      }
+      this.value = converted;
+      break;
+    }
+    }
+  }
+
+  @Override public boolean equals(@Nullable Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    VariantNonNull variant = (VariantNonNull) o;
+    return Objects.equals(value, variant.value)
+        && runtimeType == variant.runtimeType;
+  }
+
+  @Override public int hashCode() {
+    int result = Objects.hashCode(value);
+    result = 31 * result + runtimeType.hashCode();
+    return result;
+  }
+
+  /** Cast this value to the specified type.  Currently, the rule is:
+   * if the value has the specified type, the value field is returned, 
otherwise a SQL
+   * NULL is returned. */
+  // This method is invoked from {@link RexToLixTranslator} VARIANT_CAST
+  @Override public @Nullable Object cast(RuntimeTypeInformation type) {
+    if (this.runtimeType.isScalar()) {
+      if (this.runtimeType == type.getTypeName()) {
+        return this.value;
+      } else {
+        // Convert numeric values
+        @Nullable Primitive target = type.asPrimitive();
+        switch (this.runtimeType) {
+        case TINYINT: {
+          byte b = (byte) value;
+          switch (type.getTypeName()) {
+          case TINYINT:
+          case SMALLINT:
+          case INTEGER:
+          case BIGINT:
+          case REAL:
+          case DOUBLE:
+            return requireNonNull(target, "target").numberValue(b, 
roundingMode);
+          case DECIMAL:
+            return BigDecimal.valueOf(b);
+          default:
+            break;
+          }
+          break;
+        }
+        case SMALLINT: {
+          short s = (short) value;
+          switch (type.getTypeName()) {
+          case TINYINT:
+          case SMALLINT:
+          case INTEGER:
+          case BIGINT:
+          case REAL:
+          case DOUBLE:
+            return requireNonNull(target, "target").numberValue(s, 
roundingMode);
+          case DECIMAL:
+            return BigDecimal.valueOf(s);
+          default:
+            break;
+          }
+          break;
+        }
+        case INTEGER: {
+          int i = (int) value;
+          switch (type.getTypeName()) {
+          case TINYINT:
+          case SMALLINT:
+          case INTEGER:
+          case BIGINT:
+          case REAL:
+          case DOUBLE:
+            return requireNonNull(target, "target").numberValue(i, 
roundingMode);
+          case DECIMAL:
+            return BigDecimal.valueOf(i);
+          default:
+            break;
+          }
+          break;
+        }
+        case BIGINT: {
+          long l = (int) value;
+          switch (type.getTypeName()) {
+          case TINYINT:
+          case SMALLINT:
+          case INTEGER:
+          case BIGINT:
+          case REAL:
+          case DOUBLE:
+            return requireNonNull(target, "target").numberValue(l, 
roundingMode);
+          case DECIMAL:
+            return BigDecimal.valueOf(l);
+          default:
+            break;
+          }
+          break;
+        }
+        case DECIMAL: {
+          BigDecimal d = (BigDecimal) value;
+          switch (type.getTypeName()) {
+          case TINYINT:
+          case SMALLINT:
+          case INTEGER:
+          case BIGINT:
+          case REAL:
+          case DOUBLE:
+            return requireNonNull(target, "target").numberValue(d, 
roundingMode);
+          case DECIMAL:
+            return d;
+          default:
+            break;
+          }
+          break;
+        }
+        case REAL: {
+          float f = (float) value;
+          switch (type.getTypeName()) {
+          case TINYINT:
+          case SMALLINT:
+          case INTEGER:
+          case BIGINT:
+          case REAL:
+          case DOUBLE:
+            return requireNonNull(target, "target").numberValue(f, 
roundingMode);
+          case DECIMAL:
+            return BigDecimal.valueOf(f);
+          default:
+            break;
+          }
+          break;
+        }
+        case DOUBLE: {
+          double d = (double) value;
+          switch (type.getTypeName()) {
+          case TINYINT:
+          case SMALLINT:
+          case INTEGER:
+          case BIGINT:
+          case REAL:
+          case DOUBLE:
+            return requireNonNull(target, "target").numberValue(d, 
roundingMode);
+          case DECIMAL:
+            return BigDecimal.valueOf(d);
+          default:
+            break;
+          }
+          break;
+        }
+        default:
+          break;
+        }
+        return null;
+      }
+    } else {
+      switch (this.runtimeType) {
+      case ARRAY:
+        if (type.getTypeName() == 
RuntimeTypeInformation.RuntimeSqlTypeName.ARRAY) {
+          RuntimeTypeInformation elementType = 
type.asGeneric().getTypeArgument(0);
+          assert value instanceof List;
+          List<VariantSqlValue> list = (List<VariantSqlValue>) value;
+          List<@Nullable Object> result = new ArrayList<>(list.size());
+          for (VariantSqlValue o : list) {
+            @Nullable Object converted = o.cast(elementType);
+            result.add(converted);
+          }
+          return result;
+        }
+        break;
+      case MAP:
+        assert value instanceof Map;
+        Map<VariantSqlValue, VariantSqlValue> map = (Map<VariantSqlValue, 
VariantSqlValue>) value;
+        if (type.getTypeName() == 
RuntimeTypeInformation.RuntimeSqlTypeName.MAP) {
+          // Convert map to map: cast keys and values recursively
+          RuntimeTypeInformation keyType = type.asGeneric().getTypeArgument(0);
+          RuntimeTypeInformation valueType = 
type.asGeneric().getTypeArgument(0);
+          LinkedHashMap<@Nullable Object, @Nullable Object> result =
+              new LinkedHashMap<>(map.size());
+          for (Map.Entry<VariantSqlValue, VariantSqlValue> e : map.entrySet()) 
{
+            @Nullable Object key = e.getKey().cast(keyType);
+            @Nullable Object value = e.getValue().cast(valueType);
+            result.put(key, value);
+          }
+          return result;
+        } else if (type.getTypeName() == 
RuntimeTypeInformation.RuntimeSqlTypeName.ROW) {
+          // Convert map to row: lookup the row's fields in the map
+          RowSqlTypeRtti rowType = (RowSqlTypeRtti) type;
+          @Nullable Object [] result = new Object[rowType.size()];
+          for (int i = 0; i < rowType.size(); i++) {
+            Map.Entry<String, RuntimeTypeInformation> field = 
rowType.getField(i);
+            Object fieldValue = null;
+            VariantValue v = this.item(field.getKey());
+            if (v != null) {
+              fieldValue = v.cast(field.getValue());
+            }
+            result[i] = fieldValue;
+          }
+          return result;
+        }
+        break;
+      default:
+        break;
+      }
+    }
+    return null;
+  }
+
+  // Implementation of the array index operator for VARIANT values
+  @Override public @Nullable VariantValue item(Object index) {
+    boolean isInteger = index instanceof Integer;
+    switch (this.runtimeType) {
+    case ROW:
+      if (index instanceof String) {
+        RuntimeTypeInformation string =
+            new 
BasicSqlTypeRtti(RuntimeTypeInformation.RuntimeSqlTypeName.NAME);
+        index = VariantSqlValue.create(roundingMode, index, string);
+      }
+      break;
+    case MAP:
+      if (index instanceof String) {
+        RuntimeTypeInformation string =
+            new 
BasicSqlTypeRtti(RuntimeTypeInformation.RuntimeSqlTypeName.VARCHAR);
+        index = VariantSqlValue.create(roundingMode, index, string);
+      } else if (isInteger) {
+        RuntimeTypeInformation i =
+            new 
BasicSqlTypeRtti(RuntimeTypeInformation.RuntimeSqlTypeName.INTEGER);
+        index = VariantSqlValue.create(roundingMode, index, i);
+      }
+      break;
+    case ARRAY:
+      if (!isInteger) {
+        // Arrays only support integer indexes
+        return null;
+      }
+      break;
+    default:
+      return null;
+    }
+
+    // If index is VARIANT, leave it unchanged
+    Object result = SqlFunctions.itemOptional(this.value, index);
+    if (result == null) {
+      return null;
+    }
+    // If result is a variant, return as is
+    if (result instanceof VariantValue) {
+      return (VariantValue) result;
+    }
+    return null;
+  }
+
+  // This method is called by the testing code.
+  @Override public String toString() {
+    if (this.runtimeType == RuntimeTypeInformation.RuntimeSqlTypeName.ROW) {
+      if (value instanceof Map<?, ?>) {
+        // Do not print field names, only their values
+        Map<?, ?> map = (Map<?, ?>) value;
+        StringBuilder buf = new StringBuilder("{");
+
+        boolean first = true;
+        for (Map.Entry<?, ?> o : map.entrySet()) {
+          if (!first) {
+            buf.append(", ");
+          }
+          first = false;
+          if (o.getValue() != null) {
+            // This should always be true
+            buf.append(o.getValue());
+          }
+        }
+        buf.append("}");
+        return buf.toString();
+      }
+    }
+    String quote = "";
+    switch (this.runtimeType) {
+    case TIME:
+    case TIME_WITH_LOCAL_TIME_ZONE:
+    case TIME_TZ:
+    case TIMESTAMP:
+    case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+    case TIMESTAMP_TZ:
+    case INTERVAL_LONG:
+    case INTERVAL_SHORT:
+    case VARCHAR:
+    case VARBINARY:
+      // At least in Snowflake VARIANT values that are strings
+      // are printed with double quotes
+      // https://docs.snowflake.com/en/sql-reference/data-types-semistructured
+      quote = "\"";
+      break;
+    default:
+      break;
+    }
+    return quote + value + quote;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/calcite/runtime/variant/VariantNull.java 
b/core/src/main/java/org/apache/calcite/runtime/variant/VariantNull.java
new file mode 100644
index 0000000000..a11dbbed6a
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantNull.java
@@ -0,0 +1,64 @@
+/*
+ * 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.calcite.runtime.variant;
+
+import org.apache.calcite.runtime.rtti.RuntimeTypeInformation;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** The VARIANT type has its own notion of null, which is
+ * different from the SQL NULL value.  For example, two variant nulls are equal
+ * with each other.  This class represents such values. */
+public class VariantNull extends VariantValue {
+  // Protected constructor to enforce a singleton pattern
+  protected VariantNull() {}
+
+  @Override public String getTypeString() {
+    return "VARIANT";
+  }
+
+  @Override public @Nullable Object cast(RuntimeTypeInformation type) {
+    // Result is always null
+    return null;
+  }
+
+  @Override public @Nullable Object item(Object index) {
+    // Result is always null
+    return null;
+  }
+
+  public static final VariantNull INSTANCE = new VariantNull();
+
+  /** Get the single instance of this type. */
+  // Called from BuiltInMethod.VARIANTNULL
+  public static VariantNull getInstance() {
+    return INSTANCE;
+  }
+
+  @Override public int hashCode() {
+    return 0;
+  }
+
+  @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
+  @Override public boolean equals(@Nullable Object other) {
+    return other == INSTANCE;
+  }
+
+  @Override public String toString() {
+    return "null";
+  }
+}
diff --git 
a/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlNull.java 
b/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlNull.java
new file mode 100644
index 0000000000..85d424461d
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlNull.java
@@ -0,0 +1,60 @@
+/*
+ * 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.calcite.runtime.variant;
+
+import org.apache.calcite.runtime.rtti.RuntimeTypeInformation;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.Objects;
+
+/** A VARIANT value that contains a NULL runtime value. */
+public class VariantSqlNull extends VariantSqlValue {
+  VariantSqlNull(RuntimeTypeInformation.RuntimeSqlTypeName runtimeType) {
+    super(runtimeType);
+  }
+
+  @Override public @Nullable Object item(Object index) {
+    // Result is always null
+    return null;
+  }
+
+  @Override public @Nullable Object cast(RuntimeTypeInformation type) {
+    // Result is always null
+    return null;
+  }
+
+  @Override public String toString() {
+    return "NULL";
+  }
+
+  @Override public int hashCode() {
+    return Objects.hashCode(runtimeType);
+  }
+
+  @Override public boolean equals(@Nullable Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    VariantSqlNull variant = (VariantSqlNull) o;
+    return runtimeType == variant.runtimeType;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlValue.java 
b/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlValue.java
new file mode 100644
index 0000000000..36dc86a26d
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantSqlValue.java
@@ -0,0 +1,58 @@
+/*
+ * 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.calcite.runtime.variant;
+
+import org.apache.calcite.runtime.rtti.RuntimeTypeInformation;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.math.RoundingMode;
+
+/** A value of VARIANT type that represents a SQL value
+ * (The VARIANT type also has a null value which is different
+ * from any other SQL value). */
+public abstract class VariantSqlValue extends VariantValue {
+  final RuntimeTypeInformation.RuntimeSqlTypeName runtimeType;
+
+  protected VariantSqlValue(RuntimeTypeInformation.RuntimeSqlTypeName 
runtimeType) {
+    this.runtimeType = runtimeType;
+  }
+
+  @Override public String getTypeString() {
+    return this.runtimeType.toString();
+  }
+
+  /**
+   * Create a VariantValue from a specified SQL value and the runtime type 
information.
+   *
+   * @param object  SQL runtime value.
+   * @param roundingMode  Rounding mode used for converting numeric values.
+   * @param type    Runtime type information.
+   * @return        The created VariantValue.
+   */
+  // Normally this method should be in the VariantValue class, but the Janino
+  // compiler used by Calcite compiles to a Java version that does not
+  // support static methods in interfaces.
+  // This method is called from BuiltInMethods.VARIANT_CREATE.
+  public static VariantValue create(
+      RoundingMode roundingMode, @Nullable Object object, 
RuntimeTypeInformation type) {
+    if (object == null) {
+      return new VariantSqlNull(type.getTypeName());
+    }
+    return new VariantNonNull(roundingMode, object, type);
+  }
+}
diff --git 
a/core/src/main/java/org/apache/calcite/runtime/variant/VariantValue.java 
b/core/src/main/java/org/apache/calcite/runtime/variant/VariantValue.java
new file mode 100644
index 0000000000..483b34db66
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/runtime/variant/VariantValue.java
@@ -0,0 +1,44 @@
+/*
+ * 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.calcite.runtime.variant;
+
+import org.apache.calcite.runtime.rtti.RuntimeTypeInformation;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Base class for the runtime support for values of the VARIANT SQL type. */
+public abstract class VariantValue {
+  // We made this an abstract class rather than an interface,
+  // because Janino does not like static methods in interfaces.
+  public static @Nullable String getTypeString(Object object) {
+    if (object instanceof VariantValue) {
+      return ((VariantValue) object).getTypeString();
+    }
+    return null;
+  }
+
+  /** A string describing the runtime type information of this value. */
+  public abstract String getTypeString();
+
+  /** Cast this value to the specified type.  Currently, the rule is:
+   * if the value has the specified type, the value field is returned, 
otherwise a SQL
+   * NULL is returned. */
+  public abstract @Nullable Object cast(RuntimeTypeInformation type);
+
+  // Implementation of the array index operator for VARIANT values
+  public abstract @Nullable Object item(Object index);
+}
diff --git a/core/src/main/java/org/apache/calcite/util/rtti/package-info.java 
b/core/src/main/java/org/apache/calcite/runtime/variant/package-info.java
similarity index 88%
rename from core/src/main/java/org/apache/calcite/util/rtti/package-info.java
rename to 
core/src/main/java/org/apache/calcite/runtime/variant/package-info.java
index 7113279765..f8910457bf 100644
--- a/core/src/main/java/org/apache/calcite/util/rtti/package-info.java
+++ b/core/src/main/java/org/apache/calcite/runtime/variant/package-info.java
@@ -16,6 +16,6 @@
  */
 
 /**
- * Support for runtime type information.
+ * Runtime support for values of the VARIANT data type.
  */
-package org.apache.calcite.util.rtti;
+package org.apache.calcite.runtime.variant;
diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
index 6aba1cdaa4..9e0ca8bb0a 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
@@ -1935,6 +1935,16 @@ public class SqlStdOperatorTable extends 
ReflectiveSqlOperatorTable {
           SqlFunctionCategory.NUMERIC)
           .withSyntax(SqlSyntax.FUNCTION_ID_CONSTANT);
 
+  /** The {@code TYPEOF} function. */
+  public static final SqlFunction TYPEOF =
+      SqlBasicFunction.create("TYPEOF", ReturnTypes.VARCHAR, 
OperandTypes.VARIANT,
+              SqlFunctionCategory.STRING);
+
+  /** The {@code VARIANTNULL} function. */
+  public static final SqlFunction VARIANTNULL =
+      SqlBasicFunction.create("VARIANTNULL", ReturnTypes.VARIANT, 
OperandTypes.NILADIC,
+          SqlFunctionCategory.SYSTEM);
+
   /** {@code FIRST} function to be used within {@code MATCH_RECOGNIZE}. */
   public static final SqlFunction FIRST =
       SqlBasicFunction.create(SqlKind.FIRST,
diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java 
b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
index 5a82308e91..6e129f5888 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
@@ -401,6 +401,9 @@ public abstract class OperandTypes {
   public static final SqlSingleOperandTypeChecker INTEGER_INTEGER =
       family(SqlTypeFamily.INTEGER, SqlTypeFamily.INTEGER);
 
+  public static final SqlSingleOperandTypeChecker VARIANT =
+      family(SqlTypeFamily.VARIANT);
+
   public static final SqlSingleOperandTypeChecker NUMERIC_OPTIONAL_NUMERIC =
       family(ImmutableList.of(SqlTypeFamily.NUMERIC, SqlTypeFamily.NUMERIC),
           // Second operand optional (operand index 0, 1)
diff --git a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java 
b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
index 12849a60db..c61aebd0b2 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
@@ -532,6 +532,12 @@ public abstract class ReturnTypes {
   public static final SqlReturnTypeInference VARCHAR =
       ReturnTypes.explicit(SqlTypeName.VARCHAR);
 
+  /**
+   * Type-inference strategy that always returns "VARIANT".
+   */
+  public static final SqlReturnTypeInference VARIANT =
+      ReturnTypes.explicit(SqlTypeName.VARIANT);
+
   /**
    * Type-inference strategy that always returns "VARCHAR" with nulls
    * allowed if any of the operands allow nulls.
diff --git 
a/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java 
b/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java
index d7754b4801..f2a44602d1 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java
@@ -60,7 +60,11 @@ public class ConvertToChecked extends RelHomogeneousShuttle {
 
     @Override public RexNode visitSubQuery(RexSubQuery subQuery) {
       RelNode result = subQuery.rel.accept(ConvertToChecked.this);
-      return subQuery.clone(result);
+      if (result != subQuery.rel) {
+        return subQuery.clone(result);
+      } else {
+        return subQuery;
+      }
     }
 
     @Override public RexNode visitCall(final RexCall call) {
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java 
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index ab6e2c3ce0..6ea6701a65 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -101,6 +101,10 @@ import 
org.apache.calcite.runtime.SqlFunctions.FlatProductInputType;
 import org.apache.calcite.runtime.UrlFunctions;
 import org.apache.calcite.runtime.Utilities;
 import org.apache.calcite.runtime.XmlFunctions;
+import org.apache.calcite.runtime.rtti.RuntimeTypeInformation;
+import org.apache.calcite.runtime.variant.VariantNull;
+import org.apache.calcite.runtime.variant.VariantSqlValue;
+import org.apache.calcite.runtime.variant.VariantValue;
 import org.apache.calcite.schema.FilterableTable;
 import org.apache.calcite.schema.ModifiableTable;
 import org.apache.calcite.schema.ProjectableFilterableTable;
@@ -116,7 +120,6 @@ import org.apache.calcite.sql.SqlJsonExistsErrorBehavior;
 import org.apache.calcite.sql.SqlJsonQueryEmptyOrErrorBehavior;
 import org.apache.calcite.sql.SqlJsonQueryWrapperBehavior;
 import org.apache.calcite.sql.SqlJsonValueEmptyOrErrorBehavior;
-import org.apache.calcite.util.rtti.RuntimeTypeInformation;
 
 import com.google.common.collect.ImmutableMap;
 
@@ -937,7 +940,12 @@ public enum BuiltInMethod {
   BIG_DECIMAL_ADD(BigDecimal.class, "add", BigDecimal.class),
   BIG_DECIMAL_NEGATE(BigDecimal.class, "negate"),
   COMPARE_TO(Comparable.class, "compareTo", Object.class),
-  VARIANT_CAST(Variant.class, "cast", Object.class, 
RuntimeTypeInformation.class);
+  VARIANT_CREATE(VariantSqlValue.class, "create", RoundingMode.class,
+      Object.class, RuntimeTypeInformation.class),
+  VARIANT_CAST(VariantValue.class, "cast", RuntimeTypeInformation.class),
+  TYPEOF(VariantValue.class, "getTypeString", VariantValue.class),
+  VARIANT_ITEM(SqlFunctions.class, "item", VariantValue.class, Object.class),
+  VARIANTNULL(VariantNull.class, "getInstance");
 
   @SuppressWarnings("ImmutableEnumChecker")
   public final Method method;
diff --git a/core/src/main/java/org/apache/calcite/util/Variant.java 
b/core/src/main/java/org/apache/calcite/util/Variant.java
deleted file mode 100644
index 05c91cf186..0000000000
--- a/core/src/main/java/org/apache/calcite/util/Variant.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * 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.calcite.util;
-
-import org.apache.calcite.runtime.SqlFunctions;
-import org.apache.calcite.sql.type.BasicSqlType;
-import org.apache.calcite.util.rtti.BasicSqlTypeRtti;
-import org.apache.calcite.util.rtti.RuntimeTypeInformation;
-
-import org.checkerframework.checker.nullness.qual.Nullable;
-
-import java.util.Objects;
-
-/** This class is the runtime support for values of the VARIANT SQL type. */
-public class Variant {
-  /** Actual value. */
-  final @Nullable Object value;
-  /** Type of the value. */
-  final RuntimeTypeInformation runtimeType;
-  /** The VARIANT type has its own notion of null, which is
-   * different from the SQL NULL value.  For example, two variant nulls are 
equal
-   * with each other.  This flag is 'true' if this value represents a variant 
null. */
-  final boolean isVariantNull;
-
-  private Variant(@Nullable Object value, RuntimeTypeInformation runtimeType,
-      boolean isVariantNull) {
-    this.value = value;
-    this.runtimeType = runtimeType;
-    this.isVariantNull = isVariantNull;
-  }
-
-  public Variant(@Nullable Object value, RuntimeTypeInformation runtimeType) {
-    this(value, runtimeType, false);
-  }
-
-  /** Create a variant object with a null value. */
-  public Variant() {
-    this(null,
-        new BasicSqlTypeRtti(RuntimeTypeInformation.RuntimeSqlTypeName.VARIANT,
-            BasicSqlType.PRECISION_NOT_SPECIFIED, 
BasicSqlType.SCALE_NOT_SPECIFIED),
-        true);
-  }
-
-  public String getType() {
-    return this.runtimeType.getTypeString();
-  }
-
-  public boolean isVariantNull() {
-    return this.isVariantNull;
-  }
-
-  @Override public boolean equals(@Nullable Object o) {
-    if (this == o) {
-      return true;
-    }
-    if (o == null || getClass() != o.getClass()) {
-      return false;
-    }
-
-    Variant variant = (Variant) o;
-    return isVariantNull == variant.isVariantNull
-        && Objects.equals(value, variant.value)
-        && runtimeType.equals(variant.runtimeType);
-  }
-
-  @Override public int hashCode() {
-    int result = Objects.hashCode(value);
-    result = 31 * result + runtimeType.hashCode();
-    result = 31 * result + Boolean.hashCode(isVariantNull);
-    return result;
-  }
-
-  /** Cast this value to the specified type.  Currently, the rule is:
-   * if the value has the specified type, the value field is returned, 
otherwise a SQL
-   * NULL is returned. */
-  @Nullable Object cast(RuntimeTypeInformation type) {
-    if (this.runtimeType.isScalar()) {
-      if (this.runtimeType.equals(type)) {
-        return this.value;
-      } else {
-        return null;
-      }
-    } else {
-      if (this.runtimeType.equals(type)) {
-        return this.value;
-      }
-      // TODO: allow casts that change some of the generic arguments only
-    }
-    return null;
-  }
-
-  // This method is invoked from {@link RexToLixTranslator} VARIANT_CAST
-  public static @Nullable Object cast(@Nullable Object variant, 
RuntimeTypeInformation type) {
-    if (variant == null) {
-      return null;
-    }
-    if (!(variant instanceof Variant)) {
-      throw new RuntimeException("Expected a variant value " + variant);
-    }
-    return ((Variant) variant).cast(type);
-  }
-
-  // Implementation of the array index operator for VARIANT values
-  public @Nullable Object item(Object index) {
-    if (this.value == null) {
-      return null;
-    }
-    switch (this.runtimeType.getTypeName()) {
-    case ROW:
-    case ARRAY:
-    case MAP:
-      return SqlFunctions.item(this.value, index);
-    default:
-      return null;
-    }
-  }
-
-  // This method is called by the testing code.
-  @Override public String toString() {
-    if (value == null) {
-      return "NULL";
-    }
-    if (this.isVariantNull()) {
-      return "null";
-    }
-    if (this.runtimeType.getTypeName() == 
RuntimeTypeInformation.RuntimeSqlTypeName.ROW) {
-      if (value instanceof Object[]) {
-        Object[] array = (Object []) value;
-        StringBuilder buf = new StringBuilder("{");
-
-        boolean first = true;
-        for (Object o : array) {
-          if (!first) {
-            buf.append(", ");
-          }
-          first = false;
-          buf.append(o.toString());
-        }
-        buf.append("}");
-        return buf.toString();
-      }
-    }
-    String quote = "";
-    switch (this.runtimeType.getTypeName()) {
-    case TIME:
-    case TIME_WITH_LOCAL_TIME_ZONE:
-    case TIME_TZ:
-    case TIMESTAMP:
-    case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
-    case TIMESTAMP_TZ:
-    case INTERVAL_LONG:
-    case INTERVAL_SHORT:
-    case CHAR:
-    case VARCHAR:
-    case BINARY:
-    case VARBINARY:
-      // At least in Snowflake VARIANT values that are strings
-      // are printed with double quotes
-      // https://docs.snowflake.com/en/sql-reference/data-types-semistructured
-      quote = "\"";
-      break;
-    default:
-      break;
-    }
-    return quote + value + quote;
-  }
-}
diff --git a/core/src/test/resources/sql/variant.iq 
b/core/src/test/resources/sql/variant.iq
new file mode 100644
index 0000000000..486e77514d
--- /dev/null
+++ b/core/src/test/resources/sql/variant.iq
@@ -0,0 +1,197 @@
+# variant.iq - VARIANT type examples
+#
+# 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.
+#
+
+# We don't really need scott, but we need to have a connection
+!use scott
+!set outputformat csv
+
+SELECT CAST(1 AS VARIANT) as C;
+C
+1
+!ok
+
+SELECT TYPEOF(CAST(1 AS VARIANT)) AS C;
+C
+INTEGER
+!ok
+
+SELECT CAST(CAST(1 AS TINYINT) AS VARIANT) AS C;
+C
+1
+!ok
+
+# The runtime knows that this is a TINYINT
+SELECT TYPEOF(CAST(CAST(1 AS TINYINT) AS VARIANT)) AS C;
+C
+TINYINT
+!ok
+
+# Converting something to VARIANT and back works
+SELECT CAST(CAST(1 AS VARIANT) AS INT) AS C;
+C
+1
+!ok
+
+# Variant converts between numeric types
+SELECT CAST(CAST(1 AS VARIANT) AS TINYINT) AS C;
+C
+1
+!ok
+
+# Some VARIANT objects when output receive double quotes
+select CAST('string' as VARIANT) as C;
+C
+"string"
+!ok
+
+# CHAR(3) values are represented as VARCHAR in variants
+SELECT CAST(CAST('abc' AS VARIANT) AS VARCHAR) AS C;
+C
+abc
+!ok
+
+# VARCHAR and CHAR(N) have the same underlying runtime type
+SELECT CAST(CAST('abc' AS VARIANT) AS CHAR(3)) AS C;
+C
+abc
+!ok
+
+# The value representing a VARIANT null value (think of a JSON null)
+SELECT VARIANTNULL() AS C;
+C
+null
+!ok
+
+# VARIANT null is not the same as SQL NULL
+SELECT VARIANTNULL() IS NULL AS C;
+C
+false
+!ok
+
+# Two VARIANT nulls are equal, unlike SQL NULL
+SELECT VARIANTNULL() = VARIANTNULL() AS C;
+C
+true
+!ok
+
+SELECT TYPEOF(VARIANTNULL()) AS C;
+C
+VARIANT
+!ok
+
+# Variants delegate equality to the underlying values
+SELECT CAST(1 AS VARIANT) = CAST(1 AS VARIANT) AS C;
+C
+true
+!ok
+
+# To be equal two variants must have the same value and the same runtime type
+SELECT CAST(1 AS VARIANT) = CAST(CAST(1 AS TINYINT) AS VARIANT) AS C;
+C
+false
+!ok
+
+# An array of variant values can have values with any underlying type
+SELECT ARRAY[CAST(1 AS VARIANT), CAST('abc' AS VARIANT)] AS C;
+C
+[1, "abc"]
+!ok
+
+# A map with VARCHAR keys and VARIANT values
+SELECT MAP['a', CAST(1 AS VARIANT), 'b', CAST('abc' AS VARIANT), 'c', 
CAST(ARRAY[1,2,3] AS VARIANT)] AS C;
+C
+{a=1, b="abc", c=[1, 2, 3]}
+!ok
+
+# Variant values allow access by index, but return null if they are not arrays
+SELECT (CAST(1 AS VARIANT))[1] AS C;
+C
+null
+!ok
+
+SELECT CAST(ARRAY[1,2,3] AS VARIANT)[1] AS C;
+C
+1
+!ok
+
+# Acessing items in a VARIANT array returns VARIANT values,
+# even if the array itself does not contain VARIANT values
+# (Otherwise TYPEOF would not compile)
+SELECT TYPEOF(CAST(ARRAY[1,2,3] AS VARIANT)[1]) AS C;
+C
+INTEGER
+!ok
+
+# One can access fields by name in a VARIANT, even if the
+# variant does not have named fields
+SELECT CAST(ARRAY[1,2,3] AS VARIANT)['name'] AS C;
+C
+null
+!ok
+
+# One can access fields by name in a VARIANT, even if the
+# variant does not have named fields
+SELECT CAST(ARRAY[1,2,3] AS VARIANT)."name" AS C;
+C
+null
+!ok
+
+# One can access fields by index in a VARIANT
+SELECT CAST(Map[1,'a',2,'b',3,'c'] AS VARIANT)[1] AS C;
+C
+"a"
+!ok
+
+SELECT TYPEOF(CAST(Map[1,'a',2,'b',3,'c'] AS VARIANT)[1]) AS C;
+C
+VARCHAR
+!ok
+
+# Note that field name is quoted to match the case of the key
+SELECT CAST(Map['a',1,'b',2,'c',3] AS VARIANT)."a" AS C;
+C
+1
+!ok
+
+# Unquoted field may not match, depending on dialect
+SELECT CAST(Map['a',1,'b',2,'c',3] AS VARIANT).a AS C;
+C
+null
+!ok
+
+# The safest way is to use an index
+SELECT CAST(Map['a',1,'b',2,'c',3] AS VARIANT)['a'] AS C;
+C
+1
+!ok
+
+# Maps can have variant keys too
+# (but you have to index with a variant).
+SELECT (Map[CAST('a' AS VARIANT), 1, CAST(1 AS VARIANT), 2])[CAST(1 AS 
VARIANT)] as C;
+C
+2
+!ok
+
+# Navigating a JSON-like object
+SELECT CAST(MAP['a', CAST(1 AS VARIANT), 'b', CAST('abc' AS VARIANT), 'c', 
CAST(ARRAY[1,2,3] AS VARIANT)]
+               ['c'][1] AS INTEGER) AS C;
+C
+1
+!ok
+
+# End variant.iq
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 8795333b58..e48d6afc6b 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -1259,15 +1259,16 @@ Any such value holds at runtime two pieces of 
information:
 Values of `VARIANT` type can be created by casting any other value to a 
`VARIANT`: e.g.
 `SELECT CAST(x AS VARIANT)`.  Conversely, values of type `VARIANT` can be cast 
to any other data type
 `SELECT CAST(variant AS INT)`.  A cast of a value of type `VARIANT` to target 
type T
-will compare the runtime type with T.  If the types are identical, the
-original value is returned.  Otherwise the `CAST` returns `NULL`.
+will compare the runtime type with T.  If the types are identical or the types 
are
+numeric and there is a natural conversion between the two types, the
+original value is converted to the target type and returned.  Otherwise the 
`CAST` returns `NULL`.
 
 Values of type `ARRAY`, `MAP`, and `ROW` type can be cast to `VARIANT`.  
`VARIANT` values
 also offer the following operations:
 
 - indexing using array indexing notation `variant[index]`.  If the `VARIANT` is
   obtained from an `ARRAY` value, the indexing operation returns a `VARIANT` 
whose value element
-  is the element at the specified index.  Otherwise this operation returns 
`NULL`
+  is the element at the specified index.  Otherwise, this operation returns 
`NULL`
 - indexing using map element access notation `variant[key]`, where `key` can 
have
   any legal `MAP` key type.  If the `VARIANT` is obtained from a `MAP` value
   that has en element with this key, a `VARIANT` value holding the associated 
value in
@@ -1279,6 +1280,24 @@ also offer the following operations:
   is subject to the capitalization rules of the SQL dialect, so for correct
   operation the field may need to be quoted: `variant."field"`
 
+The runtime types do not need to match exactly the compile-time types.
+As a compiler front-end, Calcite does not mandate exactly how the runtime types
+are represented.  Calcite does include one particular implementation in
+Java runtime, which is used for testing.  In this representation
+the runtime types are represented as follows:
+
+- The scalar types do not include information about precision and scale.  Thus 
all `DECIMAL`
+  compile-time types are represented by a single run-time type.
+- `CHAR(N)` and `VARCHAR` are both represented by a single runtime `VARCHAR` 
type.
+- `BINARY(N)` and `VARBINARY` are both represented by a single runtime 
`VARBINARY` type.
+- `FLOAT` and `DOUBLE` are both represented by the same runtime type.
+- All "short interval" types (from days to seconds) are represented by a 
single type.
+- All "long interval" types (from years to months) are represented by a single 
type.
+- Generic types such as `INT ARRAY`, `MULTISET`, and `MAP` convert all their 
elements to VARIANT values
+
+The function VARIANTNULL() can be used to create an instance
+of the `VARIANT` `null` value.
+
 ### Spatial types
 
 Spatial data is represented as character strings encoded as
@@ -1542,6 +1561,8 @@ Algorithms for implicit conversion are subject to change 
across Calcite releases
 | CONVERT(string, charSet1, charSet2)     | Converts *string* from *charSet1* 
to *charSet2*
 | CONVERT(value USING transcodingName)    | Alter *value* from one base 
character set to *transcodingName*
 | TRANSLATE(value USING transcodingName)  | Alter *value* from one base 
character set to *transcodingName*
+| TYPEOF(variant)                        | Returns a string that describes the 
runtime type of *variant*, where variant has a `VARIANT` type
+| VARIANTNULL()                          | Returns an instance of the 
`VARIANT` null value (constructor)
 
 Converting a string to a **BINARY** or **VARBINARY** type produces the
 list of bytes of the string's encoding in the strings' charset.  A
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java 
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index a75cf71c1e..0bb72cff54 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -1804,14 +1804,15 @@ public class SqlOperatorTest {
 
   /** Test cases for
    * <a href="https://issues.apache.org/jira/browse/CALCITE-4918";>
-   * [CALCITE-4918]  Add a VARIANT data type</a>. */
+   * [CALCITE-4918] Add a VARIANT data type</a>. */
   @Test public void testVariant() {
     SqlOperatorFixture f = fixture();
     f.checkScalar("cast(1 as VARIANT)", "1", "VARIANT NOT NULL");
     // String variants include quotes when output
     f.checkScalar("cast('abc' as VARIANT)", "\"abc\"", "VARIANT NOT NULL");
     f.checkScalar("cast(ARRAY[1,2,3] as VARIANT)", "[1, 2, 3]", "VARIANT NOT 
NULL");
-    f.checkScalar("cast(MAP['a',1,'b',2] as VARIANT)", "{a=1, b=2}", "VARIANT 
NOT NULL");
+    f.checkScalar("cast(MULTISET[1,2,3] as VARIANT)", "[1, 2, 3]", "VARIANT 
NOT NULL");
+    f.checkScalar("cast(MAP['a',1,'b',2] as VARIANT)", "{\"a\"=1, \"b\"=2}", 
"VARIANT NOT NULL");
     f.checkScalar("cast((1, 2) as row(f0 integer, f1 bigint))", "{1, 2}",
         "RecordType(INTEGER NOT NULL F0, BIGINT NOT NULL F1) NOT NULL");
     f.checkScalar("cast(row(1, 2) AS VARIANT)", "{1, 2}", "VARIANT NOT NULL");
@@ -1824,8 +1825,7 @@ public class SqlOperatorTest {
         "VARCHAR");
     f.checkScalar("cast(cast(ARRAY[1,2,3] as VARIANT) AS INTEGER ARRAY)", "[1, 
2, 3]",
         "INTEGER NOT NULL ARRAY");
-    // If the type is not exaclty the same the conversion fails (here CHAR to 
VARCHAR)
-    f.checkNull("cast(cast('abc' as VARIANT) AS VARCHAR)");
+    f.checkScalar("cast(cast('abc' as VARIANT) AS VARCHAR)", "abc", "VARCHAR");
     f.checkScalar("cast(cast('abc' as VARIANT) AS CHAR(3))", "abc", "CHAR(3)");
 
     // Converting a variant to anything that does not match the runtime type 
returns null
@@ -1861,7 +1861,7 @@ public class SqlOperatorTest {
             + "'b', CAST(ARRAY["
             + "CAST(MAP['c', CAST(2.3 AS VARIANT)] AS VARIANT), CAST(5 AS 
VARIANT)]"
             + " AS VARIANT)]",
-        "{a=1, b=[{c=2.3}, 5]}",
+        "{a=1, b=[{\"c\"=2.3}, 5]}",
         "(CHAR(1) NOT NULL, VARIANT NOT NULL) MAP NOT NULL");
   }
 

Reply via email to