This is an automated email from the ASF dual-hosted git repository.
deniskuzZ pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git
The following commit(s) were added to refs/heads/master by this push:
new 170e8e5326e HIVE-29773: Iceberg: Support CAST(x AS VARIANT) (#6654)
170e8e5326e is described below
commit 170e8e5326e5bd9596daf675d0cc5910cb8d39bf
Author: Denys Kuzmenko <[email protected]>
AuthorDate: Fri Oct 16 18:59:26 2026 +0200
HIVE-29773: Iceberg: Support CAST(x AS VARIANT) (#6654)
---
.../src/test/queries/positive/variant_cast.q | 47 +++++
.../src/test/results/positive/variant_cast.q.out | 203 +++++++++++++++++++++
.../hadoop/hive/ql/exec/FunctionRegistry.java | 3 +-
.../hive/ql/parse/type/TypeCheckProcFactory.java | 2 +
.../hive/ql/udf/generic/GenericUDFToVariant.java | 162 ++++++++++++++++
.../ql/udf/generic/TestGenericUDFToVariant.java | 106 +++++++++++
.../clientpositive/llap/show_functions.q.out | 2 +
7 files changed, 524 insertions(+), 1 deletion(-)
diff --git a/iceberg/iceberg-handler/src/test/queries/positive/variant_cast.q
b/iceberg/iceberg-handler/src/test/queries/positive/variant_cast.q
new file mode 100644
index 00000000000..1fea3724ff7
--- /dev/null
+++ b/iceberg/iceberg-handler/src/test/queries/positive/variant_cast.q
@@ -0,0 +1,47 @@
+set hive.explain.user=false;
+
+drop table if exists variant_cast;
+
+CREATE EXTERNAL TABLE variant_cast (
+ id INT,
+ v VARIANT
+) STORED BY ICEBERG
+TBLPROPERTIES (
+ 'format-version'='3'
+);
+
+-- typed variant construction: values JSON cannot express keep their real type
+INSERT INTO variant_cast VALUES
+(1, CAST(true AS VARIANT)),
+(2, CAST(42 AS VARIANT)),
+(3, CAST(1.5BD AS VARIANT)),
+(4, CAST(unhex('DEADBEEF') AS VARIANT)),
+(5, CAST(DATE '2026-07-28' AS VARIANT)),
+(6, CAST(TIMESTAMP '2026-07-28 10:00:00' AS VARIANT)),
+(7, CAST('plain' AS VARIANT)),
+(8, CAST(array(1, 2, 3) AS VARIANT)),
+(9, CAST(NULL AS VARIANT));
+
+SELECT id, v FROM variant_cast ORDER BY id;
+
+-- binary node is rendered as base64, matching Impala/Trino
+SELECT variant_get(v, '$', 'string') FROM variant_cast WHERE id = 4;
+
+-- cast over a column expression, not a constant
+drop table if exists variant_src;
+
+CREATE TABLE variant_src (id INT, s STRING);
+
+INSERT INTO variant_src VALUES (11, 'from_col');
+
+EXPLAIN INSERT INTO variant_cast SELECT id, CAST(s AS VARIANT) FROM
variant_src;
+
+INSERT INTO variant_cast SELECT id, CAST(s AS VARIANT) FROM variant_src;
+
+SELECT id, v FROM variant_cast WHERE id > 10;
+
+-- identity cast over a reader-produced variant column
+SELECT id, CAST(v AS VARIANT) FROM variant_cast WHERE id > 10;
+
+drop table variant_cast;
+drop table variant_src;
diff --git
a/iceberg/iceberg-handler/src/test/results/positive/variant_cast.q.out
b/iceberg/iceberg-handler/src/test/results/positive/variant_cast.q.out
new file mode 100644
index 00000000000..17daa4fba48
--- /dev/null
+++ b/iceberg/iceberg-handler/src/test/results/positive/variant_cast.q.out
@@ -0,0 +1,203 @@
+PREHOOK: query: drop table if exists variant_cast
+PREHOOK: type: DROPTABLE
+PREHOOK: Output: database:default
+POSTHOOK: query: drop table if exists variant_cast
+POSTHOOK: type: DROPTABLE
+POSTHOOK: Output: database:default
+PREHOOK: query: CREATE EXTERNAL TABLE variant_cast (
+ id INT,
+ v VARIANT
+) STORED BY ICEBERG
+TBLPROPERTIES (
+ 'format-version'='3'
+)
+PREHOOK: type: CREATETABLE
+PREHOOK: Output: database:default
+PREHOOK: Output: default@variant_cast
+POSTHOOK: query: CREATE EXTERNAL TABLE variant_cast (
+ id INT,
+ v VARIANT
+) STORED BY ICEBERG
+TBLPROPERTIES (
+ 'format-version'='3'
+)
+POSTHOOK: type: CREATETABLE
+POSTHOOK: Output: database:default
+POSTHOOK: Output: default@variant_cast
+PREHOOK: query: INSERT INTO variant_cast VALUES
+(1, CAST(true AS VARIANT)),
+(2, CAST(42 AS VARIANT)),
+(3, CAST(1.5BD AS VARIANT)),
+(4, CAST(unhex('DEADBEEF') AS VARIANT)),
+(5, CAST(DATE '2026-07-28' AS VARIANT)),
+(6, CAST(TIMESTAMP '2026-07-28 10:00:00' AS VARIANT)),
+(7, CAST('plain' AS VARIANT)),
+(8, CAST(array(1, 2, 3) AS VARIANT)),
+(9, CAST(NULL AS VARIANT))
+PREHOOK: type: QUERY
+PREHOOK: Input: _dummy_database@_dummy_table
+PREHOOK: Output: default@variant_cast
+POSTHOOK: query: INSERT INTO variant_cast VALUES
+(1, CAST(true AS VARIANT)),
+(2, CAST(42 AS VARIANT)),
+(3, CAST(1.5BD AS VARIANT)),
+(4, CAST(unhex('DEADBEEF') AS VARIANT)),
+(5, CAST(DATE '2026-07-28' AS VARIANT)),
+(6, CAST(TIMESTAMP '2026-07-28 10:00:00' AS VARIANT)),
+(7, CAST('plain' AS VARIANT)),
+(8, CAST(array(1, 2, 3) AS VARIANT)),
+(9, CAST(NULL AS VARIANT))
+POSTHOOK: type: QUERY
+POSTHOOK: Input: _dummy_database@_dummy_table
+POSTHOOK: Output: default@variant_cast
+PREHOOK: query: SELECT id, v FROM variant_cast ORDER BY id
+PREHOOK: type: QUERY
+PREHOOK: Input: default@variant_cast
+PREHOOK: Output: hdfs://### HDFS PATH ###
+POSTHOOK: query: SELECT id, v FROM variant_cast ORDER BY id
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@variant_cast
+POSTHOOK: Output: hdfs://### HDFS PATH ###
+1 true
+2 42
+3 1.5
+4 "3q2+7w=="
+5 "2026-07-28"
+6 "2026-07-28 10:00:00"
+7 "plain"
+8 [1,2,3]
+9 NULL
+PREHOOK: query: SELECT variant_get(v, '$', 'string') FROM variant_cast WHERE
id = 4
+PREHOOK: type: QUERY
+PREHOOK: Input: default@variant_cast
+PREHOOK: Output: hdfs://### HDFS PATH ###
+POSTHOOK: query: SELECT variant_get(v, '$', 'string') FROM variant_cast WHERE
id = 4
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@variant_cast
+POSTHOOK: Output: hdfs://### HDFS PATH ###
+3q2+7w==
+PREHOOK: query: drop table if exists variant_src
+PREHOOK: type: DROPTABLE
+PREHOOK: Output: database:default
+POSTHOOK: query: drop table if exists variant_src
+POSTHOOK: type: DROPTABLE
+POSTHOOK: Output: database:default
+PREHOOK: query: CREATE TABLE variant_src (id INT, s STRING)
+PREHOOK: type: CREATETABLE
+PREHOOK: Output: database:default
+PREHOOK: Output: default@variant_src
+POSTHOOK: query: CREATE TABLE variant_src (id INT, s STRING)
+POSTHOOK: type: CREATETABLE
+POSTHOOK: Output: database:default
+POSTHOOK: Output: default@variant_src
+PREHOOK: query: INSERT INTO variant_src VALUES (11, 'from_col')
+PREHOOK: type: QUERY
+PREHOOK: Input: _dummy_database@_dummy_table
+PREHOOK: Output: default@variant_src
+POSTHOOK: query: INSERT INTO variant_src VALUES (11, 'from_col')
+POSTHOOK: type: QUERY
+POSTHOOK: Input: _dummy_database@_dummy_table
+POSTHOOK: Output: default@variant_src
+POSTHOOK: Lineage: variant_src.id SCRIPT []
+POSTHOOK: Lineage: variant_src.s SCRIPT []
+PREHOOK: query: EXPLAIN INSERT INTO variant_cast SELECT id, CAST(s AS VARIANT)
FROM variant_src
+PREHOOK: type: QUERY
+PREHOOK: Input: default@variant_src
+PREHOOK: Output: default@variant_cast
+POSTHOOK: query: EXPLAIN INSERT INTO variant_cast SELECT id, CAST(s AS
VARIANT) FROM variant_src
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@variant_src
+POSTHOOK: Output: default@variant_cast
+STAGE DEPENDENCIES:
+ Stage-1 is a root stage
+ Stage-2 depends on stages: Stage-1
+ Stage-0 depends on stages: Stage-2
+ Stage-3 depends on stages: Stage-0
+
+STAGE PLANS:
+ Stage: Stage-1
+ Tez
+#### A masked pattern was here ####
+ Vertices:
+ Map 1
+ Map Operator Tree:
+ TableScan
+ alias: variant_src
+ Statistics: Num rows: 1 Data size: 96 Basic stats: COMPLETE
Column stats: COMPLETE
+ Select Operator
+ expressions: id (type: int), CAST( s AS VARIANT) (type:
struct<metadata:binary,value:binary>)
+ outputColumnNames: _col0, _col1
+ Statistics: Num rows: 1 Data size: 340 Basic stats:
COMPLETE Column stats: COMPLETE
+ File Output Operator
+ compressed: false
+ Statistics: Num rows: 1 Data size: 340 Basic stats:
COMPLETE Column stats: COMPLETE
+ table:
+ input format:
org.apache.iceberg.mr.hive.HiveIcebergInputFormat
+ output format:
org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
+ serde: org.apache.iceberg.mr.hive.HiveIcebergSerDe
+ name: default.variant_cast
+ Execution mode: vectorized
+
+ Stage: Stage-2
+ Dependency Collection
+
+ Stage: Stage-0
+ Move Operator
+ tables:
+ replace: false
+ table:
+ input format: org.apache.iceberg.mr.hive.HiveIcebergInputFormat
+ output format: org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
+ serde: org.apache.iceberg.mr.hive.HiveIcebergSerDe
+ name: default.variant_cast
+
+ Stage: Stage-3
+ Stats Work
+ Basic Stats Work:
+
+PREHOOK: query: INSERT INTO variant_cast SELECT id, CAST(s AS VARIANT) FROM
variant_src
+PREHOOK: type: QUERY
+PREHOOK: Input: default@variant_src
+PREHOOK: Output: default@variant_cast
+POSTHOOK: query: INSERT INTO variant_cast SELECT id, CAST(s AS VARIANT) FROM
variant_src
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@variant_src
+POSTHOOK: Output: default@variant_cast
+PREHOOK: query: SELECT id, v FROM variant_cast WHERE id > 10
+PREHOOK: type: QUERY
+PREHOOK: Input: default@variant_cast
+PREHOOK: Output: hdfs://### HDFS PATH ###
+POSTHOOK: query: SELECT id, v FROM variant_cast WHERE id > 10
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@variant_cast
+POSTHOOK: Output: hdfs://### HDFS PATH ###
+11 "from_col"
+PREHOOK: query: SELECT id, CAST(v AS VARIANT) FROM variant_cast WHERE id > 10
+PREHOOK: type: QUERY
+PREHOOK: Input: default@variant_cast
+PREHOOK: Output: hdfs://### HDFS PATH ###
+POSTHOOK: query: SELECT id, CAST(v AS VARIANT) FROM variant_cast WHERE id > 10
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@variant_cast
+POSTHOOK: Output: hdfs://### HDFS PATH ###
+11 "from_col"
+PREHOOK: query: drop table variant_cast
+PREHOOK: type: DROPTABLE
+PREHOOK: Input: default@variant_cast
+PREHOOK: Output: database:default
+PREHOOK: Output: default@variant_cast
+POSTHOOK: query: drop table variant_cast
+POSTHOOK: type: DROPTABLE
+POSTHOOK: Input: default@variant_cast
+POSTHOOK: Output: database:default
+POSTHOOK: Output: default@variant_cast
+PREHOOK: query: drop table variant_src
+PREHOOK: type: DROPTABLE
+PREHOOK: Input: default@variant_src
+PREHOOK: Output: database:default
+PREHOOK: Output: default@variant_src
+POSTHOOK: query: drop table variant_src
+POSTHOOK: type: DROPTABLE
+POSTHOOK: Input: default@variant_src
+POSTHOOK: Output: database:default
+POSTHOOK: Output: default@variant_src
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/FunctionRegistry.java
b/ql/src/java/org/apache/hadoop/hive/ql/exec/FunctionRegistry.java
index 81b8f30d652..6d0bdfed2b6 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/exec/FunctionRegistry.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/FunctionRegistry.java
@@ -789,6 +789,7 @@ public final class FunctionRegistry {
system.registerFunction("ST_Z", ST_Z.class);
// Iceberg UDFs
+ system.registerGenericUDF(serdeConstants.VARIANT_TYPE_NAME,
GenericUDFToVariant.class);
system.registerGenericUDF("parse_json", GenericUDFParseJson.class);
system.registerGenericUDF("to_json", GenericUDFToJson.class);
system.registerGenericUDF("variant_get", GenericUDFVariantGet.class);
@@ -1638,7 +1639,7 @@ public static boolean isOpCast(GenericUDF genericUDF) {
udfClass == GenericUDFToVarchar.class || udfClass ==
GenericUDFToChar.class ||
udfClass == GenericUDFTimestamp.class || udfClass ==
GenericUDFToBinary.class ||
udfClass == GenericUDFToDate.class || udfClass ==
GenericUDFToDecimal.class ||
- udfClass == GenericUDFToTimestampLocalTZ.class;
+ udfClass == GenericUDFToTimestampLocalTZ.class || udfClass ==
GenericUDFToVariant.class;
}
/**
diff --git
a/ql/src/java/org/apache/hadoop/hive/ql/parse/type/TypeCheckProcFactory.java
b/ql/src/java/org/apache/hadoop/hive/ql/parse/type/TypeCheckProcFactory.java
index 261942cd93f..509a4c225e1 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/parse/type/TypeCheckProcFactory.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/type/TypeCheckProcFactory.java
@@ -140,6 +140,8 @@ public class TypeCheckProcFactory<T> {
serdeConstants.INTERVAL_DAY_TIME_TYPE_NAME);
CONVERSION_FUNCTION_TEXT_MAP.put(HiveParser.TOK_DECIMAL,
serdeConstants.DECIMAL_TYPE_NAME);
+ CONVERSION_FUNCTION_TEXT_MAP.put(HiveParser.TOK_VARIANT,
+ serdeConstants.VARIANT_TYPE_NAME);
CONVERSION_FUNCTION_TEXT_MAP.put(HiveParser.TOK_MAP, "toMap");
CONVERSION_FUNCTION_TEXT_MAP.put(HiveParser.TOK_LIST, "toArray");
CONVERSION_FUNCTION_TEXT_MAP.put(HiveParser.TOK_STRUCT, "toStruct");
diff --git
a/ql/src/java/org/apache/hadoop/hive/ql/udf/generic/GenericUDFToVariant.java
b/ql/src/java/org/apache/hadoop/hive/ql/udf/generic/GenericUDFToVariant.java
new file mode 100644
index 00000000000..b2f94bb7f0d
--- /dev/null
+++ b/ql/src/java/org/apache/hadoop/hive/ql/udf/generic/GenericUDFToVariant.java
@@ -0,0 +1,162 @@
+/*
+ * 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.hadoop.hive.ql.udf.generic;
+
+import org.apache.hadoop.hive.ql.exec.Description;
+import org.apache.hadoop.hive.ql.exec.UDFArgumentException;
+import org.apache.hadoop.hive.ql.exec.UDFArgumentLengthException;
+import org.apache.hadoop.hive.ql.exec.UDFArgumentTypeException;
+import org.apache.hadoop.hive.ql.metadata.HiveException;
+import org.apache.hadoop.hive.serde2.objectinspector.ListObjectInspector;
+import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
+import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory;
+import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector;
+import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector;
+import org.apache.hadoop.hive.serde2.objectinspector.VariantObjectInspector;
+import
org.apache.hadoop.hive.serde2.objectinspector.primitive.BinaryObjectInspector;
+import
org.apache.hadoop.hive.serde2.objectinspector.primitive.BooleanObjectInspector;
+import
org.apache.hadoop.hive.serde2.objectinspector.primitive.DateObjectInspector;
+import
org.apache.hadoop.hive.serde2.objectinspector.primitive.DoubleObjectInspector;
+import
org.apache.hadoop.hive.serde2.objectinspector.primitive.FloatObjectInspector;
+import
org.apache.hadoop.hive.serde2.objectinspector.primitive.HiveDecimalObjectInspector;
+import
org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorUtils;
+import
org.apache.hadoop.hive.serde2.objectinspector.primitive.TimestampLocalTZObjectInspector;
+import
org.apache.hadoop.hive.serde2.objectinspector.primitive.TimestampObjectInspector;
+import org.apache.hadoop.hive.serde2.variant.Variant;
+import org.apache.hadoop.hive.serde2.variant.VariantBuilder;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Converts a value to a variant, preserving the value's type as the
corresponding variant
+ * primitive; the typed counterpart of parse_json for types JSON cannot
express (binary,
+ * date, timestamp, decimal). Mirrors Spark's cast to variant: primitive,
array and variant
+ * inputs are accepted; struct and map inputs are not. Timestamps are
truncated to
+ * microseconds, the variant format's precision.
+ */
+@Description(name = "variant", value = "_FUNC_(x) - Converts x to a VARIANT,
preserving its type", extended = """
+ Example:
+ > SELECT CAST(unhex('DEADBEEF') AS VARIANT);
+ "3q2+7w==\"""")
+public class GenericUDFToVariant extends GenericUDF {
+ private static final String VARIANT_TYPE_NAME =
VariantObjectInspector.get().getTypeName();
+
+ private transient ObjectInspector inputOI;
+
+ @Override
+ public ObjectInspector initialize(ObjectInspector[] arguments) throws
UDFArgumentException {
+ if (arguments.length != 1) {
+ throw new UDFArgumentLengthException("variant requires one argument");
+ }
+ checkSupported(arguments[0]);
+ inputOI = arguments[0];
+ return ObjectInspectorFactory.getVariantObjectInspector();
+ }
+
+ private static void checkSupported(ObjectInspector oi) throws
UDFArgumentException {
+ boolean supported = switch (oi.getCategory()) {
+ case PRIMITIVE -> switch
(PrimitiveObjectInspectorUtils.getPrimitiveGrouping(
+ ((PrimitiveObjectInspector) oi).getPrimitiveCategory())) {
+ case INTERVAL_GROUP, UNKNOWN_GROUP -> false;
+ default -> true;
+ };
+ case LIST -> {
+ checkSupported(((ListObjectInspector)
oi).getListElementObjectInspector());
+ yield true;
+ }
+ case STRUCT -> VARIANT_TYPE_NAME.equals(oi.getTypeName());
+ default -> false;
+ };
+ if (!supported) {
+ throw new UDFArgumentTypeException(0, "Cannot cast " + oi.getTypeName()
+ " to VARIANT");
+ }
+ }
+
+ @Override
+ public Object evaluate(DeferredObject[] arguments) throws HiveException {
+ Object value = arguments[0].get();
+ if (value == null) {
+ return null;
+ }
+ try {
+ VariantBuilder builder = new VariantBuilder(false);
+ append(builder, value, inputOI);
+ Variant variant = builder.result();
+ return List.of(variant.getMetadata(), variant.getValue());
+ } catch (RuntimeException e) {
+ throw new HiveException("Cannot cast value to VARIANT: " +
e.getMessage(), e);
+ }
+ }
+
+ private static void append(VariantBuilder builder, Object o, ObjectInspector
oi) throws HiveException {
+ if (o == null) {
+ builder.appendNull();
+ return;
+ }
+ switch (oi.getCategory()) {
+ case PRIMITIVE -> appendPrimitive(builder, o, (PrimitiveObjectInspector)
oi);
+ case LIST -> appendArray(builder, o, (ListObjectInspector) oi);
+ // the variant shape, guaranteed by initialize: embed as-is
+ case STRUCT ->
builder.appendVariant(Variant.from(((StructObjectInspector)
oi).getStructFieldsDataAsList(o)));
+ default -> throw new HiveException("Cannot cast " + oi.getTypeName() + "
to VARIANT");
+ }
+ }
+
+ private static void appendPrimitive(VariantBuilder builder, Object o,
PrimitiveObjectInspector poi)
+ throws HiveException {
+ switch (poi.getPrimitiveCategory()) {
+ case VOID -> builder.appendNull();
+ case BOOLEAN -> builder.appendBoolean(((BooleanObjectInspector)
poi).get(o));
+ case BYTE, SHORT, INT, LONG ->
builder.appendLong(PrimitiveObjectInspectorUtils.getLong(o, poi));
+ case FLOAT -> builder.appendFloat(((FloatObjectInspector) poi).get(o));
+ case DOUBLE -> builder.appendDouble(((DoubleObjectInspector)
poi).get(o));
+ case DECIMAL ->
+ builder.appendDecimal(((HiveDecimalObjectInspector)
poi).getPrimitiveJavaObject(o).bigDecimalValue());
+ case STRING, CHAR, VARCHAR ->
builder.appendString(PrimitiveObjectInspectorUtils.getString(o, poi));
+ case BINARY -> builder.appendBinary(((BinaryObjectInspector)
poi).getPrimitiveJavaObject(o));
+ case DATE -> builder.appendDate(((DateObjectInspector)
poi).getPrimitiveJavaObject(o).toEpochDay());
+ case TIMESTAMP ->
+ builder.appendTimestampNtz(((TimestampObjectInspector)
poi).getPrimitiveJavaObject(o).toEpochMicro());
+ case TIMESTAMPLOCALTZ -> {
+ Instant instant = ((TimestampLocalTZObjectInspector)
poi).getPrimitiveJavaObject(o)
+ .getZonedDateTime().toInstant();
+ builder.appendTimestamp(instant.getEpochSecond() * 1_000_000 +
instant.getNano() / 1_000);
+ }
+ default -> throw new HiveException("Cannot cast " + poi.getTypeName() +
" to VARIANT");
+ }
+ }
+
+ private static void appendArray(VariantBuilder builder, Object o,
ListObjectInspector loi) throws HiveException {
+ int start = builder.getWritePos();
+ int length = loi.getListLength(o);
+ List<Integer> offsets = new ArrayList<>(length);
+ for (int i = 0; i < length; i++) {
+ offsets.add(builder.getWritePos() - start);
+ append(builder, loi.getListElement(o, i),
loi.getListElementObjectInspector());
+ }
+ builder.finishWritingArray(start, offsets);
+ }
+
+ @Override
+ public String getDisplayString(String[] children) {
+ return "CAST( " + children[0] + " AS VARIANT)";
+ }
+}
diff --git
a/ql/src/test/org/apache/hadoop/hive/ql/udf/generic/TestGenericUDFToVariant.java
b/ql/src/test/org/apache/hadoop/hive/ql/udf/generic/TestGenericUDFToVariant.java
new file mode 100644
index 00000000000..271710d5354
--- /dev/null
+++
b/ql/src/test/org/apache/hadoop/hive/ql/udf/generic/TestGenericUDFToVariant.java
@@ -0,0 +1,106 @@
+/*
+ * 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.hadoop.hive.ql.udf.generic;
+
+import org.apache.hadoop.hive.common.type.Date;
+import org.apache.hadoop.hive.common.type.HiveChar;
+import org.apache.hadoop.hive.common.type.HiveDecimal;
+import org.apache.hadoop.hive.common.type.HiveVarchar;
+import org.apache.hadoop.hive.common.type.Timestamp;
+import org.apache.hadoop.hive.ql.exec.UDFArgumentTypeException;
+import org.apache.hadoop.hive.ql.metadata.HiveException;
+import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
+import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoUtils;
+import org.apache.hadoop.hive.serde2.variant.Variant;
+import org.apache.hadoop.hive.serde2.variant.VariantBuilder;
+import org.junit.jupiter.api.Test;
+
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class TestGenericUDFToVariant {
+
+ @SuppressWarnings("unchecked")
+ private static String toJson(String typeString, Object value) throws
Exception {
+ GenericUDFToVariant udf = new GenericUDFToVariant();
+ udf.initialize(new ObjectInspector[] {
TypeInfoUtils.getStandardJavaObjectInspectorFromTypeInfo(
+ TypeInfoUtils.getTypeInfoFromTypeString(typeString)) });
+ Object result = udf.evaluate(new GenericUDF.DeferredObject[] { new
GenericUDF.DeferredJavaObject(value) });
+ if (result == null) {
+ return null;
+ }
+ List<byte[]> variant = (List<byte[]>) result;
+ return new Variant(variant.get(1), variant.get(0)).toJson(ZoneOffset.UTC);
+ }
+
+ @Test
+ public void testPrimitives() throws Exception {
+ assertEquals("true", toJson("boolean", true));
+ assertEquals("42", toJson("int", 42));
+ assertEquals("9007199254740993", toJson("bigint", 9007199254740993L));
+ assertEquals("1.5", toJson("double", 1.5d));
+ assertEquals("1.5", toJson("decimal(4,2)", HiveDecimal.create("1.50")));
+ assertEquals("\"John\"", toJson("string", "John"));
+ assertEquals("\"ab\"", toJson("varchar(5)", new HiveVarchar("ab", 5)));
+ // char padding is stripped, consistent with Hive's char-to-string coercion
+ assertEquals("\"ab\"", toJson("char(5)", new HiveChar("ab", 5)));
+ assertNull(toJson("int", null));
+ }
+
+ @Test
+ public void testBinaryBecomesTypedNode() throws Exception {
+ // base64 of DEADBEEF; a real BINARY node, unreachable via parse_json
+ assertEquals("\"3q2+7w==\"", toJson("binary", new byte[] { (byte) 0xDE,
(byte) 0xAD, (byte) 0xBE, (byte) 0xEF }));
+ }
+
+ @Test
+ public void testDateAndTimestamp() throws Exception {
+ assertEquals("\"2026-07-28\"", toJson("date", Date.valueOf("2026-07-28")));
+ assertEquals("\"2026-07-28 10:00:00\"", toJson("timestamp",
Timestamp.valueOf("2026-07-28 10:00:00")));
+ }
+
+ @Test
+ public void testArrayWithNulls() throws Exception {
+ assertEquals("[1,null,3]", toJson("array<int>", Arrays.asList(1, null,
3)));
+ }
+
+ @Test
+ public void testVariantInputIsEmbeddedAsIs() throws Exception {
+ Variant v = VariantBuilder.parseJson("{\"k\":1}", false);
+ assertEquals("{\"k\":1}", toJson("variant", List.of(v.getMetadata(),
v.getValue())));
+ }
+
+ @Test
+ public void testStructAndMapRejected() {
+ assertThrows(UDFArgumentTypeException.class, () -> toJson("struct<a:int>",
null));
+ assertThrows(UDFArgumentTypeException.class, () ->
toJson("map<string,int>", null));
+ }
+
+ @Test
+ public void testMalformedVariantInputThrows() {
+ // strict like parse_json and Spark's cast: bytes that are not variant
encoding fail loudly
+ assertThrows(HiveException.class,
+ () -> toJson("struct<metadata:binary,value:binary>", Arrays.asList(new
byte[] { 9 }, new byte[] { 1 })));
+ }
+}
diff --git a/ql/src/test/results/clientpositive/llap/show_functions.q.out
b/ql/src/test/results/clientpositive/llap/show_functions.q.out
index 605e953d634..dd17569bf0a 100644
--- a/ql/src/test/results/clientpositive/llap/show_functions.q.out
+++ b/ql/src/test/results/clientpositive/llap/show_functions.q.out
@@ -491,6 +491,7 @@ validate_acid_sort_order
var_pop
var_samp
variance
+variant
variant_get
version
weekofyear
@@ -1133,6 +1134,7 @@ validate_acid_sort_order
var_pop
var_samp
variance
+variant
variant_get
version
weekofyear