Skip to content

Commit 3786e78

Browse files
authored
Merge pull request #152 from Embucket/df55-array-insert-variant
Convert tagged ARRAY values for Variant-backed INSERT targets
2 parents d412f43 + 5d2ac6a commit 3786e78

3 files changed

Lines changed: 103 additions & 3 deletions

File tree

‎datafusion/sql/src/statement.rs‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use datafusion_common::{
4343
use datafusion_expr::dml::{
4444
CopyTo, InsertOp, MergeIntoAction, MergeIntoClause, MergeIntoClauseKind, MergeIntoOp,
4545
};
46+
use datafusion_expr::expr::ScalarFunction;
4647
use datafusion_expr::expr_rewriter::normalize_col_with_schemas_and_ambiguity_check;
4748
use datafusion_expr::logical_plan::DdlStatement;
4849
use datafusion_expr::logical_plan::builder::project;
@@ -2956,8 +2957,34 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
29562957
let target_field = table_schema.field(i);
29572958
let expr = match value_index {
29582959
Some(v) => {
2959-
Expr::Column(Column::from(source.schema().qualified_field(v)))
2960-
.cast_to(target_field.data_type(), source.schema())?
2960+
let source_field = source.schema().field(v);
2961+
let expr =
2962+
Expr::Column(Column::from(source.schema().qualified_field(v)));
2963+
let expr = if source_field
2964+
.metadata()
2965+
.get("embucket.snowflake_logical_type")
2966+
.is_some_and(|value| value == "ARRAY")
2967+
&& target_field.extension_type_name()
2968+
== Some("arrow.parquet.variant")
2969+
&& source_field.data_type() != target_field.data_type()
2970+
{
2971+
let to_variant = self
2972+
.context_provider
2973+
.get_function_meta("to_variant")
2974+
.ok_or_else(|| {
2975+
DataFusionError::Plan(
2976+
"TO_VARIANT is required to insert ARRAY into VARIANT storage"
2977+
.to_string(),
2978+
)
2979+
})?;
2980+
Expr::ScalarFunction(ScalarFunction::new_udf(
2981+
to_variant,
2982+
vec![expr],
2983+
))
2984+
} else {
2985+
expr
2986+
};
2987+
expr.cast_to(target_field.data_type(), source.schema())?
29612988
}
29622989
// The value is not specified. Fill in the default value for the column.
29632990
None => table_source

‎datafusion/sql/tests/common/mod.rs‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ impl MockSessionState {
114114
}
115115
}
116116

117+
pub(crate) fn variant_storage_type() -> DataType {
118+
DataType::Struct(Fields::from(vec![
119+
Field::new("metadata", DataType::Binary, false),
120+
Field::new("value", DataType::Binary, false),
121+
]))
122+
}
123+
117124
pub(crate) struct MockContextProvider {
118125
pub(crate) state: MockSessionState,
119126
}
@@ -196,6 +203,33 @@ impl ContextProvider for MockContextProvider {
196203
.into(),
197204
),
198205
])),
206+
"untyped_array_source" => Ok(Schema::new(vec![
207+
Field::new("items", DataType::Utf8, true).with_metadata(
208+
[(
209+
"embucket.snowflake_logical_type".to_string(),
210+
"ARRAY".to_string(),
211+
)]
212+
.into(),
213+
),
214+
])),
215+
"plain_string_source" => {
216+
Ok(Schema::new(vec![Field::new("items", DataType::Utf8, true)]))
217+
}
218+
"variant_array_target" => Ok(Schema::new(vec![
219+
Field::new("items", variant_storage_type(), true).with_metadata(
220+
[
221+
(
222+
"ARROW:extension:name".to_string(),
223+
"arrow.parquet.variant".to_string(),
224+
),
225+
(
226+
"embucket.snowflake_logical_type".to_string(),
227+
"ARRAY".to_string(),
228+
),
229+
]
230+
.into(),
231+
),
232+
])),
199233
"orders" => Ok(Schema::new(vec![
200234
Field::new("order_id", DataType::UInt32, false),
201235
Field::new("o_orderkey", DataType::UInt32, false),

‎datafusion/sql/tests/sql_integration.rs‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use std::sync::Arc;
2525
use std::vec;
2626

2727
use arrow::datatypes::{TimeUnit::Nanosecond, *};
28-
use common::MockContextProvider;
28+
use common::{MockContextProvider, variant_storage_type};
2929
use datafusion_common::{
3030
DFSchema, DataFusionError, Result, ScalarValue, assert_contains,
3131
};
@@ -842,6 +842,45 @@ fn plan_insert_does_not_promise_ordinary_target_field_metadata() {
842842
assert!(dml.input.schema().field(0).metadata().is_empty());
843843
}
844844

845+
#[test]
846+
fn plan_insert_converts_tagged_array_to_variant_storage() -> Result<()> {
847+
let state = mock_session_state().with_scalar_function(Arc::new(make_udf(
848+
"to_variant",
849+
vec![DataType::Utf8],
850+
variant_storage_type(),
851+
)));
852+
let plan = logical_plan_from_state(
853+
"INSERT INTO variant_array_target SELECT items FROM untyped_array_source",
854+
&SnowflakeDialect {},
855+
ParserOptions::default(),
856+
state,
857+
)?;
858+
assert_contains!(plan.display_indent().to_string(), "to_variant(");
859+
let LogicalPlan::Dml(dml) = &plan else {
860+
panic!("expected DML plan");
861+
};
862+
assert_eq!(
863+
dml.input.schema().field(0).extension_type_name(),
864+
Some("arrow.parquet.variant")
865+
);
866+
Ok(())
867+
}
868+
869+
#[test]
870+
fn plan_insert_does_not_convert_plain_string_to_array() {
871+
let error = logical_plan_from_state(
872+
"INSERT INTO variant_array_target SELECT items FROM plain_string_source",
873+
&SnowflakeDialect {},
874+
ParserOptions::default(),
875+
mock_session_state(),
876+
)
877+
.unwrap_err();
878+
assert_contains!(
879+
error.to_string(),
880+
"Cannot automatically convert Utf8 to Struct"
881+
);
882+
}
883+
845884
#[rstest]
846885
#[case::duplicate_columns(
847886
"INSERT INTO test_decimal (id, price, price) VALUES (1, 2, 3), (4, 5, 6)",

0 commit comments

Comments
 (0)