Skip to content

Commit ce2fbc4

Browse files
committed
Merge branch 'xuanwo/asof-logical' into xuanwo/asof-proto
2 parents 9556ed3 + 3a4f995 commit ce2fbc4

5 files changed

Lines changed: 130 additions & 80 deletions

File tree

datafusion/core/src/physical_planner.rs

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ use datafusion_expr::physical_planning_context::{
9494
use datafusion_expr::utils::{expr_to_columns, split_conjunction};
9595
use datafusion_expr::{
9696
Analyze, BinaryExpr, DescribeTable, DmlStatement, Explain, ExplainFormat, Extension,
97-
FetchType, Filter, JoinConstraint, JoinType, Operator, RecursiveQuery, SkipType,
98-
StringifiedPlan, WindowFrame, WindowFrameBound, WriteOp,
97+
FetchType, Filter, JoinType, Operator, RecursiveQuery, SkipType, StringifiedPlan,
98+
WindowFrame, WindowFrameBound, WriteOp,
9999
};
100100
use datafusion_physical_expr::aggregate::{
101101
AggregateFunctionExpr, LoweredAggregate, LoweredAggregateBuilder,
@@ -1903,22 +1903,8 @@ impl DefaultPhysicalPlanner {
19031903
planning_ctx,
19041904
)?,
19051905
);
1906-
let omitted_right = if join.join_constraint == JoinConstraint::Using {
1907-
join.on
1908-
.iter()
1909-
.map(|(_, right)| {
1910-
let column = right.get_as_join_column().ok_or_else(|| {
1911-
internal_datafusion_err!("ASOF USING key is not a column")
1912-
})?;
1913-
join.right.schema().index_of_column(column)
1914-
})
1915-
.collect::<Result<HashSet<_>>>()?
1916-
} else {
1917-
HashSet::new()
1918-
};
1919-
let right_output_indices = (0..join.right.schema().fields().len())
1920-
.filter(|index| !omitted_right.contains(index))
1921-
.collect();
1906+
let right_output_indices =
1907+
(0..join.right.schema().fields().len()).collect();
19221908
Arc::new(AsOfJoinExec::try_new(
19231909
physical_left,
19241910
physical_right,

datafusion/expr/src/logical_plan/builder.rs

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1840,38 +1840,10 @@ pub fn build_join_schema(
18401840

18411841
/// Creates the schema for a left-preserving ASOF join.
18421842
///
1843-
/// `ON` emits all left fields followed by nullable right fields. `USING` emits
1844-
/// each equality key once by omitting the corresponding right field.
1845-
pub fn build_asof_join_schema(
1846-
left: &DFSchema,
1847-
right: &DFSchema,
1848-
on: &[(Expr, Expr)],
1849-
join_constraint: JoinConstraint,
1850-
) -> Result<DFSchema> {
1851-
let omitted_right_indices = if join_constraint == JoinConstraint::Using {
1852-
on.iter()
1853-
.map(|(_, right_expr)| {
1854-
let column = right_expr.get_as_join_column().ok_or_else(|| {
1855-
plan_datafusion_err!("ASOF USING keys must be columns")
1856-
})?;
1857-
right.index_of_column(column)
1858-
})
1859-
.collect::<Result<HashSet<_>>>()?
1860-
} else {
1861-
HashSet::new()
1862-
};
1863-
1864-
let full_schema = build_join_schema(left, right, &JoinType::Left)?;
1865-
let left_len = left.fields().len();
1866-
let fields = full_schema
1867-
.iter()
1868-
.enumerate()
1869-
.filter(|(index, _)| {
1870-
*index < left_len || !omitted_right_indices.contains(&(*index - left_len))
1871-
})
1872-
.map(|(_, (qualifier, field))| (qualifier.cloned(), Arc::clone(field)))
1873-
.collect();
1874-
DFSchema::new_with_metadata(fields, full_schema.metadata().clone())?
1843+
/// Both `ON` and `USING` preserve all qualified input fields. SQL wildcard
1844+
/// expansion handles the unqualified `USING` key as a single column.
1845+
pub fn build_asof_join_schema(left: &DFSchema, right: &DFSchema) -> Result<DFSchema> {
1846+
build_join_schema(left, right, &JoinType::Left)?
18751847
.with_functional_dependencies(left.functional_dependencies().clone())
18761848
}
18771849

datafusion/expr/src/logical_plan/plan.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,6 @@ pub enum LogicalPlan {
240240
/// Join two logical plans on one or more join columns.
241241
/// This is used to implement SQL `JOIN`
242242
Join(Join),
243-
/// Match each left row with at most one ordered row from the right input.
244-
/// This is used to implement SQL `ASOF JOIN`.
245-
AsOfJoin(AsOfJoin),
246243
/// Repartitions the input based on a partitioning scheme. This is
247244
/// used to add parallelism and is sometimes referred to as an
248245
/// "exchange" operator in other systems
@@ -299,6 +296,9 @@ pub enum LogicalPlan {
299296
Unnest(Unnest),
300297
/// A variadic query (e.g. "Recursive CTEs")
301298
RecursiveQuery(RecursiveQuery),
299+
/// Match each left row with at most one ordered row from the right input.
300+
/// This is used to implement SQL `ASOF JOIN`.
301+
AsOfJoin(AsOfJoin),
302302
}
303303

304304
impl Default for LogicalPlan {
@@ -4455,8 +4455,7 @@ impl AsOfJoin {
44554455
return plan_err!("ASOF USING keys must be columns");
44564456
}
44574457

4458-
let schema =
4459-
build_asof_join_schema(left.schema(), right.schema(), &on, join_constraint)?;
4458+
let schema = build_asof_join_schema(left.schema(), right.schema())?;
44604459
Ok(Self {
44614460
left,
44624461
right,
@@ -6867,6 +6866,32 @@ mod tests {
68676866
Ok(())
68686867
}
68696868

6869+
#[test]
6870+
fn test_asof_using_preserves_qualified_keys() -> Result<()> {
6871+
let schema = Schema::new(vec![
6872+
Field::new("id", DataType::Int32, false),
6873+
Field::new("ts", DataType::Int64, false),
6874+
]);
6875+
let left = Arc::new(table_scan(Some("t1"), &schema, None)?.build()?);
6876+
let right = Arc::new(table_scan(Some("t2"), &schema, None)?.build()?);
6877+
let join = AsOfJoin::try_new(
6878+
left,
6879+
right,
6880+
vec![(col("t1.id"), col("t2.id"))],
6881+
AsOfMatch::new(col("t1.ts"), Operator::GtEq, col("t2.ts")),
6882+
JoinConstraint::Using,
6883+
)?;
6884+
6885+
assert_eq!(join.schema.fields().len(), 4);
6886+
assert_eq!(
6887+
join.schema
6888+
.index_of_column(&Column::from_qualified_name("t2.id"))?,
6889+
2
6890+
);
6891+
assert!(join.schema.field(2).is_nullable());
6892+
Ok(())
6893+
}
6894+
68706895
#[test]
68716896
fn test_join_try_new_schema_validation() -> Result<()> {
68726897
let left_schema = Schema::new(vec![

datafusion/optimizer/src/optimize_projections/mod.rs

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,8 @@ use crate::{OptimizerConfig, OptimizerRule};
2424
use std::sync::Arc;
2525

2626
use datafusion_common::{
27-
Column, DFSchema, HashMap, JoinConstraint, JoinType, Result,
28-
assert_eq_or_internal_err, get_required_group_by_exprs_indices,
29-
internal_datafusion_err, internal_err,
27+
Column, DFSchema, HashMap, JoinType, Result, assert_eq_or_internal_err,
28+
get_required_group_by_exprs_indices, internal_datafusion_err, internal_err,
3029
};
3130
use datafusion_expr::expr::Alias;
3231
use datafusion_expr::{
@@ -410,31 +409,13 @@ fn optimize_projections(
410409
}
411410
LogicalPlan::AsOfJoin(join) => {
412411
let left_len = join.left.schema().fields().len();
413-
let omitted_right = if join.join_constraint == JoinConstraint::Using {
414-
join.on
415-
.iter()
416-
.map(|(_, right)| {
417-
let column = right.get_as_join_column().ok_or_else(|| {
418-
internal_datafusion_err!("ASOF USING key is not a column")
419-
})?;
420-
join.right.schema().index_of_column(column)
421-
})
422-
.collect::<Result<std::collections::HashSet<_>>>()?
423-
} else {
424-
std::collections::HashSet::new()
425-
};
426-
let right_output_indices = (0..join.right.schema().fields().len())
427-
.filter(|index| !omitted_right.contains(index))
428-
.collect::<Vec<_>>();
429412
let mut left_required = Vec::new();
430413
let mut right_required = Vec::new();
431414
for index in indices.indices() {
432415
if *index < left_len {
433416
left_required.push(*index);
434-
} else if let Some(right_index) =
435-
right_output_indices.get(*index - left_len)
436-
{
437-
right_required.push(*right_index);
417+
} else {
418+
right_required.push(*index - left_len);
438419
}
439420
}
440421
let left_indices = RequiredIndices::new_from_indices(left_required)

datafusion/physical-plan/src/joins/asof_join.rs

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ use arrow::compute::{SortOptions, interleave};
6262
use arrow::datatypes::{Schema, SchemaRef};
6363
use datafusion_common::config::ConfigOptions;
6464
use datafusion_common::stats::Precision;
65+
use datafusion_common::utils::memory::RecordBatchMemoryCounter;
6566
use datafusion_common::utils::{
6667
compare_rows, get_row_at_idx, normalize_float_zero_scalar,
6768
};
@@ -93,7 +94,6 @@ use crate::metrics::{
9394
BaselineMetrics, Count, ExecutionPlanMetricsSet, Gauge, MetricBuilder,
9495
MetricCategory, MetricsSet, RecordOutput, Time,
9596
};
96-
use crate::spill::get_record_batch_memory_size;
9797
use crate::statistics::{ChildStats, StatisticsArgs};
9898
use crate::stream::RecordBatchStreamAdapter;
9999
use crate::{
@@ -752,9 +752,10 @@ async fn collect_right_input(
752752
metrics: AsOfJoinMetrics,
753753
) -> Result<BroadcastRightInput> {
754754
let schema = input.schema();
755+
let mut memory_counter = RecordBatchMemoryCounter::new();
755756
let batches = input
756757
.try_fold(Vec::new(), |mut batches, batch| {
757-
let batch_size = get_record_batch_memory_size(&batch);
758+
let batch_size = memory_counter.count_batch(&batch);
758759
futures::future::ready(reservation.try_grow(batch_size).map(|_| {
759760
metrics.build_mem_used.add(batch_size);
760761
metrics.build_input_batches.add(1);
@@ -1213,6 +1214,7 @@ mod tests {
12131214
};
12141215
use arrow::datatypes::{DataType, Field, Int8Type};
12151216
use datafusion_execution::config::SessionConfig;
1217+
use datafusion_execution::runtime_env::RuntimeEnvBuilder;
12161218
use datafusion_expr::ColumnarValue;
12171219
use datafusion_physical_expr_common::metrics::MetricValue;
12181220
use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
@@ -1544,6 +1546,90 @@ mod tests {
15441546
Ok(())
15451547
}
15461548

1549+
#[tokio::test]
1550+
async fn shared_right_buffers_are_reserved_once() -> Result<()> {
1551+
let left_schema = Arc::new(Schema::new(vec![
1552+
Field::new("key", DataType::Utf8, false),
1553+
Field::new("ts", DataType::Int64, false),
1554+
Field::new("id", DataType::Int32, false),
1555+
]));
1556+
let left = TestMemoryExec::try_new_exec(
1557+
&[vec![make_batch(
1558+
&left_schema,
1559+
vec![Some("A")],
1560+
vec![Some(4095)],
1561+
vec![0],
1562+
)?]],
1563+
Arc::clone(&left_schema),
1564+
None,
1565+
)?;
1566+
1567+
let right_schema = Arc::new(Schema::new(vec![
1568+
Field::new("key", DataType::Utf8, false),
1569+
Field::new("ts", DataType::Int64, false),
1570+
Field::new("price", DataType::Int32, false),
1571+
]));
1572+
let row_count = 4096;
1573+
let parent = make_batch(
1574+
&right_schema,
1575+
vec![Some("A"); row_count],
1576+
(0..row_count).map(|value| Some(value as i64)).collect(),
1577+
(0..row_count as i32).collect(),
1578+
)?;
1579+
let mut memory_counter = RecordBatchMemoryCounter::new();
1580+
let retained_size = memory_counter.count_batch(&parent);
1581+
let right_batches = (0..16)
1582+
.map(|index| parent.slice(index * 256, 256))
1583+
.collect();
1584+
let right = TestMemoryExec::try_new_exec(
1585+
&[right_batches],
1586+
Arc::clone(&right_schema),
1587+
None,
1588+
)?;
1589+
1590+
let exec = Arc::new(AsOfJoinExec::try_new(
1591+
left,
1592+
right,
1593+
vec![(
1594+
Arc::new(PhysicalColumn::new("key", 0)),
1595+
Arc::new(PhysicalColumn::new("key", 0)),
1596+
)],
1597+
AsOfMatchExpr::new(
1598+
Arc::new(PhysicalColumn::new("ts", 1)),
1599+
Operator::GtEq,
1600+
Arc::new(PhysicalColumn::new("ts", 1)),
1601+
),
1602+
vec![2],
1603+
)?);
1604+
let runtime = RuntimeEnvBuilder::new()
1605+
.with_memory_limit(retained_size, 1.0)
1606+
.build_arc()?;
1607+
let context = Arc::new(TaskContext::default().with_runtime(runtime));
1608+
1609+
let batches = collect(Arc::clone(&exec) as _, context).await?;
1610+
let prices = batches
1611+
.iter()
1612+
.flat_map(|batch| {
1613+
batch
1614+
.column(3)
1615+
.as_any()
1616+
.downcast_ref::<Int32Array>()
1617+
.unwrap()
1618+
.iter()
1619+
})
1620+
.collect::<Vec<_>>();
1621+
assert_eq!(prices, vec![Some(4095)]);
1622+
1623+
let metrics = exec.metrics().expect("ASOF metrics must be present");
1624+
assert_eq!(
1625+
metrics
1626+
.sum_by_name("build_mem_used")
1627+
.map(|value| value.as_usize()),
1628+
Some(retained_size)
1629+
);
1630+
Ok(())
1631+
}
1632+
15471633
#[tokio::test]
15481634
async fn preserves_dictionary_outputs_across_large_flush() -> Result<()> {
15491635
let dictionary_type =

0 commit comments

Comments
 (0)