Skip to content

Commit 563bc87

Browse files
authored
delegate statement preparation to sqlx (#135)
* delegate statement preparation to sqlx The logic of preparing statements and caching them for later reuse is now entirely delegated to the sql driver library (sqlx). This simplifies the code and logic inside sqlpage itself. More importantly, statements are now prepared in a streaming fashion when a file is first loaded, instead of all at once, which allows referencing a temporary table created at the start of a file in a later statement in the same file. fixes #100 * remove temporary table usage mssql does not have "create temp table" * mssql permissions fix
1 parent 488dbca commit 563bc87

File tree

7 files changed

+66
-103
lines changed

7 files changed

+66
-103
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- 18 new icons available (see https://github.com/tabler/tabler-icons/releases/tag/v2.40.0)
77
- Support multiple statements in [`on_connect.sql`](./configuration.md) in MySQL.
88
- Randomize postgres prepared statement names to avoid name collisions. This should fix a bug where SQLPage would report errors like `prepared statement "sqlx_s_3" already exists` when using a connection pooler in front of a PostgreSQL database.
9+
- Delegate statement preparation to sqlx. The logic of preparing statements and caching them for later reuse is now entirely delegated to the sql driver library (sqlx). This simplifies the code and logic inside sqlpage itself. More importantly, statements are now prepared in a streaming fashion when a file is first loaded, instead of all at once, which allows referencing a temporary table created at the start of a file in a later statement in the same file.
910

1011
## 0.15.2 (2023-11-12)
1112

mssql/setup.sql

+4
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ GO
99

1010
CREATE LOGIN root WITH PASSWORD = 'Password123!';
1111
CREATE USER root FOR LOGIN root;
12+
GO
13+
14+
GRANT CREATE TABLE TO root;
15+
GRANT ALTER, DELETE, INSERT, SELECT, UPDATE ON SCHEMA::dbo TO root;
1216
GO

src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ impl AppState {
4141
let file_system = FileSystem::init(&config.web_root, &db).await;
4242
sql_file_cache.add_static(
4343
PathBuf::from("index.sql"),
44-
ParsedSqlFile::new(&db, include_str!("../index.sql")).await,
44+
ParsedSqlFile::new(&db, include_str!("../index.sql")),
4545
);
4646
Ok(AppState {
4747
db,

src/webserver/database/execute_queries.rs

+42-21
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,17 @@ use serde_json::Value;
55
use std::borrow::Cow;
66
use std::collections::HashMap;
77

8-
use super::sql::{ParsedSQLStatement, ParsedSqlFile};
8+
use super::sql::{ParsedSqlFile, ParsedStatement, StmtWithParams};
99
use crate::webserver::database::sql_pseudofunctions::extract_req_param;
1010
use crate::webserver::http::{RequestInfo, SingleOrVec};
1111

1212
use sqlx::any::{AnyArguments, AnyQueryResult, AnyRow, AnyStatement, AnyTypeInfo};
1313
use sqlx::pool::PoolConnection;
14-
use sqlx::query::Query;
15-
use sqlx::{AnyConnection, Arguments, Either, Executor, Row, Statement};
14+
use sqlx::{Any, AnyConnection, Arguments, Either, Executor, Row, Statement};
1615

1716
use super::sql_pseudofunctions::StmtParam;
1817
use super::sql_to_json::sql_to_json;
19-
use super::{highlight_sql_error, Database, DbItem, PreparedStatement};
18+
use super::{highlight_sql_error, Database, DbItem};
2019

2120
impl Database {
2221
pub(crate) async fn prepare_with(
@@ -41,33 +40,33 @@ pub fn stream_query_results<'a>(
4140
let mut connection_opt = None;
4241
for res in &sql_file.statements {
4342
match res {
44-
ParsedSQLStatement::Statement(stmt) => {
43+
ParsedStatement::StmtWithParams(stmt) => {
4544
let query = bind_parameters(stmt, request).await?;
4645
let connection = take_connection(db, &mut connection_opt).await?;
47-
let mut stream = query.fetch_many(connection);
46+
let mut stream = connection.fetch_many(query);
4847
while let Some(elem) = stream.next().await {
4948
let is_err = elem.is_err();
50-
yield parse_single_sql_result(stmt, elem);
49+
yield parse_single_sql_result(&stmt.query, elem);
5150
if is_err {
5251
break;
5352
}
5453
}
5554
},
56-
ParsedSQLStatement::SetVariable { variable, value} => {
55+
ParsedStatement::SetVariable { variable, value} => {
5756
let query = bind_parameters(value, request).await?;
5857
let connection = take_connection(db, &mut connection_opt).await?;
59-
let row = query.fetch_optional(connection).await?;
58+
let row = connection.fetch_optional(query).await?;
6059
let (vars, name) = vars_and_name(request, variable)?;
6160
if let Some(row) = row {
6261
vars.insert(name.clone(), row_to_varvalue(&row));
6362
} else {
6463
vars.remove(&name);
6564
}
6665
},
67-
ParsedSQLStatement::StaticSimpleSelect(value) => {
66+
ParsedStatement::StaticSimpleSelect(value) => {
6867
yield DbItem::Row(value.clone().into())
6968
}
70-
ParsedSQLStatement::Error(e) => yield DbItem::Error(clone_anyhow_err(e)),
69+
ParsedStatement::Error(e) => yield DbItem::Error(clone_anyhow_err(e)),
7170
}
7271
}
7372
}
@@ -132,10 +131,7 @@ async fn take_connection<'a, 'b>(
132131
}
133132

134133
#[inline]
135-
fn parse_single_sql_result(
136-
stmt: &PreparedStatement,
137-
res: sqlx::Result<Either<AnyQueryResult, AnyRow>>,
138-
) -> DbItem {
134+
fn parse_single_sql_result(sql: &str, res: sqlx::Result<Either<AnyQueryResult, AnyRow>>) -> DbItem {
139135
match res {
140136
Ok(Either::Right(r)) => DbItem::Row(super::sql_to_json::row_to_json(&r)),
141137
Ok(Either::Left(res)) => {
@@ -144,7 +140,7 @@ fn parse_single_sql_result(
144140
}
145141
Err(err) => DbItem::Error(highlight_sql_error(
146142
"Failed to execute SQL statement",
147-
stmt.statement.sql(),
143+
sql,
148144
err,
149145
)),
150146
}
@@ -159,18 +155,43 @@ fn clone_anyhow_err(err: &anyhow::Error) -> anyhow::Error {
159155
}
160156

161157
async fn bind_parameters<'a>(
162-
stmt: &'a PreparedStatement,
158+
stmt: &'a StmtWithParams,
163159
request: &'a RequestInfo,
164-
) -> anyhow::Result<Query<'a, sqlx::Any, AnyArguments<'a>>> {
160+
) -> anyhow::Result<StatementWithParams<'a>> {
161+
let sql = stmt.query.as_str();
165162
let mut arguments = AnyArguments::default();
166-
for param in &stmt.parameters {
163+
for param in &stmt.params {
167164
let argument = extract_req_param(param, request).await?;
168-
log::debug!("Binding value {:?} in statement {}", &argument, stmt);
165+
log::debug!("Binding value {:?} in statement {}", &argument, stmt.query);
169166
match argument {
170167
None => arguments.add(None::<String>),
171168
Some(Cow::Owned(s)) => arguments.add(s),
172169
Some(Cow::Borrowed(v)) => arguments.add(v),
173170
}
174171
}
175-
Ok(stmt.statement.query_with(arguments))
172+
Ok(StatementWithParams { sql, arguments })
173+
}
174+
175+
pub struct StatementWithParams<'a> {
176+
sql: &'a str,
177+
arguments: AnyArguments<'a>,
178+
}
179+
180+
impl<'q> sqlx::Execute<'q, Any> for StatementWithParams<'q> {
181+
fn sql(&self) -> &'q str {
182+
self.sql
183+
}
184+
185+
fn statement(&self) -> Option<&<Any as sqlx::database::HasStatement<'q>>::Statement> {
186+
None
187+
}
188+
189+
fn take_arguments(&mut self) -> Option<<Any as sqlx::database::HasArguments<'q>>::Arguments> {
190+
Some(std::mem::take(&mut self.arguments))
191+
}
192+
193+
fn persistent(&self) -> bool {
194+
// Let sqlx create a prepared statement the first time it is executed, and then reuse it.
195+
true
196+
}
176197
}

src/webserver/database/mod.rs

-12
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,6 @@ pub enum DbItem {
1818
Error(anyhow::Error),
1919
}
2020

21-
struct PreparedStatement {
22-
statement: sqlx::any::AnyStatement<'static>,
23-
parameters: Vec<sql_pseudofunctions::StmtParam>,
24-
}
25-
26-
impl std::fmt::Display for PreparedStatement {
27-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28-
use sqlx::Statement;
29-
write!(f, "{}", self.statement.sql())
30-
}
31-
}
32-
3321
#[must_use]
3422
pub fn highlight_sql_error(
3523
context: &str,

src/webserver/database/sql.rs

+11-69
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use super::sql_pseudofunctions::{func_call_to_param, StmtParam};
2-
use super::PreparedStatement;
32
use crate::file_cache::AsyncFromStrWithState;
43
use crate::utils::add_value_to_map;
54
use crate::{AppState, Database};
@@ -13,101 +12,51 @@ use sqlparser::dialect::{Dialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect,
1312
use sqlparser::parser::{Parser, ParserError};
1413
use sqlparser::tokenizer::Token::{SemiColon, EOF};
1514
use sqlparser::tokenizer::Tokenizer;
16-
use sqlx::any::{AnyKind, AnyTypeInfo};
17-
use sqlx::Postgres;
15+
use sqlx::any::AnyKind;
1816
use std::fmt::Write;
1917
use std::ops::ControlFlow;
2018

2119
#[derive(Default)]
2220
pub struct ParsedSqlFile {
23-
pub(super) statements: Vec<ParsedSQLStatement>,
24-
}
25-
26-
pub(super) enum ParsedSQLStatement {
27-
Statement(PreparedStatement),
28-
StaticSimpleSelect(serde_json::Map<String, serde_json::Value>),
29-
Error(anyhow::Error),
30-
SetVariable {
31-
variable: StmtParam,
32-
value: PreparedStatement,
33-
},
21+
pub(super) statements: Vec<ParsedStatement>,
3422
}
3523

3624
impl ParsedSqlFile {
37-
pub async fn new(db: &Database, sql: &str) -> ParsedSqlFile {
25+
#[must_use]
26+
pub fn new(db: &Database, sql: &str) -> ParsedSqlFile {
3827
let dialect = dialect_for_db(db.connection.any_kind());
3928
let parsed_statements = match parse_sql(dialect.as_ref(), sql) {
4029
Ok(parsed) => parsed,
4130
Err(err) => return Self::from_err(err),
4231
};
43-
let mut statements = Vec::with_capacity(8);
44-
for parsed in parsed_statements {
45-
statements.push(match parsed {
46-
ParsedStatement::StaticSimpleSelect(s) => ParsedSQLStatement::StaticSimpleSelect(s),
47-
ParsedStatement::Error(e) => ParsedSQLStatement::Error(e),
48-
ParsedStatement::StmtWithParams(stmt_with_params) => {
49-
prepare_query_with_params(db, stmt_with_params).await
50-
}
51-
ParsedStatement::SetVariable { variable, value } => {
52-
match prepare_query_with_params(db, value).await {
53-
ParsedSQLStatement::Statement(value) => {
54-
ParsedSQLStatement::SetVariable { variable, value }
55-
}
56-
err => err,
57-
}
58-
}
59-
});
60-
}
61-
statements.shrink_to_fit();
32+
let statements = parsed_statements.collect();
6233
ParsedSqlFile { statements }
6334
}
6435

6536
fn from_err(e: impl Into<anyhow::Error>) -> Self {
6637
Self {
67-
statements: vec![ParsedSQLStatement::Error(
38+
statements: vec![ParsedStatement::Error(
6839
e.into().context("SQLPage could not parse the SQL file"),
6940
)],
7041
}
7142
}
7243
}
7344

74-
async fn prepare_query_with_params(
75-
db: &Database,
76-
StmtWithParams { query, params }: StmtWithParams,
77-
) -> ParsedSQLStatement {
78-
let param_types = get_param_types(&params);
79-
match db.prepare_with(&query, &param_types).await {
80-
Ok(statement) => {
81-
log::debug!("Successfully prepared SQL statement '{query}'");
82-
ParsedSQLStatement::Statement(PreparedStatement {
83-
statement,
84-
parameters: params,
85-
})
86-
}
87-
Err(err) => {
88-
log::warn!("Failed to prepare {query:?}: {err:#}");
89-
ParsedSQLStatement::Error(err.context(format!(
90-
"The database returned an error when preparing this SQL statement: {query}"
91-
)))
92-
}
93-
}
94-
}
95-
9645
#[async_trait(? Send)]
9746
impl AsyncFromStrWithState for ParsedSqlFile {
9847
async fn from_str_with_state(app_state: &AppState, source: &str) -> anyhow::Result<Self> {
99-
Ok(ParsedSqlFile::new(&app_state.db, source).await)
48+
Ok(ParsedSqlFile::new(&app_state.db, source))
10049
}
10150
}
10251

10352
#[derive(Debug, PartialEq)]
104-
struct StmtWithParams {
105-
query: String,
106-
params: Vec<StmtParam>,
53+
pub(super) struct StmtWithParams {
54+
pub query: String,
55+
pub params: Vec<StmtParam>,
10756
}
10857

10958
#[derive(Debug)]
110-
enum ParsedStatement {
59+
pub(super) enum ParsedStatement {
11160
StmtWithParams(StmtWithParams),
11261
StaticSimpleSelect(serde_json::Map<String, serde_json::Value>),
11362
SetVariable {
@@ -201,13 +150,6 @@ fn kind_of_dialect(dialect: &dyn Dialect) -> AnyKind {
201150
}
202151
}
203152

204-
fn get_param_types(parameters: &[StmtParam]) -> Vec<AnyTypeInfo> {
205-
parameters
206-
.iter()
207-
.map(|_p| <str as sqlx::Type<Postgres>>::type_info().into())
208-
.collect()
209-
}
210-
211153
fn map_param(mut name: String) -> StmtParam {
212154
if name.is_empty() {
213155
return StmtParam::GetOrPost(name);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
drop table if exists my_tmp_store;
2+
create table my_tmp_store(x varchar(100));
3+
4+
insert into my_tmp_store(x) values ('It works !');
5+
6+
select 'card' as component;
7+
select x as description from my_tmp_store;

0 commit comments

Comments
 (0)