Skip to content

Commit 39a11aa

Browse files
committed
improve the 'simple select' optimization to take pseudo-functions into account.
1 parent fe268f4 commit 39a11aa

File tree

6 files changed

+203
-81
lines changed

6 files changed

+203
-81
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
- [Updated sqlparser](https://github.com/sqlparser-rs/sqlparser-rs/blob/main/CHANGELOG.md)
99
- adds support for named windows in window functions
1010
- New icons with tabler icons 3.2: https://tabler.io/icons/changelog
11+
- Optimize queries like `select 'xxx' as component, sqlpage.some_function(...) as parameter`
12+
to avoid making an unneeded database query.
13+
This is especially important for the performance of `sqlpage.run_sql` and the `dynamic` component.
1114

1215
## 0.20.2 (2024-04-01)
1316

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,20 @@
1-
select
2-
'chart' as component,
3-
'Quarterly Revenue' as title,
4-
'area' as type,
5-
'indigo' as color,
6-
5 as marker,
7-
TRUE as time;
8-
select
9-
'2022-01-01T00:00:00Z' as x,
10-
15 as y;
11-
select
12-
'2022-04-01T00:00:00Z' as x,
13-
46 as y;
14-
select
15-
'2022-07-01T00:00:00Z' as x,
16-
23 as y;
17-
select
18-
'2022-10-01T00:00:00Z' as x,
19-
70 as y;
20-
select
21-
'2023-01-01T00:00:00Z' as x,
22-
35 as y;
23-
select
24-
'2023-04-01T00:00:00Z' as x,
25-
106 as y;
26-
select
27-
'2023-07-01T00:00:00Z' as x,
28-
53 as y;
29-
1+
set n=coalesce($n, 1);
302

3+
select
4+
'chart' as component,
5+
'Syracuse Sequence' as title,
6+
coalesce($type, 'area') as type,
7+
coalesce($color, 'indigo') as color,
8+
5 as marker;
9+
with recursive seq(x, y) as (
10+
select 0, CAST($n as integer)
11+
union all
12+
select x+1, case
13+
when y % 2 = 0 then y/2
14+
else 3*y+1
15+
end
16+
from seq
17+
where x<10
18+
)
19+
select x, y from seq;
3120

examples/official-site/sqlpage/migrations/01_documentation.sql

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,22 @@ The `color` property sets the color of each series separately, in order.
544544
{ "series": "PostgreSQL", "x": "2000", "y": 48},{ "series": "SQLite", "x": "2000", "y": 14},{ "series": "MySQL", "x": "2000", "y": 78},
545545
{ "series": "PostgreSQL", "x": "2010", "y": 65},{ "series": "SQLite", "x": "2010", "y": 22},{ "series": "MySQL", "x": "2010", "y": 83},
546546
{ "series": "PostgreSQL", "x": "2020", "y": 73},{ "series": "SQLite", "x": "2020", "y": 28},{ "series": "MySQL", "x": "2020", "y": 87}
547-
]'));
547+
]')),
548+
('chart', '
549+
## Multiple charts on the same line
550+
551+
You can create information-dense dashboards by using the [card component](?component=card#component)
552+
to put multiple charts on the same line.
553+
554+
For this, create one sql file per visualization you want to show,
555+
and set the `embed` attribute of the [card](?component=card#component) component
556+
to the path of the file you want to include, followed by `?_sqlpage_embed`.
557+
',
558+
json('[
559+
{"component":"card", "title":"A dashboard with multiple graphs on the same line", "columns": 2},
560+
{"embed": "/examples/chart.sql?color=green&n=42&_sqlpage_embed", "footer_md": "You can find the sql file that generates the chart [here](https://github.com/lovasoa/SQLpage/tree/main/examples/official-site/examples/chart.sql)" },
561+
{"embed": "/examples/chart.sql?_sqlpage_embed" },
562+
]'));
548563

549564
INSERT INTO component(name, icon, description) VALUES
550565
('table', 'table', 'A table with optional filtering and sorting.

src/webserver/database/execute_queries.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ use std::collections::HashMap;
66
use std::pin::Pin;
77

88
use super::csv_import::run_csv_import;
9-
use super::sql::{ParsedSqlFile, ParsedStatement, StmtWithParams};
9+
use super::sql::{ParsedSqlFile, ParsedStatement, SimpleSelectValue, StmtWithParams};
1010
use crate::dynamic_component::parse_dynamic_rows;
11+
use crate::utils::add_value_to_map;
1112
use crate::webserver::database::sql_pseudofunctions::extract_req_param;
1213
use crate::webserver::database::sql_to_json::row_to_string;
1314
use crate::webserver::http::SingleOrVec;
1415
use crate::webserver::http_request_info::RequestInfo;
1516

16-
use super::sql_pseudofunctions::StmtParam;
17+
use super::sql_pseudofunctions::{extract_req_param_as_json, StmtParam};
1718
use super::{highlight_sql_error, Database, DbItem};
1819
use sqlx::any::{AnyArguments, AnyQueryResult, AnyRow, AnyStatement, AnyTypeInfo};
1920
use sqlx::pool::PoolConnection;
@@ -68,7 +69,7 @@ pub fn stream_query_results<'a>(
6869
)?;
6970
},
7071
ParsedStatement::StaticSimpleSelect(value) => {
71-
for i in parse_dynamic_rows(DbItem::Row(value.clone().into())) {
72+
for i in parse_dynamic_rows(DbItem::Row(exec_static_simple_select(value, request).await?)) {
7273
yield i;
7374
}
7475
}
@@ -79,6 +80,22 @@ pub fn stream_query_results<'a>(
7980
.map(|res| res.unwrap_or_else(DbItem::Error))
8081
}
8182

83+
/// Executes the sqlpage pseudo-functions contained in a static simple select
84+
async fn exec_static_simple_select(
85+
columns: &[(String, SimpleSelectValue)],
86+
req: &RequestInfo,
87+
) -> anyhow::Result<serde_json::Value> {
88+
let mut map = serde_json::Map::with_capacity(columns.len());
89+
for (name, value) in columns {
90+
let value = match value {
91+
SimpleSelectValue::Static(s) => s.clone(),
92+
SimpleSelectValue::Dynamic(p) => extract_req_param_as_json(p, req).await?,
93+
};
94+
map = add_value_to_map(map, (name.clone(), value));
95+
}
96+
Ok(serde_json::Value::Object(map))
97+
}
98+
8299
/// This function is used to create a pinned boxed stream of query results.
83100
/// This allows recursive calls.
84101
pub fn stream_query_results_boxed<'a>(

src/webserver/database/sql.rs

Lines changed: 132 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use super::csv_import::{extract_csv_copy_statement, CsvImport};
22
use super::sql_pseudofunctions::{func_call_to_param, StmtParam};
33
use crate::file_cache::AsyncFromStrWithState;
4-
use crate::utils::add_value_to_map;
54
use crate::{AppState, Database};
65
use anyhow::Context;
76
use async_trait::async_trait;
@@ -59,7 +58,7 @@ pub(super) struct StmtWithParams {
5958
#[derive(Debug)]
6059
pub(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+
7176
fn 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

175179
fn 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

229264
fn 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

Comments
 (0)