Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
- `sqlpage.variables('set')` returns only user-defined SET variables as JSON
- `sqlpage.variables()` returns all variables merged together, with SET variables taking precedence
- **Deprecation warnings**: Using `$var` when both a URL parameter and POST parameter exist with the same name now shows a warning. In a future version, you'll need to explicitly choose between `$var` (URL) and `:var` (POST).
- Improved performance of `sqlpage.run_sql`.
- On a simple test that just runs 4 run_sql calls, the new version is about 2.7x faster (15,708 req/s vs 5,782 req/s) with lower latency (0.637 ms vs 1.730 ms per request).
- add support for postgres range types

## v0.39.1 (2025-11-08)
Expand Down
24 changes: 12 additions & 12 deletions src/webserver/database/execute_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use super::sql::{
use crate::dynamic_component::parse_dynamic_rows;
use crate::utils::add_value_to_map;
use crate::webserver::database::sql_to_json::row_to_string;
use crate::webserver::http_request_info::RequestInfo;
use crate::webserver::http_request_info::ExecutionContext;
use crate::webserver::single_or_vec::SingleOrVec;

use super::syntax_tree::{extract_req_param, StmtParam};
Expand Down Expand Up @@ -44,7 +44,7 @@ impl Database {

pub fn stream_query_results_with_conn<'a>(
sql_file: &'a ParsedSqlFile,
request: &'a RequestInfo,
request: &'a ExecutionContext,
db_connection: &'a mut DbConn,
) -> impl Stream<Item = DbItem> + 'a {
let source_file = &sql_file.source_path;
Expand Down Expand Up @@ -131,7 +131,7 @@ pub fn stop_at_first_error(
/// Executes the sqlpage pseudo-functions contained in a static simple select
async fn exec_static_simple_select(
columns: &[(String, SimpleSelectValue)],
req: &RequestInfo,
req: &ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<serde_json::Value> {
let mut map = serde_json::Map::with_capacity(columns.len());
Expand Down Expand Up @@ -161,7 +161,7 @@ async fn try_rollback_transaction(db_connection: &mut AnyConnection) {
/// Returns `Ok(None)` when NULL should be used as the parameter value.
async fn extract_req_param_as_json(
param: &StmtParam,
request: &RequestInfo,
request: &ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<serde_json::Value> {
if let Some(val) = extract_req_param(param, request, db_connection).await? {
Expand All @@ -175,7 +175,7 @@ async fn extract_req_param_as_json(
/// This allows recursive calls.
pub fn stream_query_results_boxed<'a>(
sql_file: &'a ParsedSqlFile,
request: &'a RequestInfo,
request: &'a ExecutionContext,
db_connection: &'a mut DbConn,
) -> Pin<Box<dyn Stream<Item = DbItem> + 'a>> {
Box::pin(stream_query_results_with_conn(
Expand All @@ -187,7 +187,7 @@ pub fn stream_query_results_boxed<'a>(

async fn execute_set_variable_query<'a>(
db_connection: &'a mut DbConn,
request: &'a RequestInfo,
request: &'a ExecutionContext,
variable: &StmtParam,
statement: &StmtWithParams,
source_file: &Path,
Expand Down Expand Up @@ -223,7 +223,7 @@ async fn execute_set_variable_query<'a>(

async fn execute_set_simple_static<'a>(
db_connection: &'a mut DbConn,
request: &'a RequestInfo,
request: &'a ExecutionContext,
variable: &StmtParam,
value: &SimpleSelectValue,
_source_file: &Path,
Expand Down Expand Up @@ -254,7 +254,7 @@ async fn execute_set_simple_static<'a>(
}

fn vars_and_name<'a, 'b>(
request: &'a RequestInfo,
request: &'a ExecutionContext,
variable: &'b StmtParam,
) -> anyhow::Result<(std::cell::RefMut<'a, HashMap<String, SingleOrVec>>, &'b str)> {
match variable {
Expand All @@ -274,7 +274,7 @@ fn vars_and_name<'a, 'b>(
async fn take_connection<'a>(
db: &'a Database,
conn: &'a mut DbConn,
request: &RequestInfo,
request: &ExecutionContext,
) -> anyhow::Result<&'a mut PoolConnection<sqlx::Any>> {
if let Some(c) = conn {
return Ok(c);
Expand Down Expand Up @@ -349,7 +349,7 @@ fn clone_anyhow_err(source_file: &Path, err: &anyhow::Error) -> anyhow::Error {

async fn bind_parameters<'a>(
stmt: &'a StmtWithParams,
request: &'a RequestInfo,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<StatementWithParams<'a>> {
let sql = stmt.query.as_str();
Expand Down Expand Up @@ -378,7 +378,7 @@ async fn bind_parameters<'a>(
}

async fn apply_delayed_functions(
request: &RequestInfo,
request: &ExecutionContext,
delayed_functions: &[DelayedFunctionCall],
item: &mut DbItem,
) -> anyhow::Result<()> {
Expand All @@ -399,7 +399,7 @@ async fn apply_delayed_functions(
}

async fn apply_single_delayed_function(
request: &RequestInfo,
request: &ExecutionContext,
db_connection: &mut DbConn,
f: &DelayedFunctionCall,
row: &mut serde_json::Map<String, serde_json::Value>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ macro_rules! sqlpage_functions {
pub(crate) async fn evaluate<'a>(
&self,
#[allow(unused_variables)]
request: &'a RequestInfo,
request: &'a $crate::webserver::http_request_info::ExecutionContext,
db_connection: &mut Option<sqlx::pool::PoolConnection<sqlx::Any>>,
params: Vec<Option<Cow<'a, str>>>
) -> anyhow::Result<Option<Cow<'a, str>>> {
Expand Down
25 changes: 9 additions & 16 deletions src/webserver/database/sqlpage_functions/functions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::RequestInfo;
use super::{ExecutionContext, RequestInfo};
use crate::webserver::{
database::{
blob_to_data_url::vec_to_data_uri_with_mime, execute_queries::DbConn,
Expand Down Expand Up @@ -45,15 +45,15 @@ super::function_definition_macro::sqlpage_functions! {
read_file_as_data_url((&RequestInfo), file_path: Option<Cow<str>>);
read_file_as_text((&RequestInfo), file_path: Option<Cow<str>>);
request_method((&RequestInfo));
run_sql((&RequestInfo, &mut DbConn), sql_file_path: Option<Cow<str>>, variables: Option<Cow<str>>);
run_sql((&ExecutionContext, &mut DbConn), sql_file_path: Option<Cow<str>>, variables: Option<Cow<str>>);

uploaded_file_mime_type((&RequestInfo), upload_name: Cow<str>);
uploaded_file_path((&RequestInfo), upload_name: Cow<str>);
uploaded_file_name((&RequestInfo), upload_name: Cow<str>);
url_encode(raw_text: Option<Cow<str>>);
user_info((&RequestInfo), claim: Cow<str>);

variables((&RequestInfo), get_or_post: Option<Cow<str>>);
variables((&ExecutionContext), get_or_post: Option<Cow<str>>);
version();
request_body((&RequestInfo));
request_body_base64((&RequestInfo));
Expand Down Expand Up @@ -549,7 +549,7 @@ async fn request_method(request: &RequestInfo) -> String {
}

async fn run_sql<'a>(
request: &'a RequestInfo,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
sql_file_path: Option<Cow<'a, str>>,
variables: Option<Cow<'a, str>>,
Expand All @@ -570,19 +570,12 @@ async fn run_sql<'a>(
.await
.with_context(|| format!("run_sql: invalid path {sql_file_path:?}"))?;
let tmp_req = if let Some(variables) = variables {
let tmp_req = request.clone_without_variables();
let variables: ParamMap = serde_json::from_str(&variables).map_err(|err| {
let context = format!(
"run_sql: unable to parse the variables argument (line {}, column {})",
err.line(),
err.column()
);
anyhow::Error::new(err).context(context)
let variables: ParamMap = serde_json::from_str(&variables).with_context(|| {
format!("run_sql(\'{sql_file_path}\', \'{variables}\'): the second argument should be a JSON object with string keys and values")
})?;
tmp_req.set_variables.replace(variables);
tmp_req
request.fork_with_variables(variables)
} else {
request.clone()
request.fork()
};
let max_recursion_depth = app_state.config.max_recursion_depth;
if tmp_req.clone_depth > max_recursion_depth {
Expand Down Expand Up @@ -686,7 +679,7 @@ async fn url_encode(raw_text: Option<Cow<'_, str>>) -> Option<Cow<'_, str>> {

/// Returns all variables in the request as a JSON object.
async fn variables<'a>(
request: &'a RequestInfo,
request: &'a ExecutionContext,
get_or_post: Option<Cow<'a, str>>,
) -> anyhow::Result<String> {
Ok(if let Some(get_or_post) = get_or_post {
Expand Down
2 changes: 1 addition & 1 deletion src/webserver/database/sqlpage_functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod url_parameter_deserializer;

use sqlparser::ast::FunctionArg;

use crate::webserver::http_request_info::RequestInfo;
use crate::webserver::http_request_info::{ExecutionContext, RequestInfo};

use super::sql::function_args_to_stmt_params;
use super::syntax_tree::SqlPageFunctionCall;
Expand Down
33 changes: 16 additions & 17 deletions src/webserver/database/syntax_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::str::FromStr;

use sqlparser::ast::FunctionArg;

use crate::webserver::http_request_info::RequestInfo;
use crate::webserver::http_request_info::ExecutionContext;
use crate::webserver::single_or_vec::SingleOrVec;

use super::{
Expand Down Expand Up @@ -112,7 +112,7 @@ impl SqlPageFunctionCall {

pub async fn evaluate<'a>(
&self,
request: &'a RequestInfo,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
let mut params = Vec::with_capacity(self.arguments.len());
Expand Down Expand Up @@ -151,7 +151,7 @@ impl std::fmt::Display for SqlPageFunctionCall {
/// Returns `Ok(None)` when NULL should be used as the parameter value.
pub(super) async fn extract_req_param<'a>(
param: &StmtParam,
request: &'a RequestInfo,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
Ok(match param {
Expand All @@ -169,21 +169,20 @@ pub(super) async fn extract_req_param<'a>(
Some(Cow::Owned(val.as_json_str().into_owned()))
} else {
let url_val = request.url_params.get(x);
let post_val = request.post_variables.get(x);
if let Some(post_val) = post_val {
if let Some(url_val) = url_val {
if request.post_variables.contains_key(x) {
if url_val.is_some() {
log::warn!(
"Deprecation warning! There is both a URL parameter named '{x}' with value '{url_val}' and a form field named '{x}' with value '{post_val}'. \
SQLPage is using the value from the form submission, but this is ambiguous, can lead to unexpected behavior, and will stop working in a future version of SQLPage. \
To fix this, please rename the URL parameter to something else, and reference the form field with :{x}."
"Deprecation warning! There is both a URL parameter named '{x}' and a form field named '{x}'. \
SQLPage is using the URL parameter for ${x}. Please use :{x} to reference the form field explicitly."
);
} else {
log::warn!("Deprecation warning! ${x} was used to reference a form field value (a POST variable) instead of a URL parameter. This will stop working soon. Please use :{x} instead.");
log::warn!(
"Deprecation warning! ${x} was used to reference a form field value (a POST variable). \
This now uses only URL parameters. Please use :{x} instead."
);
}
Some(post_val.as_json_str())
} else {
url_val.map(SingleOrVec::as_json_str)
}
url_val.map(SingleOrVec::as_json_str)
}
}
StmtParam::Error(x) => anyhow::bail!("{x}"),
Expand All @@ -210,7 +209,7 @@ pub(super) async fn extract_req_param<'a>(

async fn concat_params<'a>(
args: &[StmtParam],
request: &'a RequestInfo,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
let mut result = String::new();
Expand All @@ -225,7 +224,7 @@ async fn concat_params<'a>(

async fn coalesce_params<'a>(
args: &[StmtParam],
request: &'a RequestInfo,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
for arg in args {
Expand All @@ -238,7 +237,7 @@ async fn coalesce_params<'a>(

async fn json_object_params<'a>(
args: &[StmtParam],
request: &'a RequestInfo,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
use serde::{ser::SerializeMap, Serializer};
Expand Down Expand Up @@ -276,7 +275,7 @@ async fn json_object_params<'a>(

async fn json_array_params<'a>(
args: &[StmtParam],
request: &'a RequestInfo,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
use serde::{ser::SerializeSeq, Serializer};
Expand Down
13 changes: 7 additions & 6 deletions src/webserver/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,25 +174,26 @@ async fn render_sql(
.clone()
.into_inner();

let req_param = extract_request_info(srv_req, Arc::clone(&app_state), server_timing)
let exec_ctx = extract_request_info(srv_req, Arc::clone(&app_state), server_timing)
.await
.map_err(|e| anyhow_err_to_actix(e, &app_state))?;
log::debug!("Received a request with the following parameters: {req_param:?}");
log::debug!("Received a request with the following parameters: {exec_ctx:?}");

req_param.server_timing.record("parse_req");
exec_ctx.request().server_timing.record("parse_req");

let (resp_send, resp_recv) = tokio::sync::oneshot::channel::<HttpResponse>();
let source_path: PathBuf = sql_file.source_path.clone();
actix_web::rt::spawn(async move {
let request_info = exec_ctx.request();
let request_context = RequestContext {
is_embedded: req_param.url_params.contains_key("_sqlpage_embed"),
is_embedded: request_info.url_params.contains_key("_sqlpage_embed"),
source_path,
content_security_policy: ContentSecurityPolicy::with_random_nonce(),
server_timing: Arc::clone(&req_param.server_timing),
server_timing: Arc::clone(&request_info.server_timing),
};
let mut conn = None;
let database_entries_stream =
stream_query_results_with_conn(&sql_file, &req_param, &mut conn);
stream_query_results_with_conn(&sql_file, &exec_ctx, &mut conn);
let database_entries_stream = stop_at_first_error(database_entries_stream);
let response_with_writer = build_response_header_and_stream(
Arc::clone(&app_state),
Expand Down
Loading