Skip to content

Commit b599c5e

Browse files
authored
[Analytics Backend] Timechart route end-to-end closure (1/30 → 30/30) (opensearch-project#21747)
Signed-off-by: Kai Huang <huangkaics@gmail.com>
1 parent a6b5e43 commit b599c5e

12 files changed

Lines changed: 813 additions & 15 deletions

File tree

sandbox/libs/analytics-framework/src/main/java/org/opensearch/analytics/spi/ScalarFunction.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,24 @@ public enum ScalarFunction {
228228
DATE_FORMAT(Category.SCALAR, SqlKind.OTHER_FUNCTION),
229229
TIME_FORMAT(Category.SCALAR, SqlKind.OTHER_FUNCTION),
230230
STR_TO_DATE(Category.SCALAR, SqlKind.OTHER_FUNCTION),
231+
/**
232+
* PPL {@code TIMESTAMPDIFF(unit, start, end)} — emits the integer number of {@code unit}s
233+
* between two timestamps. Resolves through the SQL plugin's {@code TimestampDiffFunction}
234+
* UDF named {@code "TIMESTAMPDIFF"}. Lowering target for PPL `timechart`'s {@code per_*}
235+
* aggregations, which expand to {@code DIVIDE(agg * scale, TIMESTAMPDIFF('MILLISECOND',
236+
* @timestamp, TIMESTAMPADD(unit, n, @timestamp)))}. The analytics-backend-datafusion adapter
237+
* rewrites the call into a DataFusion-native expression because the PPL UDF itself is
238+
* unknown to isthmus's default extension catalog.
239+
*/
240+
TIMESTAMPDIFF(Category.SCALAR, SqlKind.OTHER_FUNCTION),
241+
/**
242+
* PPL {@code TIMESTAMPADD(unit, value, timestamp)} — emits {@code timestamp + value units}.
243+
* Resolves through the SQL plugin's {@code TimestampAddFunction} UDF named
244+
* {@code "TIMESTAMPADD"}. Lowering target for PPL `timechart`'s {@code per_*}
245+
* aggregations (see {@link #TIMESTAMPDIFF}). The analytics-backend-datafusion adapter
246+
* rewrites the call into a DataFusion-native interval-add expression.
247+
*/
248+
TIMESTAMPADD(Category.SCALAR, SqlKind.OTHER_FUNCTION),
231249

232250
// ── JSON ────────────────────────────────────────────────────────
233251
JSON_APPEND(Category.SCALAR, SqlKind.OTHER_FUNCTION),

sandbox/plugins/analytics-backend-datafusion/rust/src/api.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -559,11 +559,21 @@ pub unsafe fn sql_to_substrait(
559559
}
560560

561561
/// Lowers a partial-aggregate Substrait plan against a throwaway session and
562-
/// returns its physical output schema. NamedTable references are resolved
563-
/// against empty MemTables built from the substrait base_schema — the plan
564-
/// itself is the source of truth for the producer side, so no view-type or
565-
/// timestamp-precision rewrites are applied here. The plan is dropped at
562+
/// returns its physical output schema **narrowed via
563+
/// [`schema_coerce::coerce_inferred_schema`]** to types Substrait can bind
564+
/// against. NamedTable references are resolved against empty MemTables built
565+
/// from the substrait base_schema — the plan itself is the source of truth for
566+
/// the producer side, so no view-type or timestamp-precision rewrites are
567+
/// applied here beyond the post-physical coercer. The plan is dropped at
566568
/// function exit; only the schema is returned.
569+
///
570+
/// <p>The coercer flips Arrow-only types (notably `UInt64` from DataFusion's
571+
/// `row_number()` physical op) back to their Substrait-compatible counterparts
572+
/// (`Int64` matching isthmus's `ROW_NUMBER OVER` declaration). The producer
573+
/// side runs the same coercer + wraps its physical plan with
574+
/// [`crate::relabel_exec::RelabelExec`] (zero-copy bit-tag flip per mismatched
575+
/// column), so runtime batches arrive with the same type tags the consumer
576+
/// registers here — no per-batch cast needed at the partition-stream feed.
567577
fn derive_schema_from_partial_plan(
568578
substrait_bytes: &[u8],
569579
) -> Result<arrow::datatypes::SchemaRef, DataFusionError> {
@@ -641,7 +651,7 @@ fn derive_schema_from_partial_plan(
641651

642652
let logical_plan = futures::executor::block_on(from_substrait_plan(&session_state, &plan))?;
643653
let physical_plan = futures::executor::block_on(session_state.create_physical_plan(&logical_plan))?;
644-
Ok(physical_plan.schema())
654+
Ok(crate::schema_coerce::coerce_inferred_schema(physical_plan.schema()))
645655
}
646656

647657
/// Encodes a Schema as Arrow IPC stream-format bytes (a schema-only message
@@ -794,6 +804,12 @@ pub unsafe fn register_partition_stream(
794804
partial_plan_bytes: &[u8],
795805
) -> Result<(i64, Vec<u8>), DataFusionError> {
796806
let session = &mut *(session_ptr as *mut LocalSession);
807+
// derive_schema_from_partial_plan applies `schema_coerce::coerce_inferred_schema`
808+
// to the physical plan's output schema, matching what isthmus declared on the wire
809+
// for the producer (e.g. `Int64` for `ROW_NUMBER OVER`). The producer side runs the
810+
// same coercer + wraps its physical plan with `RelabelExec`, so the batches arriving
811+
// here through the partition channel are already typed to match this schema — no
812+
// per-batch cast at feed time.
797813
let schema = derive_schema_from_partial_plan(partial_plan_bytes)?;
798814
let schema_ipc = schema_to_ipc_bytes(schema.as_ref())?;
799815
let sender = session.register_partition(input_id, schema)?;

sandbox/plugins/analytics-backend-datafusion/rust/src/indexed_executor.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,12 @@ pub async unsafe fn execute_indexed_with_context(
736736
log_debug!("DataFusion logical plan:\n{}", logical_plan.display_indent());
737737
let dataframe = ctx.execute_logical_plan(logical_plan).await?;
738738
let physical_plan = dataframe.create_physical_plan().await?;
739+
// Retag bit-compatible Int↔UInt output mismatches to match the substrait-declared
740+
// types. The target is schema_coerce::coerce_inferred_schema(physical_schema) — same
741+
// narrowing the partition-stream registration uses, so consumer-side StreamingTable
742+
// and producer-side batches agree by construction (see crate::relabel_exec).
743+
let target_schema = crate::schema_coerce::coerce_inferred_schema(physical_plan.schema());
744+
let physical_plan = crate::relabel_exec::wrap_if_relabel_needed(physical_plan, target_schema)?;
739745
log_debug!("DataFusion physical plan:\n{}", displayable(physical_plan.as_ref()).indent(true));
740746
let df_stream = execute_stream(physical_plan, ctx.task_ctx())
741747
.map_err(|e| DataFusionError::Execution(format!("execute_stream: {}", e)))?;

sandbox/plugins/analytics-backend-datafusion/rust/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub mod memory;
2929
pub mod partition_stream;
3030
pub mod query_executor;
3131
pub mod query_tracker;
32+
pub mod relabel_exec;
3233
pub mod runtime_manager;
3334
pub mod schema_coerce;
3435
pub mod session_context;

sandbox/plugins/analytics-backend-datafusion/rust/src/local_executor.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,12 @@ impl LocalSession {
144144
DataFusionError::Execution(format!("Failed to decode Substrait plan: {}", e))
145145
})?;
146146
let logical_plan = from_substrait_plan(&self.ctx.state(), &plan).await?;
147-
self.ctx
148-
.execute_logical_plan(logical_plan)
149-
.await?
150-
.execute_stream()
151-
.await
147+
let dataframe = self.ctx.execute_logical_plan(logical_plan).await?;
148+
let physical_plan = dataframe.create_physical_plan().await?;
149+
let target_schema = crate::schema_coerce::coerce_inferred_schema(physical_plan.schema());
150+
let physical_plan = crate::relabel_exec::wrap_if_relabel_needed(physical_plan, target_schema)?;
151+
datafusion::physical_plan::execute_stream(physical_plan, self.ctx.task_ctx())
152+
.map_err(|e| DataFusionError::Execution(format!("execute_substrait: {}", e)))
152153
}
153154

154155
/// Returns the memory pool the session's `RuntimeEnv` was built with.
@@ -179,6 +180,8 @@ impl LocalSession {
179180
log_debug!("DataFusion logical plan (reduce):\n{}", logical_plan.display_indent());
180181
let dataframe = self.ctx.execute_logical_plan(logical_plan).await?;
181182
let physical_plan = dataframe.create_physical_plan().await?;
183+
let target_schema = crate::schema_coerce::coerce_inferred_schema(physical_plan.schema());
184+
let physical_plan = crate::relabel_exec::wrap_if_relabel_needed(physical_plan, target_schema)?;
182185
log_debug!("DataFusion physical plan (reduce):\n{}", displayable(physical_plan.as_ref()).indent(true));
183186
let stripped = crate::agg_mode::apply_aggregate_mode(
184187
physical_plan,

sandbox/plugins/analytics-backend-datafusion/rust/src/query_executor.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ pub async fn execute_query(
152152
let logical_plan = from_substrait_plan(&ctx.state(), &substrait_plan).await?;
153153
let dataframe = ctx.execute_logical_plan(logical_plan).await?;
154154
let physical_plan = dataframe.create_physical_plan().await?;
155+
// Retag any physical-plan output columns whose type tags differ from what Substrait
156+
// declared on bit-compatible Int↔UInt pairs (see crate::relabel_exec). The target is
157+
// schema_coerce::coerce_inferred_schema(physical_schema) — the same narrowing the
158+
// partition-stream registration uses, so the consumer's StreamingTable and the
159+
// batches arriving from this producer agree by construction.
160+
let target_schema = crate::schema_coerce::coerce_inferred_schema(physical_plan.schema());
161+
let physical_plan = crate::relabel_exec::wrap_if_relabel_needed(physical_plan, target_schema)?;
155162

156163
let df_stream = execute_stream(physical_plan, ctx.task_ctx()).map_err(|e| {
157164
error!("Failed to create execution stream: {}", e);
@@ -197,6 +204,8 @@ pub async fn execute_with_context(
197204
log_debug!("DataFusion logical plan:\n{}", logical_plan.display_indent());
198205
let dataframe = handle.ctx.execute_logical_plan(logical_plan).await?;
199206
let physical_plan = dataframe.create_physical_plan().await?;
207+
let target_schema = crate::schema_coerce::coerce_inferred_schema(physical_plan.schema());
208+
let physical_plan = crate::relabel_exec::wrap_if_relabel_needed(physical_plan, target_schema)?;
200209
log_debug!("DataFusion physical plan:\n{}", displayable(physical_plan.as_ref()).indent(true));
201210

202211
let df_stream = execute_stream(physical_plan, handle.ctx.task_ctx()).map_err(|e| {

0 commit comments

Comments
 (0)