Skip to content

Commit c3fa51d

Browse files
committed
fix: infer LIMIT and OFFSET parameter types
Preserve known parameter fields before applying row-count defaults and keep incomplete PREPARE signatures deferred. Fixes #15978 Signed-off-by: 1fanwang <1fannnw@gmail.com>
1 parent 62dc7ac commit c3fa51d

3 files changed

Lines changed: 300 additions & 10 deletions

File tree

‎datafusion/core/tests/sql/select.rs‎

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,280 @@ async fn test_query_parameters_with_metadata() -> Result<()> {
360360
Ok(())
361361
}
362362

363+
#[tokio::test]
364+
async fn test_limit_offset_parameters() -> Result<()> {
365+
let ctx = SessionContext::new();
366+
367+
for (sql, values, expected_value) in [
368+
("SELECT 20 AS value LIMIT $1", vec![1_i64], 20_i64),
369+
(
370+
"SELECT value FROM (VALUES (10), (20)) AS t(value) ORDER BY value OFFSET $1",
371+
vec![1],
372+
20,
373+
),
374+
(
375+
"SELECT value FROM (VALUES (10), (20), (30)) AS t(value) \
376+
ORDER BY value LIMIT $1 OFFSET $2",
377+
vec![1, 1],
378+
20,
379+
),
380+
("SELECT $1 AS value LIMIT $1", vec![1], 1),
381+
(
382+
"SELECT $1 AS value FROM (VALUES (0), (0)) AS t(dummy) OFFSET $1",
383+
vec![1],
384+
1,
385+
),
386+
] {
387+
let df = ctx.sql(sql).await?;
388+
let parameter_types = df.logical_plan().get_parameter_types()?;
389+
let expected_types: HashMap<String, Option<DataType>> = (1..=values.len())
390+
.map(|index| (format!("${index}"), Some(DataType::Int64)))
391+
.collect();
392+
let results = df
393+
.with_param_values(
394+
values
395+
.into_iter()
396+
.map(ScalarValue::from)
397+
.collect::<Vec<_>>(),
398+
)?
399+
.collect()
400+
.await?;
401+
402+
println!(
403+
"{sql}\nparameters={parameter_types:?}\n{}",
404+
batches_to_sort_string(&results)
405+
);
406+
assert_eq!(results.iter().map(RecordBatch::num_rows).sum::<usize>(), 1);
407+
assert_eq!(
408+
ScalarValue::try_from_array(results[0].column(0), 0)?,
409+
ScalarValue::Int64(Some(expected_value))
410+
);
411+
assert_eq!(parameter_types, expected_types, "{sql}");
412+
}
413+
414+
Ok(())
415+
}
416+
417+
#[tokio::test]
418+
async fn test_limit_offset_parameters_named() -> Result<()> {
419+
let ctx = SessionContext::new();
420+
let df = ctx
421+
.sql(
422+
"SELECT value FROM (VALUES (10), (20), (30)) AS t(value) \
423+
ORDER BY value LIMIT $rows OFFSET $skip",
424+
)
425+
.await?;
426+
assert_eq!(
427+
df.logical_plan().get_parameter_types()?,
428+
HashMap::from([
429+
("$rows".to_string(), Some(DataType::Int64)),
430+
("$skip".to_string(), Some(DataType::Int64)),
431+
])
432+
);
433+
let results = df
434+
.with_param_values(vec![
435+
("rows", ScalarValue::Int64(Some(1))),
436+
("skip", ScalarValue::Int64(Some(1))),
437+
])?
438+
.collect()
439+
.await?;
440+
datafusion::assert_batches_eq!(
441+
[
442+
"+-------+",
443+
"| value |",
444+
"+-------+",
445+
"| 20 |",
446+
"+-------+"
447+
],
448+
&results
449+
);
450+
Ok(())
451+
}
452+
453+
#[tokio::test]
454+
async fn test_limit_offset_parameters_keep_field_metadata() -> Result<()> {
455+
let ctx = SessionContext::new();
456+
let metadata = HashMap::from([("some_key".to_string(), "some_value".to_string())]);
457+
let schema = Arc::new(Schema::new(vec![
458+
Field::new("value", DataType::Int32, false).with_metadata(metadata.clone()),
459+
]));
460+
ctx.register_batch(
461+
"t",
462+
RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(vec![1, 1]))])?,
463+
)?;
464+
465+
for clause in ["LIMIT", "OFFSET"] {
466+
let sql = format!("SELECT $1 AS value FROM t WHERE value = $1 {clause} $1");
467+
let df = ctx.sql(&sql).await?;
468+
let fields = df.logical_plan().get_parameter_fields()?;
469+
let field = fields["$1"].as_ref().unwrap();
470+
assert_eq!(field.data_type(), &DataType::Int32);
471+
assert_eq!(field.metadata(), &metadata);
472+
473+
let results = df
474+
.with_param_values(ParamValues::List(vec![ScalarAndMetadata::new(
475+
ScalarValue::Int32(Some(1)),
476+
Some(metadata.clone().into()),
477+
)]))?
478+
.collect()
479+
.await?;
480+
datafusion::assert_batches_eq!(
481+
[
482+
"+-------+",
483+
"| value |",
484+
"+-------+",
485+
"| 1 |",
486+
"+-------+"
487+
],
488+
&results
489+
);
490+
}
491+
492+
Ok(())
493+
}
494+
495+
#[tokio::test]
496+
async fn test_limit_offset_parameters_leave_cast_inputs_unresolved() -> Result<()> {
497+
let ctx = SessionContext::new();
498+
499+
for sql in [
500+
"SELECT $1 AS value",
501+
"SELECT $1 AS value LIMIT CAST($1 AS INT)",
502+
] {
503+
let df = ctx.sql(sql).await?;
504+
assert_eq!(
505+
df.logical_plan().get_parameter_types()?,
506+
HashMap::from([("$1".to_string(), None)])
507+
);
508+
let results = df
509+
.with_param_values(vec![ScalarValue::from("1")])?
510+
.collect()
511+
.await?;
512+
datafusion::assert_batches_eq!(
513+
[
514+
"+-------+",
515+
"| value |",
516+
"+-------+",
517+
"| 1 |",
518+
"+-------+"
519+
],
520+
&results
521+
);
522+
}
523+
524+
Ok(())
525+
}
526+
527+
#[tokio::test]
528+
async fn test_limit_offset_parameters_keep_inferred_type() -> Result<()> {
529+
let ctx = SessionContext::new();
530+
let sql = "SELECT $1 + CAST(1 AS INT) AS value \
531+
FROM (SELECT $1) AS t LIMIT $1";
532+
let df = ctx.sql(sql).await?;
533+
let parameter_types = df.logical_plan().get_parameter_types()?;
534+
let results = df
535+
.with_param_values(vec![ScalarValue::Int32(Some(1))])?
536+
.collect()
537+
.await?;
538+
539+
assert_eq!(
540+
parameter_types,
541+
HashMap::from([("$1".to_string(), Some(DataType::Int32))])
542+
);
543+
datafusion::assert_batches_eq!(
544+
[
545+
"+-------+",
546+
"| value |",
547+
"+-------+",
548+
"| 2 |",
549+
"+-------+"
550+
],
551+
&results
552+
);
553+
Ok(())
554+
}
555+
556+
#[tokio::test]
557+
async fn test_limit_offset_parameters_reject_type_conflicts() -> Result<()> {
558+
let ctx = SessionContext::new();
559+
let df = ctx
560+
.sql(
561+
"SELECT $1 + CAST(1 AS INT) AS a, \
562+
$1 + CAST(1 AS DOUBLE) AS b LIMIT $1",
563+
)
564+
.await?;
565+
let error = df
566+
.logical_plan()
567+
.get_parameter_fields()
568+
.expect_err("conflicting parameter types must fail");
569+
assert_contains!(error.to_string(), "Conflicting types for id $1");
570+
Ok(())
571+
}
572+
573+
#[tokio::test]
574+
async fn test_limit_offset_parameters_prepare_execute() -> Result<()> {
575+
let ctx = SessionContext::new();
576+
let sql = "PREPARE page AS SELECT value \
577+
FROM (VALUES (10), (20), (30)) AS t(value) \
578+
ORDER BY value LIMIT $1 OFFSET $2";
579+
let plan = ctx.state().create_logical_plan(sql).await?;
580+
ctx.sql(sql).await?.collect().await?;
581+
let results = ctx.sql("EXECUTE page(1, 1)").await?.collect().await?;
582+
583+
println!(
584+
"{}\n{}",
585+
plan.display_indent(),
586+
batches_to_sort_string(&results)
587+
);
588+
assert_snapshot!(batches_to_sort_string(&results), @r"
589+
+-------+
590+
| value |
591+
+-------+
592+
| 20 |
593+
+-------+
594+
");
595+
assert_eq!(
596+
plan.get_parameter_types()?,
597+
HashMap::from([
598+
("$1".to_string(), Some(DataType::Int64)),
599+
("$2".to_string(), Some(DataType::Int64)),
600+
])
601+
);
602+
603+
Ok(())
604+
}
605+
606+
#[tokio::test]
607+
async fn test_limit_offset_parameters_prepare_mixed_types() -> Result<()> {
608+
let ctx = SessionContext::new();
609+
610+
for sql in [
611+
"PREPARE untyped_page AS SELECT $1 AS value LIMIT $2",
612+
"PREPARE untyped_page AS SELECT $1 AS value \
613+
FROM (VALUES (0), (0)) AS t(dummy) OFFSET $2",
614+
] {
615+
ctx.sql(sql).await?.collect().await?;
616+
let results = ctx
617+
.sql("EXECUTE untyped_page('row', 1)")
618+
.await?
619+
.collect()
620+
.await?;
621+
datafusion::assert_batches_eq!(
622+
[
623+
"+-------+",
624+
"| value |",
625+
"+-------+",
626+
"| row |",
627+
"+-------+"
628+
],
629+
&results
630+
);
631+
ctx.sql("DEALLOCATE untyped_page").await?.collect().await?;
632+
}
633+
634+
Ok(())
635+
}
636+
363637
#[tokio::test]
364638
async fn test_version_function() {
365639
let expected_version = format!(

‎datafusion/expr/src/logical_plan/plan.rs‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1834,14 +1834,21 @@ impl LogicalPlan {
18341834
.collect())
18351835
}
18361836

1837-
/// Walk the logical plan, find any `Placeholder` tokens, and return a map of their IDs and FieldRefs
1837+
/// Walk the logical plan, find any `Placeholder` tokens, and return a map of their IDs and FieldRefs.
1838+
/// Bare `LIMIT`/`OFFSET` parameters default to `Int64` if no occurrence provides a type.
18381839
pub fn get_parameter_fields(
18391840
&self,
18401841
) -> Result<HashMap<String, Option<FieldRef>>, DataFusionError> {
18411842
let mut param_types: HashMap<String, Option<FieldRef>> = HashMap::new();
1843+
let mut row_count_parameters: HashSet<String> = HashSet::new();
18421844

18431845
self.apply_with_subqueries(|plan| {
18441846
plan.apply_expressions(|expr| {
1847+
if matches!(plan, LogicalPlan::Limit(_))
1848+
&& let Expr::Placeholder(Placeholder { id, field: None }) = expr
1849+
{
1850+
row_count_parameters.insert(id.clone());
1851+
}
18451852
expr.apply(|expr| {
18461853
if let Expr::Placeholder(Placeholder { id, field }) = expr {
18471854
let prev = param_types.get(id);
@@ -1858,15 +1865,22 @@ impl LogicalPlan {
18581865
param_types.insert(id.clone(), Some(Arc::clone(field)));
18591866
}
18601867
_ => {
1861-
param_types.insert(id.clone(), None);
1868+
param_types.entry(id.clone()).or_insert(None);
18621869
}
18631870
}
18641871
}
18651872
Ok(TreeNodeRecursion::Continue)
18661873
})
18671874
})
1868-
})
1869-
.map(|_| param_types)
1875+
})?;
1876+
1877+
for id in row_count_parameters {
1878+
param_types
1879+
.entry(id)
1880+
.or_default()
1881+
.get_or_insert_with(|| Arc::new(Field::new("", DataType::Int64, true)));
1882+
}
1883+
Ok(param_types)
18701884
}
18711885

18721886
// ------------

‎datafusion/sql/src/statement.rs‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -844,16 +844,18 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
844844

845845
if fields.is_empty() {
846846
let map_types = plan.get_parameter_fields()?;
847-
let param_types: Vec<_> = (1..=map_types.len())
848-
.filter_map(|i| {
847+
let param_types: Option<Vec<FieldRef>> = (1..=map_types.len())
848+
.map(|i| {
849849
let key = format!("${i}");
850850
map_types.get(&key).and_then(|opt| opt.clone())
851851
})
852852
.collect();
853-
fields.extend(param_types.iter().cloned());
854-
planner_context.with_prepare_param_data_types(
855-
param_types.into_iter().map(Some).collect(),
856-
);
853+
if let Some(param_types) = param_types {
854+
fields.extend(param_types.iter().cloned());
855+
planner_context.with_prepare_param_data_types(
856+
param_types.into_iter().map(Some).collect(),
857+
);
858+
}
857859
}
858860

859861
Ok(LogicalPlan::Statement(PlanStatement::Prepare(Prepare {

0 commit comments

Comments
 (0)