andygrove commented on code in PR #5614:
URL: https://github.com/apache/datafusion-comet/pull/5614#discussion_r3906787356


##########
spark/src/main/spark-3.4/org/apache/spark/sql/comet/shims/ShimSparkErrorConverter.scala:
##########
@@ -158,10 +158,21 @@ trait ShimSparkErrorConverter {
         
Some(QueryExecutionErrors.exceedMapSizeLimitError(params("size").toString.toInt))
 
       case "CollectionSizeLimitExceeded" =>
-        // createArrayWithElementsExceedLimitError takes (count: Any) in Spark 
3.4
+        // createArrayWithElementsExceedLimitError takes (count: Any) in Spark 
3.4; pass the
+        // decimal string through since the reported length can exceed Long 
range.
         Some(
           QueryExecutionErrors.createArrayWithElementsExceedLimitError(
-            params("numElements").toString.toLong))
+            params("numElements").toString))
+
+      case "SequenceIllegalBoundaries" =>
+        // Spark 3.x codegen throws a plain IllegalArgumentException for 
sequence boundaries.
+        Some(
+          new IllegalArgumentException(
+            s"Illegal sequence boundaries: ${params("start")} to 
${params("stop")} " +
+              s"by ${params("step")}"))
+
+      case "Internal" =>

Review Comment:
   The `case "Internal"` arm changes behavior well beyond `sequence`. There are 
around twenty `SparkError::Internal` producers today in `temporal.rs`, 
`numeric.rs`, `conversion_funcs/string.rs` and `rlike.rs`, and all of them 
previously fell through to the `None` branch in `SparkErrorConverter`, which 
renders as `new SparkException(msgParams.mkString(", "))`, so users saw 
`(message,<text>)`. After this they all become `[INTERNAL_ERROR] <text>`.
   
   That is a clear improvement and I am not asking you to revert it. Could you 
call it out in the PR description though? It is a user-visible message change 
for a set of expressions that have nothing to do with `sequence`, and right now 
the "How are these changes tested?" section does not mention it. It would also 
be worth a pass over the Spark SQL suite diffs to confirm nothing was matching 
on the old shape.
   
   The same arm is added to the 3.5 and 4.x shims, so this applies to all three.



##########
spark/src/test/resources/sql-tests/expressions/array/sequence.sql:
##########
@@ -15,17 +15,131 @@
 -- specific language governing permissions and limitations
 -- under the License.
 
--- Routes sequence through the codegen dispatcher so behavior matches Spark 
exactly.
+-- sequence(start, stop[, step]) for integral element types runs on the native 
kernel
+-- (https://github.com/apache/datafusion-comet/issues/5349). Date and 
timestamp sequences
+-- stay on the JVM codegen dispatcher and are exercised at the bottom of this 
file.
 
 statement
-CREATE TABLE test_sequence(a int, b int) USING parquet
+CREATE TABLE test_sequence(
+  b_start tinyint, b_stop tinyint, b_step tinyint,
+  s_start smallint, s_stop smallint, s_step smallint,
+  i_start int, i_stop int, i_step int,
+  l_start bigint, l_stop bigint, l_step bigint)
+USING parquet
 
+-- Row 2 descends, row 3 has start == stop, rows 4-6 carry NULLs in each 
argument position.
 statement
-INSERT INTO test_sequence VALUES (1, 5), (5, 1), (3, 3), (NULL, 5)
+INSERT INTO test_sequence VALUES
+  (1Y, 5Y, 1Y, 1S, 5S, 1S, 1, 10, 3, 1L, 5L, 2L),
+  (-3Y, -1Y, 1Y, 100S, 90S, -2S, 20, 2, -6, 9223372036854775802L, 
9223372036854775807L, 1L),
+  (0Y, 0Y, 0Y, -5S, -5S, 0S, 7, 7, 0, -9223372036854775808L, 
-9223372036854775800L, 3L),
+  (NULL, 5Y, 1Y, NULL, 5S, 1S, NULL, 10, 1, NULL, 5L, 1L),
+  (1Y, NULL, 1Y, 1S, NULL, 1S, 1, NULL, 1, 1L, NULL, 1L),
+  (1Y, 5Y, NULL, 1S, 5S, NULL, 1, 10, NULL, 1L, 5L, NULL)
+
+-- ============================================================================
+-- Explicit step, all four integral types
+-- ============================================================================
+
+query
+SELECT sequence(i_start, i_stop, i_step) FROM test_sequence
 
 query
-SELECT a, b, sequence(a, b) FROM test_sequence
+SELECT sequence(l_start, l_stop, l_step) FROM test_sequence
+
+-- Column step for the narrow integral types exercises the Byte/Short 
monomorphizations
+-- of the native kernel, not just the literal-step shape.
+query
+SELECT sequence(b_start, b_stop, b_step) FROM test_sequence
+
+query
+SELECT sequence(s_start, s_stop, s_step) FROM test_sequence
+
+-- ============================================================================
+-- Default step: per-row start <= stop ? 1 : -1, both directions in one column
+-- ============================================================================
+
+query
+SELECT sequence(b_start, b_stop), sequence(s_start, s_stop) FROM test_sequence
+
+query
+SELECT sequence(i_start, i_stop), sequence(l_start, l_stop) FROM test_sequence
+
+-- ============================================================================
+-- Literal and mixed literal/column arguments
+-- ============================================================================
+
+query
+SELECT sequence(1, 10), sequence(10, 1), sequence(5, 5), sequence(5, 5, 0)
 
--- literal arguments with step
 query
 SELECT sequence(1, 5), sequence(5, 1, -1), sequence(1, 10, 2)
+
+query
+SELECT sequence(1L, 9L, 2L), sequence(-128Y, -120Y), sequence(32760S, 32767S)
+
+-- On row 2 the source row is (i_start=20, i_stop=2, i_step=-6), so the 
literal-step column
+-- asks for sequence(1, 2, 2) = [1] while the default-step column asks for 
sequence(20, 25)
+-- = [20, 21, 22, 23, 24, 25]. The two columns disagreeing in direction on the 
same row is
+-- intentional coverage, not an oversight.
+query
+SELECT sequence(1, i_stop, 2), sequence(i_start, 25) FROM test_sequence WHERE 
i_start IS NOT NULL AND i_stop IS NOT NULL
+
+query
+SELECT sequence(CAST(NULL AS int), 5), sequence(1, CAST(NULL AS int)), 
sequence(1, 5, CAST(NULL AS int))
+
+-- Integer.MIN_VALUE/MAX_VALUE bounds for int, and a sequence spanning zero
+query
+SELECT sequence(2147483642, 2147483647), sequence(-2147483648, -2147483643), 
sequence(-3, 3, 3)
+
+-- ============================================================================
+-- sequence feeding explode, the common date-spine shape (with integers)
+-- ============================================================================
+
+query
+SELECT i_start, x FROM test_sequence LATERAL VIEW explode(sequence(i_start, 
i_stop)) AS x WHERE i_start IS NOT NULL AND i_stop IS NOT NULL
+
+-- ============================================================================
+-- Error paths: step direction contradicts bounds, or zero step with start != 
stop
+-- ============================================================================
+
+query expect_error(Illegal sequence boundaries: 1 to 5 by -1)

Review Comment:
   The fixture is thorough on the error paths. There are three shapes I think 
are worth pinning down that it does not reach today.
   
   The first is full narrow-type range, `sequence(-128Y, 127Y)` and 
`sequence(-32768S, 32767S)`. That is the one place where Spark's `arr(i) = 
start + step * num.fromInt(i)` genuinely wraps at 8 and 16 bits while your 
kernel accumulates in i64 and truncates on the way out. The two agree because 
the true value is always in range, but it is the case I would most want a 
regression test on, and `sequence(-128Y, -120Y)` does not get there.
   
   The second is a step whose product with the index overflows int, something 
like `sequence(-2147483648, 2147483647, 1073741824)`, which exercises the 
`Int32` monomorphization at the boundary.
   
   The third is `sequence` under a `CASE WHEN` where the throwing branch is not 
taken, for example `SELECT CASE WHEN step > 0 THEN sequence(1, 5, step) ELSE 
array(-1) END FROM t` with rows carrying negative and zero steps. DataFusion 
filters the batch before evaluating each `then` branch so this works today, but 
it is the one construct where an eagerly evaluated throwing expression would 
diverge from Spark, and it would be cheap insurance.
   
   I ran all three locally against this branch and they pass on both 4.1 and 
3.5, so this is about locking the behavior in rather than chasing a suspected 
bug.



##########
native/spark-expr/src/array_funcs/sequence.rs:
##########
@@ -0,0 +1,388 @@
+// 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 sequence(start, stop[, step]) for integral element types.
+//
+// Mirrors the code Spark's whole-stage codegen emits for `Sequence`
+// 
(`sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/collectionOperations.scala`,
+// identical from 3.4.3 through 4.1.1): the boundary check and 
`Sequence.sequenceLength` decide
+// per row how many elements to generate, then elements are `start + step * 
i`. Unlike the JVM
+// path, which allocates two `long[]` per row and copies every element three 
times, this kernel
+// reserves the Arrow child buffer once for the whole batch and writes each 
element exactly once.
+//
+// Date/timestamp sequences are not handled here; the Scala serde only routes 
IntegralType
+// sequences to this function.
+
+use std::sync::Arc;
+
+use arrow::array::{Array, ArrayRef, ListArray, NullBufferBuilder, 
PrimitiveArray};
+use arrow::buffer::{OffsetBuffer, ScalarBuffer};
+use arrow::datatypes::{
+    ArrowPrimitiveType, DataType, FieldRef, Int16Type, Int32Type, Int64Type, 
Int8Type,
+};
+use datafusion::common::cast::as_primitive_array;
+use datafusion::common::{exec_err, DataFusionError, Result, ScalarValue};
+use datafusion::logical_expr::ColumnarValue;
+
+use crate::SparkError;
+
+/// Spark's ByteArrayMethods.MAX_ROUNDED_ARRAY_LENGTH (Integer.MAX_VALUE - 15).
+const MAX_ROUNDED_ARRAY_LENGTH: i128 = (i32::MAX - 15) as i128;
+
+pub fn spark_sequence(args: &[ColumnarValue], data_type: &DataType) -> 
Result<ColumnarValue> {
+    let child_field = match data_type {
+        DataType::List(field) => Arc::clone(field),
+        other => return exec_err!("spark_sequence expects a List return type, 
got {other:?}"),
+    };
+    if args.len() != 2 && args.len() != 3 {
+        return exec_err!(
+            "spark_sequence expects 2 or 3 arguments, got {}",
+            args.len()
+        );
+    }
+
+    let all_scalar = args
+        .iter()
+        .all(|arg| matches!(arg, ColumnarValue::Scalar(_)));
+    let arrays = ColumnarValue::values_to_arrays(args)?;
+    let step = arrays.get(2);
+
+    let result = match child_field.data_type() {
+        DataType::Int8 => {
+            sequence_integral::<Int8Type>(&arrays[0], &arrays[1], step, 
child_field, |v| v as i8)
+        }
+        DataType::Int16 => {
+            sequence_integral::<Int16Type>(&arrays[0], &arrays[1], step, 
child_field, |v| v as i16)
+        }
+        DataType::Int32 => {
+            sequence_integral::<Int32Type>(&arrays[0], &arrays[1], step, 
child_field, |v| v as i32)
+        }
+        DataType::Int64 => {
+            sequence_integral::<Int64Type>(&arrays[0], &arrays[1], step, 
child_field, |v| v)
+        }
+        other => exec_err!("spark_sequence does not support element type 
{other:?}"),
+    }?;
+
+    if all_scalar {
+        Ok(ColumnarValue::Scalar(ScalarValue::try_from_array(
+            &result, 0,
+        )?))
+    } else {
+        Ok(ColumnarValue::Array(result))
+    }
+}
+
+fn sequence_integral<T: ArrowPrimitiveType>(
+    start: &ArrayRef,
+    stop: &ArrayRef,
+    step: Option<&ArrayRef>,
+    child_field: FieldRef,
+    from_i64: impl Fn(i64) -> T::Native,
+) -> Result<ArrayRef>
+where
+    T::Native: Into<i64>,
+{
+    let start = as_primitive_array::<T>(start)?;
+    let stop = as_primitive_array::<T>(stop)?;
+    let step = step.map(|arr| as_primitive_array::<T>(arr)).transpose()?;
+    let num_rows = start.len();
+
+    let row_is_null = |row: usize| {
+        start.is_null(row) || stop.is_null(row) || step.is_some_and(|arr| 
arr.is_null(row))
+    };
+    // With no explicit step, Spark uses `start <= stop ? 1 : -1` per row, so 
the direction
+    // always matches the bounds and the boundary check below cannot fail.
+    let row_step = |row: usize, start: i64, stop: i64| -> i64 {
+        match step {
+            Some(arr) => arr.value(row).into(),
+            None => {
+                if start <= stop {
+                    1
+                } else {
+                    -1
+                }
+            }
+        }
+    };
+
+    // First pass: compute per-row lengths so the child buffer can be reserved 
once for the
+    // whole batch. Valid rows always produce at least one element, so length 
0 marks a null row.
+    let mut lengths: Vec<usize> = Vec::with_capacity(num_rows);
+    let mut total: usize = 0;
+    for row in 0..num_rows {
+        if row_is_null(row) {
+            lengths.push(0);
+            continue;
+        }
+        let s: i64 = start.value(row).into();
+        let e: i64 = stop.value(row).into();
+        let len = sequence_length(s, e, row_step(row, s, e))?;
+        total += len;
+        lengths.push(len);
+    }
+    // Comet-specific ceiling: the sum of every row's length in one Arrow 
batch must fit in
+    // the i32 offset buffer. Spark has no equivalent guard because it stores 
each row as its
+    // own `long[]`. Report it with the same size-limit error class Spark 
would raise for a
+    // single overlong row so the user sees a familiar message.
+    if total > i32::MAX as usize {
+        return Err(DataFusionError::External(Box::new(
+            SparkError::CollectionSizeLimitExceeded {
+                num_elements: total.to_string(),
+                max_elements: i32::MAX as i64,
+                function_name: "sequence".to_string(),
+            },
+        )));
+    }
+
+    // Second pass: write elements straight into the child buffer and push 
offsets. The
+    // batch-total check above guarantees `values.len() <= i32::MAX` at every 
iteration, so
+    // the offset push cannot overflow.
+    let mut values: Vec<T::Native> = Vec::with_capacity(total);

Review Comment:
   Nice work on the error-parity side of this, the `sequenceLength` port 
matches Spark on all three failure paths and I checked it against 3.4.3 through 
4.1.1.
   
   The thing I keep coming back to is `Vec::with_capacity(total)`. `sequence` 
is the first expression we have made native where the output size is unbounded 
relative to the input size. Every other `with_capacity` in `array_funcs/` is 
sized by `row_count` or `args.len()`, but here `total` is the sum of every 
row's generated length, so a single batch can ask for up to `i32::MAX` 
elements, which is 16 GiB for `bigint`. Your own benchmark shows the shape: 
`seq_long_10000_elems` materializes 8192 x 10000 x 8 bytes, so 655 MB in one 
allocation, where Spark holds one row's `long[]` at a time. That is also the 
one row in your table with no speedup, which makes me wonder whether the 
large-per-row case is paying for itself at all.
   
   Two things I would like to see. Could the allocation go through 
`try_reserve` so an oversized batch surfaces as a query error rather than an 
allocator abort that takes the executor down? And could the batch ceiling be 
documented somewhere the user can find it?
   
   On the ceiling specifically, the message a user gets today is misleading. 
`sequence(0, 262143)` over a full 8192-row batch lands on exactly `2147483648` 
total elements and trips the check, even though every individual array is well 
inside Spark's limit and Spark itself would run the query. The shim ignores the 
`max_elements` you pass, so the message reads "Can't create array with 
2147483648 elements which exceeding the array size limit 2147483632", which 
points the user at a per-array limit that they have not actually exceeded and 
gives them nothing actionable. The real fix on their side is to lower 
`spark.comet.batchSize`. Given that, is `Compatible()` the right support level, 
or should this at least get a compatible note and a line in the audit entry?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to