diff --git a/native/spark-expr/src/array_funcs/array_slice.rs b/native/spark-expr/src/array_funcs/array_slice.rs new file mode 100644 index 0000000000..ec6ac46b03 --- /dev/null +++ b/native/spark-expr/src/array_funcs/array_slice.rs @@ -0,0 +1,379 @@ +// 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. + +// Spark-compatible slice(array, start, length). +// +// Differs from datafusion-spark's SparkSlice in that we correctly return an +// empty array when a negative start position lies before the beginning of the +// array. The upstream implementation (as of datafusion-spark 53.1.0) produces +// the first element instead. Once the upstream is fixed, this can be removed +// in favour of datafusion_spark::function::array::slice::SparkSlice. + +use arrow::array::{ + make_array, Array, ArrayRef, AsArray, Capacities, GenericListArray, Int64Array, + MutableArrayData, NullBufferBuilder, OffsetSizeTrait, +}; +use arrow::buffer::OffsetBuffer; +use arrow::datatypes::{DataType, FieldRef}; +use datafusion::common::{cast::as_int64_array, exec_err, utils::take_function_args, Result}; +use datafusion::logical_expr::{ + ColumnarValue, ReturnFieldArgs, ScalarFunctionArgs, ScalarUDFImpl, Signature, TypeSignature, + Volatility, +}; +use std::any::Any; +use std::sync::Arc; + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct SparkArraySlice { + signature: Signature, +} + +impl Default for SparkArraySlice { + fn default() -> Self { + Self::new() + } +} + +impl SparkArraySlice { + pub fn new() -> Self { + Self { + signature: Signature::new(TypeSignature::Any(3), Volatility::Immutable), + } + } +} + +impl ScalarUDFImpl for SparkArraySlice { + fn as_any(&self) -> &dyn Any { + self + } + + fn name(&self) -> &str { + "spark_array_slice" + } + + fn signature(&self) -> &Signature { + &self.signature + } + + fn return_type(&self, _arg_types: &[DataType]) -> Result { + datafusion::common::internal_err!("return_field_from_args should be used instead") + } + + fn return_field_from_args(&self, args: ReturnFieldArgs) -> Result { + Ok(Arc::clone(&args.arg_fields[0])) + } + + fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result { + let row_count = args.number_rows; + let arrays = args + .args + .iter() + .map(|arg| match arg { + ColumnarValue::Array(a) => Ok(Arc::clone(a)), + ColumnarValue::Scalar(s) => s.to_array_of_size(row_count), + }) + .collect::>>()?; + let [array, start, length] = take_function_args(self.name(), &arrays)?; + let start = as_int64_array(&start)?; + let length = as_int64_array(&length)?; + + let result = match array.data_type() { + DataType::List(_) => slice_list::(array.as_list::(), start, length)?, + DataType::LargeList(_) => slice_list::(array.as_list::(), start, length)?, + other => { + return exec_err!("{} does not support type '{other}'", self.name()); + } + }; + Ok(ColumnarValue::Array(result)) + } +} + +fn slice_list( + list_array: &GenericListArray, + start: &Int64Array, + length: &Int64Array, +) -> Result { + let list_field = match list_array.data_type() { + DataType::List(field) | DataType::LargeList(field) => field, + other => { + return exec_err!("expected List or LargeList, got {other:?}"); + } + }; + + let values = list_array.values(); + let original_data = values.to_data(); + let row_count = list_array.len(); + let mut offsets = Vec::::with_capacity(row_count + 1); + let mut last_offset = O::zero(); + offsets.push(last_offset); + let mut mutable = MutableArrayData::with_capacities( + vec![&original_data], + true, + Capacities::Array(original_data.len()), + ); + let mut nulls = NullBufferBuilder::new(row_count); + + let row_offsets = list_array.offsets(); + let list_nulls = list_array.nulls(); + let start_nulls = start.nulls(); + let length_nulls = length.nulls(); + for row in 0..row_count { + let is_row_null = list_nulls.is_some_and(|n| n.is_null(row)) + || start_nulls.is_some_and(|n| n.is_null(row)) + || length_nulls.is_some_and(|n| n.is_null(row)); + if is_row_null { + offsets.push(last_offset); + nulls.append_null(); + continue; + } + + let start_value = start.value(row); + let length_value = length.value(row); + + if start_value == 0 { + return exec_err!("Unexpected value for start in function slice. Expected a positive or negative number, but got 0."); + } + if length_value < 0 { + return exec_err!( + "Unexpected value for length in function slice. Expected a non-negative number, but got {length_value}." + ); + } + + let row_start = row_offsets[row].as_usize(); + let row_end = row_offsets[row + 1].as_usize(); + let arr_len = (row_end - row_start) as i64; + + let zero_based_start = if start_value > 0 { + start_value - 1 + } else { + start_value + arr_len + }; + + let copied = if zero_based_start < 0 || zero_based_start >= arr_len || length_value == 0 { + 0 + } else { + let take = std::cmp::min(length_value, arr_len - zero_based_start) as usize; + let begin = row_start + zero_based_start as usize; + mutable.extend(0, begin, begin + take); + take + }; + + last_offset += O::usize_as(copied); + offsets.push(last_offset); + nulls.append_non_null(); + } + + Ok(Arc::new(GenericListArray::::try_new( + Arc::clone(list_field), + OffsetBuffer::new(offsets.into()), + make_array(mutable.freeze()), + nulls.finish(), + )?)) +} + +#[cfg(test)] +mod tests { + use super::*; + use arrow::array::{Int32Array, ListArray}; + use arrow::buffer::OffsetBuffer; + use arrow::datatypes::{Field, Int32Type}; + + fn build_list(rows: Vec>>>) -> Arc { + let mut offsets = vec![0i32]; + let mut values: Vec> = Vec::new(); + let mut nulls = NullBufferBuilder::new(rows.len()); + for row in &rows { + match row { + Some(items) => { + nulls.append_non_null(); + values.extend(items.iter().copied()); + } + None => nulls.append_null(), + } + offsets.push(values.len() as i32); + } + let values = Arc::new(Int32Array::from(values)) as ArrayRef; + let field = Arc::new(Field::new("item", DataType::Int32, true)); + Arc::new(ListArray::new( + field, + OffsetBuffer::new(offsets.into()), + values, + nulls.finish(), + )) + } + + fn run( + list: Arc, + start: Vec>, + length: Vec>, + ) -> Vec>>> { + let start = Int64Array::from(start); + let length = Int64Array::from(length); + let result = slice_list::(list.as_ref(), &start, &length).unwrap(); + let result = result.as_list::(); + (0..result.len()) + .map(|i| { + if result.is_null(i) { + None + } else { + let row = result.value(i); + let row = row.as_primitive::(); + Some( + (0..row.len()) + .map(|j| { + if row.is_null(j) { + None + } else { + Some(row.value(j)) + } + }) + .collect(), + ) + } + }) + .collect() + } + + #[test] + fn positive_start() { + let list = build_list(vec![Some(vec![ + Some(1), + Some(2), + Some(3), + Some(4), + Some(5), + ])]); + assert_eq!( + run(list, vec![Some(2)], vec![Some(3)]), + vec![Some(vec![Some(2), Some(3), Some(4)])] + ); + } + + #[test] + fn length_clamped_to_array_end() { + let list = build_list(vec![Some(vec![Some(1), Some(2), Some(3)])]); + assert_eq!( + run(list, vec![Some(2)], vec![Some(100)]), + vec![Some(vec![Some(2), Some(3)])] + ); + } + + #[test] + fn length_zero_returns_empty() { + let list = build_list(vec![Some(vec![Some(1), Some(2), Some(3)])]); + assert_eq!( + run(list, vec![Some(1)], vec![Some(0)]), + vec![Some(Vec::new())] + ); + } + + #[test] + fn start_past_end_returns_empty() { + let list = build_list(vec![Some(vec![Some(1), Some(2), Some(3)])]); + assert_eq!( + run(list, vec![Some(10)], vec![Some(1)]), + vec![Some(Vec::new())] + ); + } + + #[test] + fn negative_start_counts_from_end() { + let list = build_list(vec![Some(vec![ + Some(1), + Some(2), + Some(3), + Some(4), + Some(5), + ])]); + assert_eq!( + run(list, vec![Some(-2)], vec![Some(2)]), + vec![Some(vec![Some(4), Some(5)])] + ); + } + + #[test] + fn negative_start_overflows_returns_empty() { + // Spark: slice([a], -2, 2) returns []. datafusion-spark returns [a] here. + let list = build_list(vec![Some(vec![Some(1)])]); + assert_eq!( + run(list, vec![Some(-2)], vec![Some(2)]), + vec![Some(Vec::new())] + ); + } + + #[test] + fn negative_start_far_below_zero_returns_empty() { + let list = build_list(vec![Some(vec![Some(1), Some(2), Some(3)])]); + assert_eq!( + run(list, vec![Some(-10)], vec![Some(2)]), + vec![Some(Vec::new())] + ); + } + + #[test] + fn negative_start_with_length_past_end() { + let list = build_list(vec![Some(vec![ + Some(1), + Some(2), + Some(3), + Some(4), + Some(5), + ])]); + assert_eq!( + run(list, vec![Some(-2)], vec![Some(5)]), + vec![Some(vec![Some(4), Some(5)])] + ); + } + + #[test] + fn null_inputs_yield_null() { + let list = build_list(vec![None, Some(vec![Some(1)]), Some(vec![Some(1)])]); + assert_eq!( + run( + list, + vec![Some(1), None, Some(1)], + vec![Some(1), Some(1), None] + ), + vec![None, None, None] + ); + } + + #[test] + fn empty_array_input() { + let list = build_list(vec![Some(Vec::new())]); + assert_eq!( + run(list, vec![Some(1)], vec![Some(2)]), + vec![Some(Vec::new())] + ); + } + + #[test] + fn start_zero_errors() { + let list = build_list(vec![Some(vec![Some(1)])]); + let start = Int64Array::from(vec![Some(0)]); + let length = Int64Array::from(vec![Some(1)]); + assert!(slice_list::(list.as_ref(), &start, &length).is_err()); + } + + #[test] + fn negative_length_errors() { + let list = build_list(vec![Some(vec![Some(1)])]); + let start = Int64Array::from(vec![Some(1)]); + let length = Int64Array::from(vec![Some(-1)]); + assert!(slice_list::(list.as_ref(), &start, &length).is_err()); + } +} diff --git a/native/spark-expr/src/array_funcs/mod.rs b/native/spark-expr/src/array_funcs/mod.rs index 5a503eba39..057b0462ee 100644 --- a/native/spark-expr/src/array_funcs/mod.rs +++ b/native/spark-expr/src/array_funcs/mod.rs @@ -18,6 +18,7 @@ mod array_compact; mod array_insert; mod array_position; +mod array_slice; mod arrays_overlap; mod arrays_zip; mod get_array_struct_fields; @@ -27,6 +28,7 @@ mod size; pub use array_compact::SparkArrayCompact; pub use array_insert::ArrayInsert; pub use array_position::SparkArrayPositionFunc; +pub use array_slice::SparkArraySlice; pub use arrays_overlap::SparkArraysOverlap; pub use arrays_zip::SparkArraysZipFunc; pub use get_array_struct_fields::GetArrayStructFields; diff --git a/native/spark-expr/src/comet_scalar_funcs.rs b/native/spark-expr/src/comet_scalar_funcs.rs index 0957868a60..3480b8def8 100644 --- a/native/spark-expr/src/comet_scalar_funcs.rs +++ b/native/spark-expr/src/comet_scalar_funcs.rs @@ -24,9 +24,9 @@ use crate::math_funcs::modulo_expr::spark_modulo; use crate::{ spark_ceil, spark_decimal_div, spark_decimal_integral_div, spark_floor, spark_isnan, spark_lpad, spark_make_decimal, spark_read_side_padding, spark_round, spark_rpad, spark_unhex, - spark_unscaled_value, EvalMode, SparkArrayCompact, SparkArrayPositionFunc, SparkArraysOverlap, - SparkContains, SparkDateDiff, SparkDateFromUnixDate, SparkDateTrunc, SparkMakeDate, - SparkSizeFunc, + spark_unscaled_value, EvalMode, SparkArrayCompact, SparkArrayPositionFunc, SparkArraySlice, + SparkArraysOverlap, SparkContains, SparkDateDiff, SparkDateFromUnixDate, SparkDateTrunc, + SparkMakeDate, SparkSizeFunc, }; use arrow::datatypes::DataType; use datafusion::common::{DataFusionError, Result as DataFusionResult}; @@ -208,6 +208,7 @@ fn all_scalar_functions() -> Vec> { vec![ Arc::new(ScalarUDF::new_from_impl(SparkArrayCompact::default())), Arc::new(ScalarUDF::new_from_impl(SparkArrayPositionFunc::default())), + Arc::new(ScalarUDF::new_from_impl(SparkArraySlice::default())), Arc::new(ScalarUDF::new_from_impl(SparkArraysOverlap::default())), Arc::new(ScalarUDF::new_from_impl(SparkContains::default())), Arc::new(ScalarUDF::new_from_impl(SparkDateDiff::default())), diff --git a/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala b/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala index 448c2c2cb3..08bbfce5a5 100644 --- a/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala +++ b/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala @@ -61,6 +61,7 @@ object QueryPlanSerde extends Logging with CometExprShim with CometTypeShim { classOf[ArrayPosition] -> CometArrayPosition, classOf[ArrayRemove] -> CometArrayRemove, classOf[ArrayRepeat] -> CometArrayRepeat, + classOf[Slice] -> CometSlice, classOf[SortArray] -> CometSortArray, classOf[ArraysOverlap] -> CometArraysOverlap, classOf[ArrayUnion] -> CometArrayUnion, diff --git a/spark/src/main/scala/org/apache/comet/serde/arrays.scala b/spark/src/main/scala/org/apache/comet/serde/arrays.scala index 5edc08840a..de6e0d9923 100644 --- a/spark/src/main/scala/org/apache/comet/serde/arrays.scala +++ b/spark/src/main/scala/org/apache/comet/serde/arrays.scala @@ -22,7 +22,7 @@ package org.apache.comet.serde import scala.annotation.tailrec import scala.jdk.CollectionConverters._ -import org.apache.spark.sql.catalyst.expressions.{And, ArrayAppend, ArrayContains, ArrayExcept, ArrayFilter, ArrayInsert, ArrayIntersect, ArrayJoin, ArrayMax, ArrayMin, ArrayPosition, ArrayRemove, ArrayRepeat, ArraysOverlap, ArraysZip, ArrayUnion, Attribute, CreateArray, ElementAt, EmptyRow, Expression, Flatten, GetArrayItem, IsNotNull, Literal, Reverse, Size, SortArray} +import org.apache.spark.sql.catalyst.expressions.{And, ArrayAppend, ArrayContains, ArrayExcept, ArrayFilter, ArrayInsert, ArrayIntersect, ArrayJoin, ArrayMax, ArrayMin, ArrayPosition, ArrayRemove, ArrayRepeat, ArraysOverlap, ArraysZip, ArrayUnion, Attribute, Cast, CreateArray, ElementAt, EmptyRow, Expression, Flatten, GetArrayItem, IsNotNull, Literal, Reverse, Size, Slice, SortArray} import org.apache.spark.sql.catalyst.util.GenericArrayData import org.apache.spark.sql.internal.SQLConf import org.apache.spark.sql.types._ @@ -451,6 +451,30 @@ object CometArrayInsert extends CometExpressionSerde[ArrayInsert] { } } +object CometSlice extends CometExpressionSerde[Slice] { + override def convert( + expr: Slice, + inputs: Seq[Attribute], + binding: Boolean): Option[ExprOuterClass.Expr] = { + val elementType = expr.x.dataType.asInstanceOf[ArrayType].elementType + val arrayExprProto = exprToProto(expr.x, inputs, binding) + val startExprProto = exprToProto(Cast(expr.start, LongType), inputs, binding) + val lengthExprProto = exprToProto(Cast(expr.length, LongType), inputs, binding) + // DataFusion list types always have nullable inner elements, so promise + // ArrayType(elementType, containsNull = true) here even if Spark's + // expr.dataType reports containsNull = false (e.g. for array(1, 2, 3)). + val sliceScalarExpr = + scalarFunctionExprToProtoWithReturnType( + "spark_array_slice", + ArrayType(elementType, containsNull = true), + false, + arrayExprProto, + startExprProto, + lengthExprProto) + optExprWithInfo(sliceScalarExpr, expr, expr.children: _*) + } +} + object CometArrayUnion extends CometExpressionSerde[ArrayUnion] { override def convert( expr: ArrayUnion, diff --git a/spark/src/test/resources/sql-tests/expressions/array/slice.sql b/spark/src/test/resources/sql-tests/expressions/array/slice.sql new file mode 100644 index 0000000000..fcb2bb254b --- /dev/null +++ b/spark/src/test/resources/sql-tests/expressions/array/slice.sql @@ -0,0 +1,261 @@ +-- 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. + +statement +CREATE TABLE test_slice(int_arr array, str_arr array, start_idx int, len int) USING parquet + +statement +INSERT INTO test_slice VALUES + (array(1, 2, 3, 4, 5), array('a', 'b', 'c', 'd', 'e'), 2, 3), + (array(1, 2, NULL, 4), array('a', NULL, 'c'), 1, 2), + (array(10, 20, 30), array('x', 'y', 'z'), -2, 2), + (array(), array(), 1, 1), + (NULL, NULL, 1, 2), + (array(1, 2, 3), array('a', 'b', 'c'), 4, 1), + (array(1, 2, 3), array('a', 'b', 'c'), -10, 2), + (array(1, 2, 3, 4), array('a', 'b', 'c', 'd'), -2, 100), + (array(7, 8, 9), array('p', 'q', 'r'), NULL, 2), + (array(7, 8, 9), array('p', 'q', 'r'), 1, NULL) + +-- all literal arguments +query +SELECT slice(array(1, 2, 3, 4, 5), 2, 3) + +query +SELECT slice(array(1, 2, 3, 4, 5), 1, 5) + +query +SELECT slice(array(1, 2, 3, 4, 5), -2, 2) + +query +SELECT slice(array(1, 2, 3, 4, 5), -3, 5) + +-- length 0 returns empty array +query +SELECT slice(array(1, 2, 3, 4), 1, 0) + +-- length larger than remaining elements is clamped +query +SELECT slice(array(1, 2, 3, 4), 2, 100) + +-- start past end returns empty array +query +SELECT slice(array(1, 2, 3), 10, 1) + +-- empty array input +query +SELECT slice(array(), 1, 1) + +-- NULL array returns NULL +query +SELECT slice(cast(NULL as array), 1, 2) + +-- NULL start returns NULL +query +SELECT slice(array(1, 2, 3), cast(NULL as int), 2) + +-- NULL length returns NULL +query +SELECT slice(array(1, 2, 3), 1, cast(NULL as int)) + +-- column array + column start + column length +query +SELECT slice(int_arr, start_idx, len) FROM test_slice + +-- column array + literal start + literal length +query +SELECT slice(int_arr, 1, 2) FROM test_slice + +-- column array + column start + literal length +query +SELECT slice(int_arr, start_idx, 2) FROM test_slice + +-- column array + literal start + column length +query +SELECT slice(int_arr, 1, len) FROM test_slice + +-- string column array + column args +query +SELECT slice(str_arr, start_idx, len) FROM test_slice + +-- string column array + literal args +query +SELECT slice(str_arr, 1, 2) FROM test_slice + +-- expressions for start / length +query +SELECT slice(int_arr, start_idx + 1, len - 1) FROM test_slice WHERE start_idx > 0 AND len > 1 + +-- boolean arrays +statement +CREATE TABLE test_slice_bool(arr array) USING parquet + +statement +INSERT INTO test_slice_bool VALUES + (array(true, false, true, false)), + (array(true)), + (NULL), + (array()) + +query +SELECT slice(arr, 1, 2) FROM test_slice_bool + +query +SELECT slice(arr, -1, 1) FROM test_slice_bool + +-- tinyint arrays +statement +CREATE TABLE test_slice_byte(arr array) USING parquet + +statement +INSERT INTO test_slice_byte VALUES + (array(cast(1 as tinyint), cast(2 as tinyint), cast(3 as tinyint))), + (array(cast(-128 as tinyint), cast(0 as tinyint), cast(127 as tinyint))), + (NULL) + +query +SELECT slice(arr, 2, 2) FROM test_slice_byte + +-- smallint arrays +statement +CREATE TABLE test_slice_short(arr array) USING parquet + +statement +INSERT INTO test_slice_short VALUES + (array(cast(100 as smallint), cast(200 as smallint), cast(300 as smallint))), + (array(cast(-1 as smallint), cast(0 as smallint))), + (NULL) + +query +SELECT slice(arr, 1, 2) FROM test_slice_short + +-- bigint arrays +statement +CREATE TABLE test_slice_long(arr array) USING parquet + +statement +INSERT INTO test_slice_long VALUES + (array(cast(1000000000000 as bigint), cast(2000000000000 as bigint), cast(3000000000000 as bigint))), + (array(cast(-1 as bigint), cast(0 as bigint), cast(1 as bigint))), + (NULL) + +query +SELECT slice(arr, -2, 2) FROM test_slice_long + +-- float arrays +statement +CREATE TABLE test_slice_float(arr array) USING parquet + +statement +INSERT INTO test_slice_float VALUES + (array(cast(1.1 as float), cast(2.2 as float), cast(3.3 as float))), + (array(cast(0.0 as float), cast(-1.5 as float), cast('NaN' as float), cast('Infinity' as float))), + (NULL) + +query +SELECT slice(arr, 1, 2) FROM test_slice_float + +-- double arrays +statement +CREATE TABLE test_slice_double(arr array) USING parquet + +statement +INSERT INTO test_slice_double VALUES + (array(1.1, 2.2, 3.3, 4.4)), + (array(0.0, -1.5, cast('NaN' as double), cast('Infinity' as double))), + (NULL) + +query +SELECT slice(arr, 2, 3) FROM test_slice_double + +-- decimal arrays +statement +CREATE TABLE test_slice_decimal(arr array) USING parquet + +statement +INSERT INTO test_slice_decimal VALUES + (array(cast(1.10 as decimal(10,2)), cast(2.20 as decimal(10,2)), cast(3.30 as decimal(10,2)))), + (array(cast(0.00 as decimal(10,2)))), + (NULL) + +query +SELECT slice(arr, 1, 2) FROM test_slice_decimal + +-- date arrays +statement +CREATE TABLE test_slice_date(arr array) USING parquet + +statement +INSERT INTO test_slice_date VALUES + (array(date '2024-01-01', date '2024-06-15', date '2024-12-31')), + (array(date '2000-01-01')), + (NULL) + +query +SELECT slice(arr, 2, 1) FROM test_slice_date + +-- timestamp arrays +statement +CREATE TABLE test_slice_ts(arr array) USING parquet + +statement +INSERT INTO test_slice_ts VALUES + (array(timestamp '2024-01-01 00:00:00', timestamp '2024-06-15 12:30:00', timestamp '2024-12-31 23:59:59')), + (array(timestamp '2000-01-01 00:00:00')), + (NULL) + +query +SELECT slice(arr, 1, 2) FROM test_slice_ts + +-- timestamp_ntz arrays +statement +CREATE TABLE test_slice_ts_ntz(arr array) USING parquet + +statement +INSERT INTO test_slice_ts_ntz VALUES + (array(timestamp_ntz '2024-01-01 00:00:00', timestamp_ntz '2024-06-15 12:30:00', timestamp_ntz '2024-12-31 23:59:59')), + (array(timestamp_ntz '2000-01-01 00:00:00')), + (NULL) + +query +SELECT slice(arr, 1, 2) FROM test_slice_ts_ntz + +-- nested int arrays +statement +CREATE TABLE test_slice_nested_int(arr array>) USING parquet + +statement +INSERT INTO test_slice_nested_int VALUES + (array(array(1, 2), array(3, 4), array(5, 6))), + (array(array(1, 2, 3))), + (NULL) + +query +SELECT slice(arr, 1, 2) FROM test_slice_nested_int + +-- nested string arrays +statement +CREATE TABLE test_slice_nested_str(arr array>) USING parquet + +statement +INSERT INTO test_slice_nested_str VALUES + (array(array('a', 'b'), array('c', 'd'), array('e', 'f'))), + (array(array('a'))), + (NULL) + +query +SELECT slice(arr, -2, 2) FROM test_slice_nested_str