Skip to content

Commit c705d0f

Browse files
officialasishkumarasish-deccan
authored andcommitted
[AURON #2177] Implement native support for lag window function
Spark `lag(...)` is not supported in Auron's native window execution path, causing queries using it to fall back to Spark instead of running natively. This maps Spark `Lag` window expressions to the existing native `LEAD` window function by passing Spark's signed `offset`, so lag reuses the native LeadProcessor and offset-window execution path already present on master. Changes included here: - add `Lag` handling in `NativeWindowBase` - keep `lag(... IGNORE NULLS)` on the Spark fallback path - make reflective `ignoreNulls` detection deterministic by defaulting to false on reflection failures - make `LeadProcessor` return execution errors for malformed children and handle empty batches without reading offset row zero - add Scala regression tests for native `lag(...)` and fallback for `lag(... IGNORE NULLS)` Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
1 parent 572ce5a commit c705d0f

3 files changed

Lines changed: 70 additions & 12 deletions

File tree

native-engine/datafusion-ext-plans/src/window/processors/lead_processor.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515

1616
use std::sync::Arc;
1717

18-
use arrow::{array::ArrayRef, datatypes::DataType, record_batch::RecordBatch};
18+
use arrow::{
19+
array::{ArrayRef, new_empty_array},
20+
datatypes::DataType,
21+
record_batch::RecordBatch,
22+
};
1923
use datafusion::{
2024
common::{DataFusionError, Result, ScalarValue},
2125
physical_expr::PhysicalExprRef,
@@ -36,15 +40,19 @@ impl LeadProcessor {
3640

3741
impl WindowFunctionProcessor for LeadProcessor {
3842
fn process_batch(&mut self, context: &WindowContext, batch: &RecordBatch) -> Result<ArrayRef> {
39-
assert_eq!(
40-
self.children.len(),
41-
3,
42-
"lead expects input/offset/default children",
43-
);
43+
if self.children.len() != 3 {
44+
return Err(DataFusionError::Execution(format!(
45+
"lead expects input/offset/default children, got {}",
46+
self.children.len()
47+
)));
48+
}
4449

4550
let input_values = self.children[0]
4651
.evaluate(batch)
4752
.and_then(|v| v.into_array(batch.num_rows()))?;
53+
if batch.num_rows() == 0 {
54+
return Ok(new_empty_array(input_values.data_type()));
55+
}
4856

4957
let offset_values = self.children[1]
5058
.evaluate(batch)

spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronWindowSuite.scala

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,43 @@ class AuronWindowSuite extends AuronQueryTest with BaseAuronSQLSuite with AuronS
6161
}
6262
}
6363
}
64+
65+
test("lag window function") {
66+
withSQLConf("spark.auron.enable.window" -> "true") {
67+
withTable("t1") {
68+
sql("create table t1(id int, grp int, v string) using parquet")
69+
sql("insert into t1 values (1, 1, 'a'), (2, 1, null), (3, 1, 'c'), (4, 2, 'x')")
70+
71+
checkSparkAnswerAndOperator("""select
72+
| id,
73+
| grp,
74+
| v,
75+
| lag(v) over (partition by grp order by id) as prev_v,
76+
| lag(v, 2, 'fallback') over (partition by grp order by id) as prev2_v
77+
|from t1
78+
|""".stripMargin)
79+
}
80+
}
81+
}
82+
83+
test("lag window function with ignore nulls falls back") {
84+
if (AuronTestUtils.isSparkV32OrGreater) {
85+
withSQLConf("spark.auron.enable.window" -> "true") {
86+
withTable("t1") {
87+
sql("create table t1(id int, grp int, v string) using parquet")
88+
sql("insert into t1 values (1, 1, 'a'), (2, 1, null), (3, 1, 'c'), (4, 2, 'x')")
89+
90+
val df = checkSparkAnswer("""select
91+
| id,
92+
| grp,
93+
| lag(v, 1, 'fallback') ignore nulls
94+
| over (partition by grp order by id) as prev_non_null_v
95+
|from t1
96+
|""".stripMargin)
97+
val plan = stripAQEPlan(df.queryExecution.executedPlan)
98+
assert(plan.collectFirst { case _: NativeWindowBase => true }.isEmpty)
99+
}
100+
}
101+
}
102+
}
64103
}

spark-extension/src/main/scala/org/apache/spark/sql/execution/auron/plan/NativeWindowBase.scala

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package org.apache.spark.sql.execution.auron.plan
1818

1919
import scala.collection.immutable.SortedMap
2020
import scala.jdk.CollectionConverters._
21+
import scala.util.Try
2122

2223
import org.apache.spark.OneToOneDependency
2324
import org.apache.spark.sql.auron.NativeConverters
@@ -29,6 +30,7 @@ import org.apache.spark.sql.catalyst.expressions.Attribute
2930
import org.apache.spark.sql.catalyst.expressions.CumeDist
3031
import org.apache.spark.sql.catalyst.expressions.DenseRank
3132
import org.apache.spark.sql.catalyst.expressions.Expression
33+
import org.apache.spark.sql.catalyst.expressions.Lag
3234
import org.apache.spark.sql.catalyst.expressions.Lead
3335
import org.apache.spark.sql.catalyst.expressions.Literal
3436
import org.apache.spark.sql.catalyst.expressions.NamedExpression
@@ -92,14 +94,12 @@ abstract class NativeWindowBase(
9294
override def requiredChildOrdering: Seq[Seq[SortOrder]] =
9395
Seq(partitionSpec.map(SortOrder(_, Ascending)) ++ orderSpec)
9496

95-
private def leadIgnoreNulls(expr: Lead): Boolean =
96-
expr.getClass.getMethods
97-
.find(method => method.getName == "ignoreNulls" && method.getParameterCount == 0)
98-
.exists(method => method.invoke(expr).asInstanceOf[Boolean])
99-
10097
private def invokeNoArg[T](expr: Expression, methodName: String): T =
10198
expr.getClass.getMethod(methodName).invoke(expr).asInstanceOf[T]
10299

100+
private def ignoreNulls(expr: Expression): Boolean =
101+
Try(invokeNoArg[Boolean](expr, "ignoreNulls")).getOrElse(false)
102+
103103
private def isNthValue(expr: Expression): Boolean = expr.getClass.getSimpleName == "NthValue"
104104

105105
private def nthValueInput(expr: Expression): Expression = invokeNoArg[Expression](expr, "input")
@@ -180,7 +180,18 @@ abstract class NativeWindowBase(
180180
assert(
181181
spec.frameSpecification == e.frame,
182182
s"window frame not supported: ${spec.frameSpecification}")
183-
assert(!leadIgnoreNulls(e), "window function not supported: lead with IGNORE NULLS")
183+
assert(!ignoreNulls(e), "window function not supported: lead with IGNORE NULLS")
184+
windowExprBuilder.setFuncType(pb.WindowFunctionType.Window)
185+
windowExprBuilder.setWindowFunc(pb.WindowFunction.LEAD)
186+
windowExprBuilder.addChildren(NativeConverters.convertExpr(e.input))
187+
windowExprBuilder.addChildren(NativeConverters.convertExpr(e.offset))
188+
windowExprBuilder.addChildren(NativeConverters.convertExpr(e.default))
189+
190+
case e: Lag =>
191+
assert(
192+
spec.frameSpecification == e.frame,
193+
s"window frame not supported: ${spec.frameSpecification}")
194+
assert(!ignoreNulls(e), "window function not supported: lag with IGNORE NULLS")
184195
windowExprBuilder.setFuncType(pb.WindowFunctionType.Window)
185196
windowExprBuilder.setWindowFunc(pb.WindowFunction.LEAD)
186197
windowExprBuilder.addChildren(NativeConverters.convertExpr(e.input))

0 commit comments

Comments
 (0)