@@ -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}\n parameters={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]
364638async fn test_version_function ( ) {
365639 let expected_version = format ! (
0 commit comments