Skip to content

Commit a9279fc

Browse files
committed
fix: preserve ordering through retained double negation
Treat an exact deterministic -(-x) key as determining x within its tie group, and make permutation analysis use the shared satisfied-key path. Carry the same relationship through projections that evaluate the retained expression, so grouped aggregates, windows, and aliases preserve the full suffix ordering without cancelling checked temporal negation. Signed-off-by: Amogh Ramesh <ramogh2404@gmail.com>
1 parent a88a120 commit a9279fc

2 files changed

Lines changed: 496 additions & 15 deletions

File tree

‎datafusion/core/tests/physical_optimizer/enforce_sorting.rs‎

Lines changed: 339 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717

1818
use std::sync::Arc;
19+
use std::time::Duration;
1920

2021
use crate::memory_limit::DummyStreamPartition;
2122
use crate::physical_optimizer::test_utils::{
@@ -31,9 +32,12 @@ use crate::physical_optimizer::test_utils::{
3132

3233
use arrow::compute::{SortOptions};
3334
use arrow::datatypes::{DataType, SchemaRef};
35+
use arrow::error::ArrowError;
3436
use datafusion_common::config::{ConfigOptions, CsvOptions};
3537
use datafusion_common::tree_node::{TreeNode, TransformedResult};
36-
use datafusion_common::{create_array, DataFusionError, NullEquality, Result, TableReference};
38+
use datafusion_common::{
39+
DataFusionError, NullEquality, Result, ScalarValue, TableReference, create_array,
40+
};
3741
use datafusion_datasource::file_scan_config::{FileScanConfig, FileScanConfigBuilder};
3842
use datafusion_datasource::source::DataSourceExec;
3943
use datafusion_expr_common::operator::Operator;
@@ -49,7 +53,12 @@ use datafusion_physical_plan::limit::{GlobalLimitExec, LocalLimitExec};
4953
use datafusion_physical_plan::repartition::RepartitionExec;
5054
use datafusion_physical_plan::sorts::sort_preserving_merge::SortPreservingMergeExec;
5155
use datafusion_physical_plan::sorts::sort::SortExec;
52-
use datafusion_physical_plan::{displayable, get_plan_string, ExecutionPlan, ExecutionPlanProperties};
56+
use datafusion_physical_plan::stream::RecordBatchStreamAdapter;
57+
use datafusion_physical_plan::streaming::PartitionStream;
58+
use datafusion_physical_plan::{
59+
ExecutionPlan, ExecutionPlanProperties, SendableRecordBatchStream, displayable,
60+
get_plan_string,
61+
};
5362
use datafusion::datasource::physical_plan::CsvSource;
5463
use datafusion::datasource::listing::PartitionedFile;
5564
use datafusion_physical_optimizer::enforce_sorting::{PlanWithCorrespondingCoalescePartitions, PlanWithCorrespondingSort, parallelize_sorts, ensure_sorting};
@@ -2944,6 +2953,334 @@ async fn test_sort_with_streaming_table() -> Result<()> {
29442953
Ok(())
29452954
}
29462955

2956+
#[derive(Debug)]
2957+
struct PendingOrderedPartition {
2958+
schema: SchemaRef,
2959+
batches: Vec<RecordBatch>,
2960+
}
2961+
2962+
impl PartitionStream for PendingOrderedPartition {
2963+
fn schema(&self) -> &SchemaRef {
2964+
&self.schema
2965+
}
2966+
2967+
fn execute(&self, _ctx: Arc<TaskContext>) -> SendableRecordBatchStream {
2968+
let stream = futures::stream::iter(
2969+
self.batches
2970+
.clone()
2971+
.into_iter()
2972+
.map(Ok::<_, DataFusionError>),
2973+
)
2974+
.chain(futures::stream::pending());
2975+
Box::pin(RecordBatchStreamAdapter::new(
2976+
Arc::clone(&self.schema),
2977+
stream,
2978+
))
2979+
}
2980+
}
2981+
2982+
fn pending_ordered_context(
2983+
value: &ScalarValue,
2984+
nullable: bool,
2985+
) -> Result<SessionContext> {
2986+
let schema: SchemaRef = Arc::new(Schema::new(vec![
2987+
Field::new("i", value.data_type(), nullable),
2988+
Field::new("j", DataType::Int64, false),
2989+
]));
2990+
let batches = [0_i64, 1_i64]
2991+
.into_iter()
2992+
.map(|j| -> Result<RecordBatch> {
2993+
Ok(RecordBatch::try_new(
2994+
Arc::clone(&schema),
2995+
vec![
2996+
value.to_array_of_size(1)?,
2997+
ScalarValue::Int64(Some(j)).to_array_of_size(1)?,
2998+
],
2999+
)?)
3000+
})
3001+
.collect::<Result<Vec<_>>>()?;
3002+
let partition: Arc<dyn PartitionStream> = Arc::new(PendingOrderedPartition {
3003+
schema: Arc::clone(&schema),
3004+
batches,
3005+
});
3006+
let provider = StreamingTable::try_new(Arc::clone(&schema), vec![partition])?
3007+
.with_sort_order(vec![
3008+
datafusion_expr::col("i").sort(true, false),
3009+
datafusion_expr::col("j").sort(true, false),
3010+
])
3011+
.with_infinite_table(true);
3012+
let ctx = SessionContext::new_with_config(
3013+
SessionConfig::new()
3014+
.with_target_partitions(1)
3015+
.with_batch_size(1),
3016+
);
3017+
ctx.register_table("s", Arc::new(provider))?;
3018+
Ok(ctx)
3019+
}
3020+
3021+
async fn assert_double_negation_group_streams(
3022+
case: &str,
3023+
value: ScalarValue,
3024+
nullable: bool,
3025+
) -> Result<()> {
3026+
let ctx = pending_ordered_context(&value, nullable)?;
3027+
let plan = ctx
3028+
.sql(
3029+
"SELECT -(-i) AS k, j, COUNT(*) AS n \
3030+
FROM s GROUP BY -(-i), j LIMIT 1",
3031+
)
3032+
.await?
3033+
.create_physical_plan()
3034+
.await?;
3035+
let plan_text = displayable(plan.as_ref()).indent(true).to_string();
3036+
let aggregate = plan_text
3037+
.lines()
3038+
.find(|line| line.contains("AggregateExec:"))
3039+
.expect("aggregate plan");
3040+
assert!(
3041+
aggregate.contains("ordering_mode=Sorted"),
3042+
"{case}: expected fully sorted aggregation:\n{plan_text}"
3043+
);
3044+
assert!(
3045+
!plan_text.contains("SortExec:"),
3046+
"{case}: existing input order should avoid a sort:\n{plan_text}"
3047+
);
3048+
3049+
let mut stream = plan.execute(0, ctx.task_ctx())?;
3050+
let output = tokio::time::timeout(Duration::from_secs(5), stream.next())
3051+
.await
3052+
.expect("sorted aggregation withheld a completed group")
3053+
.expect("stream ended before producing a group")?;
3054+
assert_eq!(output.num_rows(), 1, "{case}");
3055+
assert_eq!(
3056+
ScalarValue::try_from_array(output.column(0), 0)?,
3057+
value,
3058+
"{case}"
3059+
);
3060+
assert_eq!(
3061+
ScalarValue::try_from_array(output.column(1), 0)?,
3062+
ScalarValue::Int64(Some(0)),
3063+
"{case}"
3064+
);
3065+
assert_eq!(
3066+
ScalarValue::try_from_array(output.column(2), 0)?,
3067+
ScalarValue::Int64(Some(1)),
3068+
"{case}"
3069+
);
3070+
Ok(())
3071+
}
3072+
3073+
#[tokio::test]
3074+
async fn double_negation_group_by_streams_on_full_input_order() -> Result<()> {
3075+
for (case, value, nullable) in [
3076+
(
3077+
"timestamp",
3078+
ScalarValue::TimestampNanosecond(Some(1), None),
3079+
false,
3080+
),
3081+
(
3082+
"nullable timestamp",
3083+
ScalarValue::TimestampNanosecond(None, None),
3084+
true,
3085+
),
3086+
(
3087+
"year-month interval",
3088+
ScalarValue::IntervalYearMonth(Some(1)),
3089+
false,
3090+
),
3091+
] {
3092+
assert_double_negation_group_streams(case, value, nullable).await?;
3093+
}
3094+
Ok(())
3095+
}
3096+
3097+
#[tokio::test]
3098+
async fn double_negation_projection_preserves_suffix_ordering() -> Result<()> {
3099+
for (case, sql) in [
3100+
(
3101+
"projection",
3102+
"SELECT k, j FROM (SELECT -(-i) AS k, j FROM s) \
3103+
ORDER BY k, j LIMIT 1",
3104+
),
3105+
(
3106+
"aggregate output",
3107+
"SELECT k, j FROM ( \
3108+
SELECT -(-i) AS k, j, COUNT(*) AS n \
3109+
FROM s GROUP BY -(-i), j \
3110+
) ORDER BY k, j LIMIT 1",
3111+
),
3112+
] {
3113+
let ctx = pending_ordered_context(
3114+
&ScalarValue::TimestampNanosecond(Some(1), None),
3115+
false,
3116+
)?;
3117+
let plan = ctx.sql(sql).await?.create_physical_plan().await?;
3118+
let plan_text = displayable(plan.as_ref()).indent(true).to_string();
3119+
assert!(
3120+
!plan_text.contains("SortExec:"),
3121+
"{case}: projected ordering should include the suffix:\n{plan_text}"
3122+
);
3123+
3124+
let mut stream = plan.execute(0, ctx.task_ctx())?;
3125+
let output = tokio::time::timeout(Duration::from_secs(5), stream.next())
3126+
.await
3127+
.unwrap_or_else(|_| panic!("{case}: ordered projection did not stream"))
3128+
.expect("stream ended before producing a row")?;
3129+
assert_eq!(output.num_rows(), 1, "{case}");
3130+
assert_eq!(
3131+
ScalarValue::try_from_array(output.column(0), 0)?,
3132+
ScalarValue::TimestampNanosecond(Some(1), None),
3133+
"{case}"
3134+
);
3135+
assert_eq!(
3136+
ScalarValue::try_from_array(output.column(1), 0)?,
3137+
ScalarValue::Int64(Some(0)),
3138+
"{case}"
3139+
);
3140+
}
3141+
Ok(())
3142+
}
3143+
3144+
#[tokio::test]
3145+
async fn double_negation_group_by_preserves_timestamp_overflow() -> Result<()> {
3146+
let ctx = pending_ordered_context(
3147+
&ScalarValue::TimestampNanosecond(Some(i64::MIN), None),
3148+
false,
3149+
)?;
3150+
let plan = ctx
3151+
.sql(
3152+
"SELECT -(-i) AS k, j, COUNT(*) AS n \
3153+
FROM s GROUP BY -(-i), j LIMIT 1",
3154+
)
3155+
.await?
3156+
.create_physical_plan()
3157+
.await?;
3158+
3159+
let mut stream = plan.execute(0, ctx.task_ctx())?;
3160+
let err = tokio::time::timeout(Duration::from_secs(5), stream.next())
3161+
.await
3162+
.expect("timestamp overflow was not reported")
3163+
.expect("stream ended before reporting timestamp overflow")
3164+
.expect_err("double negation must still evaluate its checked inner negation");
3165+
assert!(
3166+
matches!(
3167+
err.find_root(),
3168+
DataFusionError::ArrowError(error, _)
3169+
if matches!(error.as_ref(), ArrowError::ArithmeticOverflow(_))
3170+
),
3171+
"unexpected error: {err}"
3172+
);
3173+
Ok(())
3174+
}
3175+
3176+
async fn assert_double_negation_lead_streams(
3177+
case: &str,
3178+
value: ScalarValue,
3179+
nullable: bool,
3180+
) -> Result<()> {
3181+
let ctx = pending_ordered_context(&value, nullable)?;
3182+
let plan = ctx
3183+
.sql(
3184+
"SELECT i, j, LEAD(j, 1) OVER (PARTITION BY -(-i), j) AS n \
3185+
FROM s LIMIT 1",
3186+
)
3187+
.await?
3188+
.create_physical_plan()
3189+
.await?;
3190+
let plan_text = displayable(plan.as_ref()).indent(true).to_string();
3191+
let window = plan_text
3192+
.lines()
3193+
.find(|line| line.contains("BoundedWindowAggExec:"))
3194+
.expect("bounded window plan");
3195+
assert!(
3196+
window.contains("mode=[Sorted]"),
3197+
"{case}: expected fully sorted window partitions:\n{plan_text}"
3198+
);
3199+
assert!(
3200+
!plan_text.contains("SortExec:"),
3201+
"{case}: existing input order should avoid a sort:\n{plan_text}"
3202+
);
3203+
3204+
let mut stream = plan.execute(0, ctx.task_ctx())?;
3205+
let output = tokio::time::timeout(Duration::from_secs(5), stream.next())
3206+
.await
3207+
.unwrap_or_else(|_| panic!("{case}: LEAD withheld a completed partition"))
3208+
.expect("stream ended before producing a row")?;
3209+
assert_eq!(output.num_rows(), 1, "{case}");
3210+
assert_eq!(
3211+
ScalarValue::try_from_array(output.column(0), 0)?,
3212+
value,
3213+
"{case}"
3214+
);
3215+
assert_eq!(
3216+
ScalarValue::try_from_array(output.column(1), 0)?,
3217+
ScalarValue::Int64(Some(0)),
3218+
"{case}"
3219+
);
3220+
assert_eq!(
3221+
ScalarValue::try_from_array(output.column(2), 0)?,
3222+
ScalarValue::Int64(None),
3223+
"{case}"
3224+
);
3225+
Ok(())
3226+
}
3227+
3228+
#[tokio::test]
3229+
async fn double_negation_lead_partition_streams_on_full_input_order() -> Result<()> {
3230+
for (case, value, nullable) in [
3231+
(
3232+
"timestamp",
3233+
ScalarValue::TimestampNanosecond(Some(1), None),
3234+
false,
3235+
),
3236+
(
3237+
"nullable timestamp",
3238+
ScalarValue::TimestampNanosecond(None, None),
3239+
true,
3240+
),
3241+
(
3242+
"year-month interval",
3243+
ScalarValue::IntervalYearMonth(Some(1)),
3244+
false,
3245+
),
3246+
] {
3247+
assert_double_negation_lead_streams(case, value, nullable).await?;
3248+
}
3249+
Ok(())
3250+
}
3251+
3252+
#[tokio::test]
3253+
async fn double_negation_window_partition_preserves_timestamp_overflow() -> Result<()> {
3254+
let ctx = pending_ordered_context(
3255+
&ScalarValue::TimestampNanosecond(Some(i64::MIN), None),
3256+
false,
3257+
)?;
3258+
let plan = ctx
3259+
.sql(
3260+
"SELECT LEAD(j, 1) OVER (PARTITION BY -(-i), j) AS n \
3261+
FROM s LIMIT 1",
3262+
)
3263+
.await?
3264+
.create_physical_plan()
3265+
.await?;
3266+
3267+
let mut stream = plan.execute(0, ctx.task_ctx())?;
3268+
let err = tokio::time::timeout(Duration::from_secs(5), stream.next())
3269+
.await
3270+
.expect("timestamp overflow was not reported")
3271+
.expect("stream ended before reporting timestamp overflow")
3272+
.expect_err("the original PARTITION BY expression must still be evaluated");
3273+
assert!(
3274+
matches!(
3275+
err.find_root(),
3276+
DataFusionError::ArrowError(error, _)
3277+
if matches!(error.as_ref(), ArrowError::ArithmeticOverflow(_))
3278+
),
3279+
"unexpected error: {err}"
3280+
);
3281+
Ok(())
3282+
}
3283+
29473284
/// Regression: `parallelize_sorts` must not relocate a per-partition `SortExec`
29483285
/// below an order-preserving `ProjectionExec` that *reorders* columns without
29493286
/// remapping the sort-key column indices.

0 commit comments

Comments
 (0)