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 bd3d854cadf37c5f0c0cbd39ce1f583709986765
Author: Mihai Budiu <[email protected]>
AuthorDate: Mon Sep 2 14:22:27 2024 -0700

    [CALCITE-4918] Add a VARIANT data type - parser and validator
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../org/apache/calcite/test/BabelParserTest.java   |  7 +++++
 core/src/main/codegen/templates/Parser.jj          |  3 +++
 .../apache/calcite/sql/fun/SqlCastFunction.java    |  6 +++++
 .../org/apache/calcite/sql/fun/SqlDotOperator.java |  6 +++++
 .../apache/calcite/sql/fun/SqlItemOperator.java    |  8 +++++-
 .../calcite/sql/fun/SqlStdOperatorTable.java       |  2 +-
 .../sql/type/JavaToSqlTypeConversionRules.java     |  1 +
 .../org/apache/calcite/sql/type/OperandTypes.java  |  6 +++++
 .../calcite/sql/type/SqlTypeCoercionRule.java      | 29 +++++++++++++++++---
 .../org/apache/calcite/sql/type/SqlTypeFamily.java |  6 +++++
 .../org/apache/calcite/sql/type/SqlTypeName.java   | 10 ++++---
 .../org/apache/calcite/test/SqlValidatorTest.java  | 31 ++++++++++++++++++++++
 site/_docs/reference.md                            |  1 +
 13 files changed, 107 insertions(+), 9 deletions(-)

diff --git a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java 
b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
index 1b3c218ac6..fa645fe784 100644
--- a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
+++ b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
@@ -330,6 +330,13 @@ class BabelParserTest extends SqlParserTest {
     sql(sql).ok(expected);
   }
 
+  @Test void testCreateVariantTable() {
+    final String sql = "create table foo (bar variant not null)";
+    final String expected = "CREATE TABLE `FOO` "
+        + "(`BAR` VARIANT NOT NULL)";
+    sql(sql).ok(expected);
+  }
+
   @Test void testArrayLiteralFromString() {
     sql("select array '{1,2,3}'")
         .ok("SELECT (ARRAY[1, 2, 3])");
diff --git a/core/src/main/codegen/templates/Parser.jj 
b/core/src/main/codegen/templates/Parser.jj
index 09e33643f9..f826c4241e 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -5896,6 +5896,8 @@ SqlTypeNameSpec SqlTypeName1(Span s) :
         [ <PRECISION> ] { sqlTypeName = SqlTypeName.DOUBLE; }
     |
         <FLOAT> { s.add(this); sqlTypeName = SqlTypeName.FLOAT; }
+    |
+        <VARIANT> { s.add(this); sqlTypeName = SqlTypeName.VARIANT; }
     )
     {
         return new SqlBasicTypeNameSpec(sqlTypeName, s.end(this));
@@ -8701,6 +8703,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < VAR_SAMP: "VAR_SAMP" >
 |   < VARBINARY: "VARBINARY" >
 |   < VARCHAR: "VARCHAR" >
+|   < VARIANT: "VARIANT" >
 |   < VARYING: "VARYING" >
 |   < VERSION: "VERSION" >
 |   < VERSIONING: "VERSIONING" >
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
index 7292d0eb44..8e3388236f 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
@@ -190,6 +190,12 @@ public class SqlCastFunction extends SqlFunction {
       SqlTypeUtil.createMapType(typeFactory, keyType, valueType, isNullable);
     }
 
+    if (expressionType.getSqlTypeName() == SqlTypeName.VARIANT) {
+      // A variant can be cast to any other type, but the result
+      // is always nullable, like in the case of a safe cast.
+      return typeFactory.createTypeWithNullability(targetType, true);
+    }
+
     return typeFactory.createTypeWithNullability(targetType, isNullable);
   }
 
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java
index 3d3896f76e..7c9a231579 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java
@@ -101,6 +101,12 @@ public class SqlDotOperator extends SqlSpecialOperator {
     final SqlNode operand = call.getOperandList().get(0);
     final RelDataType nodeType =
         requireNonNull(validator.deriveType(scope, operand));
+    assert nodeType != null;
+    if (nodeType.getSqlTypeName() == SqlTypeName.VARIANT) {
+      // Result is always a nullable VARIANT
+      return validator.getTypeFactory().createTypeWithNullability(nodeType, 
true);
+    }
+
     if (!nodeType.isStruct()) {
       throw SqlUtil.newContextException(operand.getParserPosition(),
           Static.RESOURCE.incompatibleTypes());
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
index 4f5371c9fc..fc832a64cc 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
@@ -125,6 +125,7 @@ public class SqlItemOperator extends SqlSpecialOperator {
     case ROW:
     case ANY:
     case DYNAMIC_STAR:
+    case VARIANT:
       return OperandTypes.family(SqlTypeFamily.INTEGER)
           .or(OperandTypes.family(SqlTypeFamily.CHARACTER));
     default:
@@ -136,7 +137,8 @@ public class SqlItemOperator extends SqlSpecialOperator {
     if (name.equals("ITEM")) {
       return "<ARRAY>[<INTEGER>]\n"
           + "<MAP>[<ANY>]\n"
-          + "<ROW>[<CHARACTER>|<INTEGER>]";
+          + "<ROW>[<CHARACTER>|<INTEGER>]"
+          + "<VARIANT>[<CHARACTER>|<INTEGER>]";
     } else {
       return "<ARRAY>[" + name + "(<INTEGER>)]";
     }
@@ -146,6 +148,10 @@ public class SqlItemOperator extends SqlSpecialOperator {
     final RelDataTypeFactory typeFactory = opBinding.getTypeFactory();
     final RelDataType operandType = opBinding.getOperandType(0);
     switch (operandType.getSqlTypeName()) {
+    case VARIANT:
+      // Return type is always nullable VARIANT
+      return typeFactory.createTypeWithNullability(
+          operandType, true);
     case ARRAY:
       return typeFactory.createTypeWithNullability(
           getComponentTypeOrThrow(operandType), true);
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 a2e3d66504..6aba1cdaa4 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
@@ -2226,7 +2226,7 @@ public class SqlStdOperatorTable extends 
ReflectiveSqlOperatorTable {
    * <p>MAP is not standard SQL.
    */
   public static final SqlOperator ITEM =
-      new SqlItemOperator("ITEM", OperandTypes.ARRAY_OR_MAP, 1, true);
+      new SqlItemOperator("ITEM", OperandTypes.ARRAY_OR_MAP_OR_VARIANT, 1, 
true);
 
   /**
    * The ARRAY Value Constructor. e.g. "<code>ARRAY[1, 2, 3]</code>".
diff --git 
a/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java
 
b/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java
index f1941b29fe..dc910c0dd1 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java
@@ -81,6 +81,7 @@ public class JavaToSqlTypeConversionRules {
           .put(List.class, SqlTypeName.ARRAY)
           .put(Map.class, SqlTypeName.MAP)
           .put(Void.class, SqlTypeName.NULL)
+          .put(Object.class, SqlTypeName.VARIANT)
           .build();
 
   //~ Methods ----------------------------------------------------------------
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 39bdad21ca..5a82308e91 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
@@ -545,6 +545,12 @@ public abstract class OperandTypes {
           .or(OperandTypes.family(SqlTypeFamily.MAP))
           .or(OperandTypes.family(SqlTypeFamily.ANY));
 
+  public static final SqlSingleOperandTypeChecker ARRAY_OR_MAP_OR_VARIANT =
+      OperandTypes.family(SqlTypeFamily.ARRAY)
+          .or(OperandTypes.family(SqlTypeFamily.MAP))
+          .or(OperandTypes.family(SqlTypeFamily.VARIANT))
+          .or(OperandTypes.family(SqlTypeFamily.ANY));
+
   public static final SqlOperandTypeChecker 
STRING_ARRAY_CHARACTER_OPTIONAL_CHARACTER =
       new FamilyOperandTypeChecker(
           ImmutableList.of(SqlTypeFamily.ARRAY, SqlTypeFamily.CHARACTER, 
SqlTypeFamily.CHARACTER),
diff --git 
a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java 
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
index ab03b24ce7..e462a11fc7 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
@@ -138,6 +138,7 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
       coerceRules.add(exactType,
           coerceRules.copyValues(exactType)
               .addAll(SqlTypeName.INTERVAL_TYPES)
+              .add(SqlTypeName.VARIANT)
               .build());
     }
 
@@ -152,27 +153,31 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
               .add(SqlTypeName.DECIMAL)
               .add(SqlTypeName.CHAR)
               .add(SqlTypeName.VARCHAR)
+              .add(SqlTypeName.VARIANT)
               .build());
     }
 
-    // BINARY is castable from VARBINARY, CHARACTERS.
+    // BINARY is castable from VARBINARY, CHARACTERS, VARIANT.
     coerceRules.add(SqlTypeName.BINARY,
         coerceRules.copyValues(SqlTypeName.BINARY)
             .add(SqlTypeName.VARBINARY)
+            .add(SqlTypeName.VARIANT)
             .addAll(SqlTypeName.CHAR_TYPES)
             .build());
 
-    // VARBINARY is castable from BINARY, CHARACTERS.
+    // VARBINARY is castable from BINARY, CHARACTERS, VARIANT.
     coerceRules.add(SqlTypeName.VARBINARY,
         coerceRules.copyValues(SqlTypeName.VARBINARY)
+            .add(SqlTypeName.VARIANT)
             .add(SqlTypeName.BINARY)
             .addAll(SqlTypeName.CHAR_TYPES)
             .build());
 
     // VARCHAR is castable from BOOLEAN, DATE, TIME, TIMESTAMP, numeric types, 
binary and
-    // intervals
+    // intervals, VARIANT
     coerceRules.add(SqlTypeName.VARCHAR,
         coerceRules.copyValues(SqlTypeName.VARCHAR)
+            .add(SqlTypeName.VARIANT)
             .add(SqlTypeName.CHAR)
             .add(SqlTypeName.BOOLEAN)
             .add(SqlTypeName.DATE)
@@ -187,10 +192,17 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
             .addAll(SqlTypeName.INTERVAL_TYPES)
             .build());
 
+    // VARIANT is castable from anything
+    coerceRules.add(SqlTypeName.VARIANT,
+        coerceRules.copyValues(SqlTypeName.CHAR)
+            .addAll(SqlTypeName.ALL_TYPES)
+            .build());
+
     // CHAR is castable from BOOLEAN, DATE, TIME, TIMESTAMP, numeric types, 
binary and
-    // intervals
+    // intervals, VARIANT
     coerceRules.add(SqlTypeName.CHAR,
         coerceRules.copyValues(SqlTypeName.CHAR)
+            .add(SqlTypeName.VARIANT)
             .add(SqlTypeName.VARCHAR)
             .add(SqlTypeName.BOOLEAN)
             .add(SqlTypeName.DATE)
@@ -208,6 +220,7 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
     // BOOLEAN is castable from ...
     coerceRules.add(SqlTypeName.BOOLEAN,
         coerceRules.copyValues(SqlTypeName.BOOLEAN)
+            .add(SqlTypeName.VARIANT)
             .add(SqlTypeName.CHAR)
             .add(SqlTypeName.VARCHAR)
             .addAll(SqlTypeName.NUMERIC_TYPES)
@@ -219,6 +232,7 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
     // DATE is castable from...
     coerceRules.add(SqlTypeName.DATE,
         coerceRules.copyValues(SqlTypeName.DATE)
+            .add(SqlTypeName.VARIANT)
             .add(SqlTypeName.TIMESTAMP)
             .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
             .add(SqlTypeName.TIMESTAMP_TZ)
@@ -230,6 +244,7 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
     // TIME is castable from...
     coerceRules.add(SqlTypeName.TIME,
         coerceRules.copyValues(SqlTypeName.TIME)
+            .add(SqlTypeName.VARIANT)
             .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
             .add(SqlTypeName.TIME_TZ)
             .add(SqlTypeName.TIMESTAMP)
@@ -243,6 +258,7 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
     // TIME WITH LOCAL TIME ZONE is castable from...
     coerceRules.add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE,
         coerceRules.copyValues(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.VARIANT)
             .add(SqlTypeName.TIME_TZ)
             .add(SqlTypeName.TIME)
             .add(SqlTypeName.TIMESTAMP)
@@ -257,6 +273,7 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
     coerceRules.add(SqlTypeName.TIME_TZ,
         coerceRules.copyValues(SqlTypeName.TIME_TZ)
             .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.VARIANT)
             .add(SqlTypeName.TIME)
             .add(SqlTypeName.TIMESTAMP)
             .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
@@ -269,6 +286,7 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
     // TIMESTAMP is castable from...
     coerceRules.add(SqlTypeName.TIMESTAMP,
         coerceRules.copyValues(SqlTypeName.TIMESTAMP)
+            .add(SqlTypeName.VARIANT)
             .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
             .add(SqlTypeName.TIMESTAMP_TZ)
             .add(SqlTypeName.DATE)
@@ -284,6 +302,7 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
     // TIMESTAMP WITH LOCAL TIME ZONE is castable from...
     coerceRules.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE,
         coerceRules.copyValues(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.VARIANT)
             .add(SqlTypeName.TIMESTAMP_TZ)
             .add(SqlTypeName.TIMESTAMP)
             .add(SqlTypeName.DATE)
@@ -299,6 +318,7 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
     // TIMESTAMP WITH TIME ZONE is castable from...
     coerceRules.add(SqlTypeName.TIMESTAMP_TZ,
         coerceRules.copyValues(SqlTypeName.TIMESTAMP_TZ)
+            .add(SqlTypeName.VARIANT)
             .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
             .add(SqlTypeName.TIMESTAMP)
             .add(SqlTypeName.DATE)
@@ -314,6 +334,7 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
     // GEOMETRY is castable from ...
     coerceRules.add(SqlTypeName.GEOMETRY,
         coerceRules.copyValues(SqlTypeName.GEOMETRY)
+            .add(SqlTypeName.VARIANT)
             .addAll(SqlTypeName.CHAR_TYPES)
             .build());
 
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java 
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
index 050902238c..63832873a9 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
@@ -78,6 +78,7 @@ public enum SqlTypeFamily implements RelDataTypeFamily {
   COLUMN_LIST,
   GEO,
   FUNCTION,
+  VARIANT,
   /** Like ANY, but do not even validate the operand. It may not be an
    * expression. */
   IGNORE;
@@ -119,6 +120,7 @@ public enum SqlTypeFamily implements RelDataTypeFamily {
 
           .put(ExtraSqlTypes.REF_CURSOR, CURSOR)
           .put(Types.ARRAY, ARRAY)
+          .put(Types.JAVA_OBJECT, VARIANT)
           .build();
 
   /**
@@ -222,6 +224,8 @@ public enum SqlTypeFamily implements RelDataTypeFamily {
       return ImmutableList.of(SqlTypeName.COLUMN_LIST);
     case FUNCTION:
       return ImmutableList.of(SqlTypeName.FUNCTION);
+    case VARIANT:
+      return ImmutableList.of(SqlTypeName.VARIANT);
     default:
       throw new IllegalArgumentException();
     }
@@ -281,6 +285,8 @@ public enum SqlTypeFamily implements RelDataTypeFamily {
       return factory.createFunctionSqlType(
           factory.createStructType(ImmutableList.of(), ImmutableList.of()),
           factory.createSqlType(SqlTypeName.ANY));
+    case VARIANT:
+      return factory.createSqlType(SqlTypeName.VARIANT);
     default:
       return null;
     }
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java 
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
index e5a30dd581..ed986bb4ca 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
@@ -132,7 +132,10 @@ public enum SqlTypeName {
   GEOMETRY(PrecScale.NO_NO, false, ExtraSqlTypes.GEOMETRY, SqlTypeFamily.GEO),
   MEASURE(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.ANY),
   FUNCTION(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.FUNCTION),
-  SARG(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.ANY);
+  SARG(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.ANY),
+  /** VARIANT data type, a dynamically-typed value that can have at runtime
+   * any of the other data types in this table. */
+  VARIANT(PrecScale.NO_NO, false, Types.OTHER, SqlTypeFamily.VARIANT);
 
   public static final int MAX_DATETIME_PRECISION = 3;
 
@@ -165,7 +168,7 @@ public enum SqlTypeName {
           INTERVAL_HOUR_SECOND, INTERVAL_MINUTE, INTERVAL_MINUTE_SECOND,
           INTERVAL_SECOND, TIME_WITH_LOCAL_TIME_ZONE, TIME_TZ,
           TIMESTAMP_WITH_LOCAL_TIME_ZONE, TIMESTAMP_TZ,
-          FLOAT, MULTISET, DISTINCT, STRUCTURED, ROW, CURSOR, COLUMN_LIST);
+          FLOAT, MULTISET, DISTINCT, STRUCTURED, ROW, CURSOR, COLUMN_LIST, 
VARIANT);
 
   public static final List<SqlTypeName> BOOLEAN_TYPES =
       ImmutableList.of(BOOLEAN);
@@ -274,6 +277,7 @@ public enum SqlTypeName {
           .put(Types.DISTINCT, DISTINCT)
           .put(Types.STRUCT, STRUCTURED)
           .put(Types.ARRAY, ARRAY)
+          .put(Types.JAVA_OBJECT, VARIANT)
           .build();
 
   /**
@@ -282,7 +286,7 @@ public enum SqlTypeName {
   private final int signatures;
 
   /**
-   * Returns true if not of a "pure" standard sql type. "Inpure" types are
+   * Returns true if not of a "pure" standard sql type. "Inpure" types include
    * {@link #ANY}, {@link #NULL} and {@link #SYMBOL}
    */
   private final boolean special;
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 2c3ca5436f..9434f8b23d 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -1347,6 +1347,37 @@ public class SqlValidatorTest extends 
SqlValidatorTestCase {
         .columnType("TIMESTAMP_TZ(3) NOT NULL");
   }
 
+  @Test void testCastVariant() {
+    expr("cast(NULL as variant)")
+        .columnType("VARIANT");
+    expr("cast(1 as variant)")
+        .columnType("VARIANT NOT NULL");
+    expr("cast('abc' as variant)")
+        .columnType("VARIANT NOT NULL");
+    expr("cast(TIMESTAMP '2024-09-01 00:00:00' as variant)")
+        .columnType("VARIANT NOT NULL");
+
+    expr("cast(cast(NULL as variant) as int)")
+        .columnType("INTEGER");
+    expr("cast(cast(1 as variant) as int)")
+        .columnType("INTEGER");
+    expr("cast(cast(1 as variant) as varchar)")
+        .columnType("VARCHAR");
+    expr("cast(cast('abc' as variant) as varchar)")
+        .columnType("VARCHAR");
+    expr("cast(cast(TIMESTAMP '2024-09-01 00:00:00' as variant) as timestamp)")
+        .columnType("TIMESTAMP(0)");
+  }
+
+  @Test void testAccessVariant() {
+    expr("cast(1 as variant).field")
+        .columnType("VARIANT");
+    expr("cast(1 as variant)['field']")
+        .columnType("VARIANT");
+    expr("cast(1 as variant)[0]")
+        .columnType("VARIANT");
+  }
+
   @Test void testCastRegisteredType() {
     expr("cast(123 as ^customBigInt^)")
         .fails("Unknown identifier 'CUSTOMBIGINT'");
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 7a2a895025..2fe835f39f 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -1241,6 +1241,7 @@ Note:
 | ARRAY    | Ordered, contiguous collection that may contain duplicates | 
Example: varchar(10) array
 | CURSOR   | Cursor over the result of executing a query |
 | FUNCTION | A function definition that is not bound to an identifier, it is 
not fully supported in CAST or DDL | Example FUNCTION(INTEGER, VARCHAR(30)) -> 
INTEGER
+| VARIANT  | Dynamically-typed value that can have at runtime a value of any 
other type | VARIANT
 
 Note:
 

Reply via email to