11use super :: csv_import:: { extract_csv_copy_statement, CsvImport } ;
22use super :: sql_pseudofunctions:: { func_call_to_param, StmtParam } ;
33use crate :: file_cache:: AsyncFromStrWithState ;
4- use crate :: utils:: add_value_to_map;
54use crate :: { AppState , Database } ;
65use anyhow:: Context ;
76use async_trait:: async_trait;
@@ -59,7 +58,7 @@ pub(super) struct StmtWithParams {
5958#[ derive( Debug ) ]
6059pub ( super ) enum ParsedStatement {
6160 StmtWithParams ( StmtWithParams ) ,
62- StaticSimpleSelect ( serde_json :: Map < String , serde_json :: Value > ) ,
61+ StaticSimpleSelect ( Vec < ( String , SimpleSelectValue ) > ) ,
6362 SetVariable {
6463 variable : StmtParam ,
6564 value : StmtWithParams ,
@@ -68,6 +67,12 @@ pub(super) enum ParsedStatement {
6867 Error ( anyhow:: Error ) ,
6968}
7069
70+ #[ derive( Debug , PartialEq ) ]
71+ pub ( super ) enum SimpleSelectValue {
72+ Static ( serde_json:: Value ) ,
73+ Dynamic ( StmtParam ) ,
74+ }
75+
7176fn parse_sql < ' a > (
7277 dialect : & ' a dyn Dialect ,
7378 sql : & ' a str ,
@@ -93,11 +98,6 @@ fn parse_single_statement(parser: &mut Parser<'_>, db_kind: AnyKind) -> Option<P
9398 } ;
9499 log:: debug!( "Parsed statement: {stmt}" ) ;
95100 while parser. consume_token ( & SemiColon ) { }
96- if let Some ( static_statement) = extract_static_simple_select ( & stmt) {
97- log:: debug!( "Optimised a static simple select to avoid a trivial database query: {stmt} optimized to {static_statement:?}" ) ;
98- return Some ( ParsedStatement :: StaticSimpleSelect ( static_statement) ) ;
99- }
100-
101101 let params = ParameterExtractor :: extract_parameters ( & mut stmt, db_kind) ;
102102 if let Some ( ( variable, query) ) = extract_set_variable ( & mut stmt) {
103103 return Some ( ParsedStatement :: SetVariable {
@@ -108,6 +108,10 @@ fn parse_single_statement(parser: &mut Parser<'_>, db_kind: AnyKind) -> Option<P
108108 if let Some ( csv_import) = extract_csv_copy_statement ( & mut stmt) {
109109 return Some ( ParsedStatement :: CsvImport ( csv_import) ) ;
110110 }
111+ if let Some ( static_statement) = extract_static_simple_select ( & stmt, & params) {
112+ log:: debug!( "Optimised a static simple select to avoid a trivial database query: {stmt} optimized to {static_statement:?}" ) ;
113+ return Some ( ParsedStatement :: StaticSimpleSelect ( static_statement) ) ;
114+ }
111115 let query = stmt. to_string ( ) ;
112116 log:: debug!( "Final transformed statement: {stmt}" ) ;
113117 Some ( ParsedStatement :: StmtWithParams ( StmtWithParams {
@@ -174,7 +178,8 @@ fn map_param(mut name: String) -> StmtParam {
174178
175179fn extract_static_simple_select (
176180 stmt : & Statement ,
177- ) -> Option < serde_json:: Map < String , serde_json:: Value > > {
181+ params : & [ StmtParam ] ,
182+ ) -> Option < Vec < ( String , SimpleSelectValue ) > > {
178183 let set_expr = match stmt {
179184 Statement :: Query ( q)
180185 if q. limit . is_none ( )
@@ -208,22 +213,52 @@ fn extract_static_simple_select(
208213 }
209214 _ => return None ,
210215 } ;
211- let mut map = serde_json:: Map :: with_capacity ( select_items. len ( ) ) ;
216+ let mut items = Vec :: with_capacity ( select_items. len ( ) ) ;
217+ let mut params_iter = params. iter ( ) . cloned ( ) ;
212218 for select_item in select_items {
213219 let sqlparser:: ast:: SelectItem :: ExprWithAlias { expr, alias } = select_item else {
214220 return None ;
215221 } ;
222+ use serde_json:: Value :: * ;
223+ use SimpleSelectValue :: * ;
216224 let value = match expr {
217- Expr :: Value ( Value :: Boolean ( b) ) => serde_json:: Value :: Bool ( * b) ,
218- Expr :: Value ( Value :: Number ( n, _) ) => serde_json:: Value :: Number ( n. parse ( ) . ok ( ) ?) ,
219- Expr :: Value ( Value :: SingleQuotedString ( s) ) => serde_json:: Value :: String ( s. clone ( ) ) ,
220- Expr :: Value ( Value :: Null ) => serde_json:: Value :: Null ,
221- _ => return None ,
225+ Expr :: Value ( Value :: Boolean ( b) ) => Static ( Bool ( * b) ) ,
226+ Expr :: Value ( Value :: Number ( n, _) ) => Static ( Number ( n. parse ( ) . ok ( ) ?) ) ,
227+ Expr :: Value ( Value :: SingleQuotedString ( s) ) => Static ( String ( s. clone ( ) ) ) ,
228+ Expr :: Value ( Value :: Null ) => Static ( Null ) ,
229+ e if is_simple_select_placeholder ( e) => {
230+ if let Some ( p) = params_iter. next ( ) {
231+ Dynamic ( p)
232+ } else {
233+ log:: error!( "Parameter not extracted for placehorder: {expr:?}" ) ;
234+ return None ;
235+ }
236+ }
237+ other => {
238+ log:: trace!( "Cancelling simple select optimization because of expr: {other:?}" ) ;
239+ return None ;
240+ }
222241 } ;
223242 let key = alias. value . clone ( ) ;
224- map = add_value_to_map ( map, ( key, value) ) ;
243+ items. push ( ( key, value) ) ;
244+ }
245+ if let Some ( p) = params_iter. next ( ) {
246+ log:: error!( "static select extraction failed because of extraneous parameter: {p:?}" ) ;
247+ return None ;
248+ }
249+ Some ( items)
250+ }
251+
252+ fn is_simple_select_placeholder ( e : & Expr ) -> bool {
253+ match e {
254+ Expr :: Value ( Value :: Placeholder ( _) ) => true ,
255+ Expr :: Cast {
256+ expr,
257+ data_type : DataType :: Text | DataType :: Varchar ( _) | DataType :: Char ( _) ,
258+ format : None ,
259+ } if is_simple_select_placeholder ( expr) => true ,
260+ _ => false ,
225261 }
226- Some ( map)
227262}
228263
229264fn extract_set_variable ( stmt : & mut Statement ) -> Option < ( StmtParam , String ) > {
@@ -705,59 +740,109 @@ mod test {
705740
706741 #[ test]
707742 fn test_static_extract ( ) {
743+ use SimpleSelectValue :: Static ;
744+
708745 assert_eq ! (
709- extract_static_simple_select( & parse_postgres_stmt(
710- "select 'hello' as hello, 42 as answer, null as nothing, 'world' as hello"
711- ) ) ,
712- Some (
713- serde_json:: json!( {
714- "hello" : [ "hello" , "world" ] ,
715- "answer" : 42 ,
716- "nothing" : ( ) ,
717- } )
718- . as_object( )
719- . unwrap( )
720- . clone( )
721- )
746+ extract_static_simple_select(
747+ & parse_postgres_stmt(
748+ "select 'hello' as hello, 42 as answer, null as nothing, 'world' as hello"
749+ ) ,
750+ & [ ]
751+ ) ,
752+ Some ( vec![
753+ ( "hello" . into( ) , Static ( "hello" . into( ) ) ) ,
754+ ( "answer" . into( ) , Static ( 42 . into( ) ) ) ,
755+ ( "nothing" . into( ) , Static ( ( ) . into( ) ) ) ,
756+ ( "hello" . into( ) , Static ( "world" . into( ) ) ) ,
757+ ] )
758+ ) ;
759+ }
760+
761+ #[ test]
762+ fn test_simple_select_with_sqlpage_pseudofunction ( ) {
763+ let sql = "select 'text' as component, $x as contents, $y as title" ;
764+ let dialects: & [ & dyn Dialect ] = & [
765+ & PostgreSqlDialect { } ,
766+ & SQLiteDialect { } ,
767+ & MySqlDialect { } ,
768+ & MsSqlDialect { } ,
769+ ] ;
770+ for & dialect in dialects {
771+ let parsed: Vec < ParsedStatement > = parse_sql ( dialect, sql) . unwrap ( ) . collect ( ) ;
772+ use SimpleSelectValue :: { Dynamic , Static } ;
773+ use StmtParam :: GetOrPost ;
774+ match & parsed[ ..] {
775+ [ ParsedStatement :: StaticSimpleSelect ( q) ] => assert_eq ! (
776+ q,
777+ & [
778+ ( "component" . into( ) , Static ( "text" . into( ) ) ) ,
779+ ( "contents" . into( ) , Dynamic ( GetOrPost ( "x" . into( ) ) ) ) ,
780+ ( "title" . into( ) , Dynamic ( GetOrPost ( "y" . into( ) ) ) ) ,
781+ ]
782+ ) ,
783+ other => panic ! ( "failed to extract simple select in {dialect:?}: {other:?}" ) ,
784+ }
785+ }
786+ }
787+
788+ #[ test]
789+ fn test_simple_select_only_extraction ( ) {
790+ use SimpleSelectValue :: { Dynamic , Static } ;
791+ use StmtParam :: Cookie ;
792+ assert_eq ! (
793+ extract_static_simple_select(
794+ & parse_postgres_stmt( "select 'text' as component, $1 as contents" ) ,
795+ & [ Cookie ( "cook" . into( ) ) ]
796+ ) ,
797+ Some ( vec![
798+ ( "component" . into( ) , Static ( "text" . into( ) ) ) ,
799+ ( "contents" . into( ) , Dynamic ( Cookie ( "cook" . into( ) ) ) ) ,
800+ ] )
722801 ) ;
723802 }
724803
725804 #[ test]
726805 fn test_static_extract_doesnt_match ( ) {
727806 assert_eq ! (
728- extract_static_simple_select( & parse_postgres_stmt(
729- "select 'hello' as hello, 42 as answer limit 0"
730- ) ) ,
807+ extract_static_simple_select(
808+ & parse_postgres_stmt( "select 'hello' as hello, 42 as answer limit 0" ) ,
809+ & [ ]
810+ ) ,
731811 None
732812 ) ;
733813 assert_eq ! (
734- extract_static_simple_select( & parse_postgres_stmt(
735- "select 'hello' as hello, 42 as answer order by 1"
736- ) ) ,
814+ extract_static_simple_select(
815+ & parse_postgres_stmt( "select 'hello' as hello, 42 as answer order by 1" ) ,
816+ & [ ]
817+ ) ,
737818 None
738819 ) ;
739820 assert_eq ! (
740- extract_static_simple_select( & parse_postgres_stmt(
741- "select 'hello' as hello, 42 as answer offset 1"
742- ) ) ,
821+ extract_static_simple_select(
822+ & parse_postgres_stmt( "select 'hello' as hello, 42 as answer offset 1" ) ,
823+ & [ ]
824+ ) ,
743825 None
744826 ) ;
745827 assert_eq ! (
746- extract_static_simple_select( & parse_postgres_stmt(
747- "select 'hello' as hello, 42 as answer where 1 = 0"
748- ) ) ,
828+ extract_static_simple_select(
829+ & parse_postgres_stmt( "select 'hello' as hello, 42 as answer where 1 = 0" ) ,
830+ & [ ]
831+ ) ,
749832 None
750833 ) ;
751834 assert_eq ! (
752- extract_static_simple_select( & parse_postgres_stmt(
753- "select 'hello' as hello, 42 as answer FROM t"
754- ) ) ,
835+ extract_static_simple_select(
836+ & parse_postgres_stmt( "select 'hello' as hello, 42 as answer FROM t" ) ,
837+ & [ ]
838+ ) ,
755839 None
756840 ) ;
757841 assert_eq ! (
758- extract_static_simple_select( & parse_postgres_stmt(
759- "select x'CAFEBABE' as hello, 42 as answer"
760- ) ) ,
842+ extract_static_simple_select(
843+ & parse_postgres_stmt( "select x'CAFEBABE' as hello, 42 as answer" ) ,
844+ & [ ]
845+ ) ,
761846 None
762847 ) ;
763848 }
0 commit comments