Refactor SQL parameter handling into unified expression tree

Replace the `StmtParam`/`ParameterExtractor` machinery with a shared
`SqlPageExpr` type parameterized over its input source, distinguishing
standalone expressions from per-row expressions via the type system.

Rename `ParsedSqlFile` to `SqlFile` and split SQL parsing into dedicated
`dialect`, `rewrite`, and `statement` submodules. Queries are now
rewritten into either a database query with bindings and computed
columns, or a single-row query evaluated without a database round trip.

Make `SqlPageFunctionName` parsing case-insensitive and support
SQLPage-computed projections and nested functions over query results.
This commit is contained in:
Ophir LOJKINE
2026-07-15 13:44:31 +02:00
parent 7b72bd5d1a
commit e07594579b
18 changed files with 1932 additions and 2747 deletions
+4 -4
View File
@@ -85,7 +85,7 @@ pub mod webserver;
use crate::app_config::AppConfig;
use crate::filesystem::FileSystem;
use crate::webserver::database::ParsedSqlFile;
use crate::webserver::database::SqlFile;
use crate::webserver::oidc::OidcState;
use file_cache::FileCache;
use std::path::{Path, PathBuf};
@@ -106,7 +106,7 @@ pub const DEFAULT_404_FILE: &str = "default_404.sql";
pub struct AppState {
pub db: Database,
all_templates: AllTemplates,
sql_file_cache: FileCache<ParsedSqlFile>,
sql_file_cache: FileCache<SqlFile>,
file_system: FileSystem,
config: AppConfig,
pub oidc_state: Option<Arc<OidcState>>,
@@ -124,11 +124,11 @@ impl AppState {
let file_system = FileSystem::init(&config.web_root, &db).await;
sql_file_cache.add_static(
PathBuf::from("index.sql"),
ParsedSqlFile::new(&db, include_str!("index.sql"), Path::new("index.sql")),
SqlFile::new(&db, include_str!("index.sql"), Path::new("index.sql")),
);
sql_file_cache.add_static(
PathBuf::from(DEFAULT_404_FILE),
ParsedSqlFile::new(
SqlFile::new(
&db,
include_str!("default_404.sql"),
Path::new(DEFAULT_404_FILE),
+5 -4
View File
@@ -3,7 +3,7 @@ use std::{
path::{Path, PathBuf},
};
use super::sql::{SourceSpan, StmtWithParams};
use super::sql::SourceSpan;
#[derive(Debug)]
struct NiceDatabaseError {
@@ -114,14 +114,15 @@ pub fn display_db_error(
#[must_use]
pub fn display_stmt_db_error(
source_file: &Path,
stmt: &StmtWithParams,
query: &str,
query_position: SourceSpan,
db_err: sqlx::error::Error,
) -> anyhow::Error {
anyhow::Error::new(NiceDatabaseError {
source_file: source_file.to_path_buf(),
db_err,
query: stmt.query.clone(),
query_position: Some(stmt.query_position),
query: query.to_owned(),
query_position: Some(query_position),
})
}
+179 -249
View File
@@ -10,17 +10,16 @@ use tracing::Instrument;
use super::csv_import::run_csv_import;
use super::error_highlighting::{display_stmt_db_error, display_stmt_error};
use super::sql::{
DelayedFunctionCall, ParsedSqlFile, ParsedStatement, SimpleSelectValue, StmtWithParams,
DatabaseQuery, FileStatement, OutputColumn, Query, QueryBody, SingleRowQuery, SourceSpan,
SqlFile,
};
use super::sqlpage_functions::functions::SqlPageFunctionName;
use super::sqlpage_expr::{NoInputs, RowExpr, RowInputs};
use crate::dynamic_component::parse_dynamic_rows;
use crate::utils::add_value_to_map;
use crate::webserver::ErrorWithStatus;
use crate::webserver::http_request_info::ExecutionContext;
use crate::webserver::request_variables::SetVariablesMap;
use crate::webserver::single_or_vec::SingleOrVec;
use super::syntax_tree::{StmtParam, extract_req_param};
use super::{Database, DbItem, error_highlighting::display_db_error};
use sqlx::any::{AnyArguments, AnyQueryResult, AnyRow, AnyStatement, AnyTypeInfo};
use sqlx::pool::PoolConnection;
@@ -30,6 +29,11 @@ use sqlx::{
pub type DbConn = Option<PoolConnection<sqlx::Any>>;
struct QueryResult {
item: DbItem,
inputs: RowInputs,
}
fn source_line_number(line: usize) -> i64 {
i64::try_from(line).unwrap_or(i64::MAX)
}
@@ -138,14 +142,14 @@ fn create_db_query_span(
fn create_query_metrics<'a>(
request: &'a ExecutionContext,
source_file: &Path,
statement: &StmtWithParams,
query: &StatementWithParams<'_>,
source_span: SourceSpan,
query: &BoundQuery<'_>,
) -> (tracing::Span, DbQueryMetricsContext<'a>) {
let db_system_name = request.app_state.db.info.database_type.otel_name();
let (query_span, operation_name) = create_db_query_span(
query.sql,
source_file,
statement.query_position.start.line,
source_span.start.line,
db_system_name,
);
let query_metrics = DbQueryMetricsContext::new(
@@ -174,7 +178,7 @@ impl Database {
#[allow(clippy::too_many_lines)] // Keeps the single-connection statement dispatcher together.
pub fn stream_query_results_with_conn<'a>(
sql_file: &'a ParsedSqlFile,
sql_file: &'a SqlFile,
request: &'a ExecutionContext,
db_connection: &'a mut DbConn,
) -> impl Stream<Item = DbItem> + 'a {
@@ -182,24 +186,30 @@ pub fn stream_query_results_with_conn<'a>(
async_stream::try_stream! {
for res in &sql_file.statements {
match res {
ParsedStatement::CsvImport(csv_import) => {
FileStatement::CsvImport(csv_import) => {
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
log::debug!("Executing CSV import: {csv_import:?}");
run_csv_import(connection, csv_import, request).await.with_context(|| format!("Failed to import the CSV file {:?} into the table {:?}", csv_import.uploaded_file, csv_import.table_name))?;
},
ParsedStatement::StmtWithParams(stmt) => {
let query = bind_parameters(stmt, request, db_connection)
FileStatement::Query(statement) => match &statement.body {
QueryBody::SingleRow(query) => {
let row = execute_single_row(query, request, db_connection)
.await
.map_err(|e| with_stmt_position(source_file, stmt.query_position, e))?;
.map_err(|error| with_stmt_position(source_file, statement.source_span, error))?;
for item in parse_dynamic_rows(DbItem::Row(row)) {
yield item;
}
}
QueryBody::Database(stmt) => {
let query = bind_query(stmt, request, db_connection)
.await
.map_err(|error| with_stmt_position(source_file, statement.source_span, error))?;
request.server_timing.record("bind_params");
log::trace!("Executing query {:?}", query.sql);
let (query_span, mut query_metrics) = create_query_metrics(request, source_file, stmt, &query);
let (query_span, mut query_metrics) = create_query_metrics(request, source_file, statement.source_span, &query);
let mut error = None;
let mut returned_rows: i64 = 0;
let defer_functions_until_stream_closed = stmt
.delayed_functions
.iter()
.any(|f| f.function == SqlPageFunctionName::run_sql);
let buffer_rows = stmt.must_buffer_rows();
let mut deferred_query_results = Vec::new();
{
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
@@ -210,37 +220,38 @@ pub fn stream_query_results_with_conn<'a>(
query_metrics.add_duration(start_next.elapsed());
let Some(elem) = next_elem else { break; };
let mut query_result = parse_single_sql_result(source_file, stmt, elem);
if let DbItem::Error(e) = query_result {
let mut query_result = parse_single_sql_result(source_file, stmt, statement.source_span, elem);
if let DbItem::Error(e) = query_result.item {
error = Some(e);
break;
}
if matches!(query_result, DbItem::Row(_)) {
if matches!(query_result.item, DbItem::Row(_)) {
returned_rows += 1;
}
apply_json_columns(&mut query_result, &stmt.json_columns);
if defer_functions_until_stream_closed {
apply_json_columns(&mut query_result.item, &stmt.json_columns);
if buffer_rows {
deferred_query_results.push(query_result);
} else {
if let Err(err) = apply_delayed_functions(request, &stmt.delayed_functions, &mut query_result)
let mut computed_connection = None;
if let Err(err) = evaluate_computed_columns(request, &stmt.computed_columns, &mut query_result, &mut computed_connection)
.instrument(query_span.clone())
.await
{
error = Some(err);
break;
}
for db_item in parse_dynamic_rows(query_result) {
for db_item in parse_dynamic_rows(query_result.item) {
yield db_item;
}
}
}
drop(stream);
}
if error.is_none() && defer_functions_until_stream_closed {
if error.is_none() && buffer_rows {
for mut query_result in deferred_query_results {
if let Err(err) = apply_delayed_functions_on_connection(
if let Err(err) = evaluate_computed_columns(
request,
&stmt.delayed_functions,
&stmt.computed_columns,
&mut query_result,
db_connection,
)
@@ -250,7 +261,7 @@ pub fn stream_query_results_with_conn<'a>(
error = Some(err);
break;
}
for db_item in parse_dynamic_rows(query_result) {
for db_item in parse_dynamic_rows(query_result.item) {
yield db_item;
}
}
@@ -262,28 +273,15 @@ pub fn stream_query_results_with_conn<'a>(
} else {
query_metrics.record_success(returned_rows);
}
}
},
ParsedStatement::SetVariable { variable, value} => {
execute_set_variable_query(db_connection, request, variable, value, source_file).await
FileStatement::SetVariable { target, value} => {
execute_set_variable_query(db_connection, request, target, value, source_file).await
.with_context(||
format!("Failed to set the {variable} variable to {value:?}")
format!("Failed to set the {} variable to {value:?}", target.0)
)?;
},
ParsedStatement::StaticSimpleSet { variable, value} => {
execute_set_simple_static(db_connection, request, variable, value, source_file).await
.with_context(||
format!("Failed to set the {variable} variable to {value:?}")
)?;
},
ParsedStatement::StaticSimpleSelect { values, query_position } => {
let row = exec_static_simple_select(values, request, db_connection)
.await
.map_err(|e| with_stmt_position(source_file, *query_position, e))?;
for i in parse_dynamic_rows(DbItem::Row(row)) {
yield i;
}
}
ParsedStatement::Error(e) => yield DbItem::Error(clone_anyhow_err(source_file, e)),
FileStatement::Error(e) => yield DbItem::Error(clone_anyhow_err(source_file, e)),
}
}
}
@@ -325,21 +323,20 @@ pub fn stop_at_first_error(
.take_until(error_rx)
}
/// Executes the sqlpage pseudo-functions contained in a static simple select
async fn exec_static_simple_select(
columns: &[(String, SimpleSelectValue)],
async fn execute_single_row(
query: &SingleRowQuery,
req: &ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<serde_json::Value> {
let mut map = serde_json::Map::with_capacity(columns.len());
for (name, value) in columns {
let value = match value {
SimpleSelectValue::Static(s) => s.clone(),
SimpleSelectValue::Dynamic(p) => {
extract_req_param_as_json(p, req, db_connection).await?
}
};
map = add_value_to_map(map, (name.clone(), value));
let mut map = serde_json::Map::with_capacity(query.columns.len());
let mut inputs = NoInputs;
for column in &query.columns {
let value = column
.value
.evaluate(req, db_connection, &mut inputs)
.await?
.into_json();
map = add_value_to_map(map, (column.name.clone(), value));
}
Ok(serde_json::Value::Object(map))
}
@@ -354,24 +351,10 @@ async fn try_rollback_transaction(db_connection: &mut AnyConnection) {
}
}
/// Extracts the value of a parameter from the request.
/// Returns `Ok(None)` when NULL should be used as the parameter value.
async fn extract_req_param_as_json(
param: &StmtParam,
request: &ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<serde_json::Value> {
if let Some(val) = extract_req_param(param, request, db_connection).await? {
Ok(serde_json::Value::String(val.into_owned()))
} else {
Ok(serde_json::Value::Null)
}
}
/// This function is used to create a pinned boxed stream of query results.
/// This allows recursive calls.
pub fn stream_query_results_boxed<'a>(
sql_file: &'a ParsedSqlFile,
sql_file: &'a SqlFile,
request: &'a ExecutionContext,
db_connection: &'a mut DbConn,
) -> Pin<Box<dyn Stream<Item = DbItem> + 'a>> {
@@ -385,16 +368,17 @@ pub fn stream_query_results_boxed<'a>(
async fn execute_set_variable_query<'a>(
db_connection: &'a mut DbConn,
request: &'a ExecutionContext,
variable: &StmtParam,
statement: &StmtWithParams,
variable: &super::sql::VariableName,
statement: &Query,
source_file: &Path,
) -> anyhow::Result<()> {
let value = execute_scalar_query(db_connection, request, statement, source_file).await?;
let (mut vars, name) = vars_and_name(request, variable)?;
log::debug!("Setting variable {name} to {value:?}");
vars.insert(name.to_owned(), value.map(SingleOrVec::Single));
log::debug!("Setting variable {} to {value:?}", variable.0);
request
.set_variables
.borrow_mut()
.insert(variable.0.clone(), value.map(SingleOrVec::Single));
Ok(())
}
@@ -402,55 +386,76 @@ async fn execute_set_variable_query<'a>(
async fn execute_scalar_query<'a>(
db_connection: &'a mut DbConn,
request: &'a ExecutionContext,
statement: &StmtWithParams,
statement: &Query,
source_file: &Path,
) -> anyhow::Result<Option<String>> {
let query = bind_parameters(statement, request, db_connection).await?;
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
let QueryBody::Database(database_query) = &statement.body else {
let QueryBody::SingleRow(single_row) = &statement.body else {
unreachable!()
};
let row = execute_single_row(single_row, request, db_connection).await?;
return scalar_value_from_row(DbItem::Row(row));
};
let query = bind_query(database_query, request, db_connection).await?;
log::debug!("Executing scalar query: {:?}", query.sql);
let (query_span, mut query_metrics) =
create_query_metrics(request, source_file, statement, &query);
create_query_metrics(request, source_file, statement.source_span, &query);
let mut stream = connection.fetch_many(query);
let mut scalar_row = None;
let mut returned_rows: i64 = 0;
let mut error = None;
loop {
let start_next = std::time::Instant::now();
let next_elem = stream.next().instrument(query_span.clone()).await;
query_metrics.add_duration(start_next.elapsed());
let Some(elem) = next_elem else { break };
{
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
let mut stream = connection.fetch_many(query);
loop {
let start_next = std::time::Instant::now();
let next_elem = stream.next().instrument(query_span.clone()).await;
query_metrics.add_duration(start_next.elapsed());
let Some(elem) = next_elem else { break };
match parse_single_sql_result(source_file, statement, elem) {
row @ DbItem::Row(_) => {
returned_rows += 1;
if scalar_row.is_some() {
error = Some(anyhow!(
"SET scalar query returned more than one row. A SET subquery must return zero or one row."
));
let result =
parse_single_sql_result(source_file, database_query, statement.source_span, elem);
match result.item {
row @ DbItem::Row(_) => {
returned_rows += 1;
if scalar_row.is_some() {
error = Some(anyhow!(
"SET scalar query returned more than one row. A SET subquery must return zero or one row."
));
break;
}
scalar_row = Some(QueryResult {
item: row,
inputs: result.inputs,
});
}
DbItem::FinishedQuery => {}
DbItem::Error(err) => {
error = Some(err);
break;
}
scalar_row = Some(row);
}
DbItem::FinishedQuery => {}
DbItem::Error(err) => {
error = Some(err);
break;
}
}
drop(stream);
}
drop(stream);
let value = if let Some(error) = error {
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
return Err(
record_error_and_rollback(connection, &query_metrics, returned_rows, error).await,
);
} else if let Some(mut row) = scalar_row {
apply_json_columns(&mut row, &statement.json_columns);
if let Err(error) = apply_delayed_functions(request, &statement.delayed_functions, &mut row)
.instrument(query_span.clone())
.await
apply_json_columns(&mut row.item, &database_query.json_columns);
if let Err(error) = evaluate_computed_columns(
request,
&database_query.computed_columns,
&mut row,
db_connection,
)
.instrument(query_span.clone())
.await
{
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
return Err(record_error_and_rollback(
connection,
&query_metrics,
@@ -459,7 +464,7 @@ async fn execute_scalar_query<'a>(
)
.await);
}
scalar_value_from_row(row)?
scalar_value_from_row(row.item)?
} else {
None
};
@@ -505,53 +510,6 @@ fn json_to_scalar_string(value: Value) -> Option<String> {
}
}
async fn execute_set_simple_static<'a>(
db_connection: &'a mut DbConn,
request: &'a ExecutionContext,
variable: &StmtParam,
value: &SimpleSelectValue,
_source_file: &Path,
) -> anyhow::Result<()> {
let value_str = match value {
SimpleSelectValue::Static(json_value) => match json_value {
serde_json::Value::Null => None,
serde_json::Value::String(s) => Some(s.clone()),
other => Some(other.to_string()),
},
SimpleSelectValue::Dynamic(stmt_param) => {
extract_req_param(stmt_param, request, db_connection)
.await?
.map(std::borrow::Cow::into_owned)
}
};
let (mut vars, name) = vars_and_name(request, variable)?;
log::debug!("Setting variable {name} to static value {value_str:?}");
vars.insert(name.to_owned(), value_str.map(SingleOrVec::Single));
Ok(())
}
fn vars_and_name<'a, 'b>(
request: &'a ExecutionContext,
variable: &'b StmtParam,
) -> anyhow::Result<(std::cell::RefMut<'a, SetVariablesMap>, &'b str)> {
match variable {
StmtParam::PostOrGet(name) | StmtParam::Get(name) => {
if request.post_variables.contains_key(name) {
log::warn!(
"Deprecation warning! Setting the value of ${name}, but there is already a form field named :{name}. This will stop working soon. Please rename the variable, or use :{name} directly if you intended to overwrite the posted form field value."
);
}
Ok((request.set_variables.borrow_mut(), name))
}
StmtParam::Post(name) => Ok((request.set_variables.borrow_mut(), name)),
_ => Err(anyhow!(
"Only GET and POST variables can be set, not {variable:?}"
)),
}
}
async fn take_connection<'a>(
db: &'a Database,
conn: &'a mut DbConn,
@@ -625,23 +583,42 @@ async fn set_trace_context(connection: &mut AnyConnection, db: &Database) {
#[inline]
fn parse_single_sql_result(
source_file: &Path,
stmt: &StmtWithParams,
query: &DatabaseQuery,
source_span: SourceSpan,
res: sqlx::Result<Either<AnyQueryResult, AnyRow>>,
) -> DbItem {
) -> QueryResult {
match res {
Ok(Either::Right(r)) => {
if log::log_enabled!(log::Level::Trace) {
debug_row(&r);
}
DbItem::Row(super::sql_to_json::row_to_json(&r))
match super::sql_to_json::row_to_json_with_inputs(&r, query.row_input_json.len()) {
Ok((row, mut inputs)) => {
decode_json_values(&mut inputs, &query.row_input_json);
QueryResult {
item: DbItem::Row(row),
inputs: RowInputs::new(inputs),
}
}
Err(error) => QueryResult {
item: DbItem::Error(error),
inputs: RowInputs::new(Vec::new()),
},
}
}
Ok(Either::Left(res)) => {
log::debug!("Finished query with result: {res:?}");
DbItem::FinishedQuery
QueryResult {
item: DbItem::FinishedQuery,
inputs: RowInputs::new(Vec::new()),
}
}
Err(err) => {
let nice_err = display_stmt_db_error(source_file, stmt, err);
DbItem::Error(nice_err)
let nice_err = display_stmt_db_error(source_file, &query.sql, source_span, err);
QueryResult {
item: DbItem::Error(nice_err),
inputs: RowInputs::new(Vec::new()),
}
}
}
}
@@ -667,16 +644,6 @@ fn debug_row(r: &AnyRow) {
}
fn clone_anyhow_err(source_file: &Path, err: &anyhow::Error) -> anyhow::Error {
if let Some(func_err) = err.downcast_ref::<super::sql::SqlPageFunctionError>() {
let line = func_err.line;
let loc = if line > 0 {
format!(":{line}")
} else {
String::new()
};
return anyhow::anyhow!("{}{loc} {}", source_file.display(), func_err);
}
let mut e = anyhow!(
"{} contains a syntax error preventing SQLPage from parsing and preparing its SQL statements.",
source_file.display()
@@ -687,18 +654,22 @@ fn clone_anyhow_err(source_file: &Path, err: &anyhow::Error) -> anyhow::Error {
e
}
async fn bind_parameters<'a>(
stmt: &'a StmtWithParams,
async fn bind_query<'a>(
query: &'a DatabaseQuery,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<StatementWithParams<'a>> {
let sql = stmt.query.as_str();
) -> anyhow::Result<BoundQuery<'a>> {
let sql = query.sql.as_str();
log::debug!("Preparing statement: {sql}");
let mut arguments = AnyArguments::default();
let mut param_values = Vec::with_capacity(stmt.params.len());
for (param_idx, param) in stmt.params.iter().enumerate() {
log::trace!("\tevaluating parameter {}: {}", param_idx + 1, param);
let argument = extract_req_param(param, request, db_connection).await?;
let mut param_values = Vec::with_capacity(query.bindings.len());
let mut inputs = NoInputs;
for (param_idx, binding) in query.bindings.iter().enumerate() {
log::trace!("\tevaluating binding {}: {:?}", param_idx + 1, binding);
let argument = binding
.evaluate(request, db_connection, &mut inputs)
.await?
.into_function_argument();
log::debug!(
"\tparameter {}: {}",
param_idx + 1,
@@ -711,8 +682,8 @@ async fn bind_parameters<'a>(
Some(Cow::Borrowed(v)) => arguments.add(v),
}
}
let has_arguments = !stmt.params.is_empty();
Ok(StatementWithParams {
let has_arguments = !query.bindings.is_empty();
Ok(BoundQuery {
sql,
arguments,
has_arguments,
@@ -720,79 +691,26 @@ async fn bind_parameters<'a>(
})
}
async fn apply_delayed_functions(
async fn evaluate_computed_columns(
request: &ExecutionContext,
delayed_functions: &[DelayedFunctionCall],
item: &mut DbItem,
columns: &[OutputColumn<RowExpr>],
result: &mut QueryResult,
db_connection: &mut DbConn,
) -> anyhow::Result<()> {
// We need to open new connections for each delayed function call, because we are still fetching the results of the current query in the main connection.
let mut db_conn = None;
if let DbItem::Row(serde_json::Value::Object(results)) = item {
for f in delayed_functions {
log::trace!("Applying delayed function {} to {:?}", f.function, results);
apply_single_delayed_function(request, &mut db_conn, f, results).await?;
log::trace!(
"Delayed function applied {}. Result: {:?}",
f.function,
results
);
if let DbItem::Row(serde_json::Value::Object(results)) = &mut result.item {
for column in columns {
let value = column
.value
.evaluate(request, db_connection, &mut result.inputs)
.await?
.into_json();
let old_results = std::mem::take(results);
*results = add_value_to_map(old_results, (column.name.clone(), value));
}
}
Ok(())
}
async fn apply_delayed_functions_on_connection(
request: &ExecutionContext,
delayed_functions: &[DelayedFunctionCall],
item: &mut DbItem,
db_connection: &mut DbConn,
) -> anyhow::Result<()> {
if let DbItem::Row(serde_json::Value::Object(results)) = item {
for f in delayed_functions {
log::trace!("Applying delayed function {} to {:?}", f.function, results);
apply_single_delayed_function(request, db_connection, f, results).await?;
log::trace!(
"Delayed function applied {}. Result: {:?}",
f.function,
results
);
}
}
Ok(())
}
async fn apply_single_delayed_function(
request: &ExecutionContext,
db_connection: &mut DbConn,
f: &DelayedFunctionCall,
row: &mut serde_json::Map<String, serde_json::Value>,
) -> anyhow::Result<()> {
let mut params = Vec::new();
for arg in &f.argument_col_names {
let Some(arg_value) = row.remove(arg) else {
anyhow::bail!(
"The column {arg} is missing in the result set, but it is required by the {} function.",
f.function
);
};
params.push(json_to_fn_param(arg_value));
}
let result_str = f.function.evaluate(request, db_connection, params).await?;
let result_json = result_str
.map(Cow::into_owned)
.map_or(serde_json::Value::Null, serde_json::Value::String);
row.insert(f.target_col_name.clone(), result_json);
Ok(())
}
fn json_to_fn_param(json: serde_json::Value) -> Option<Cow<'static, str>> {
match json {
serde_json::Value::String(s) => Some(Cow::Owned(s)),
serde_json::Value::Null => None,
_ => Some(Cow::Owned(json.to_string())),
}
}
fn apply_json_columns(item: &mut DbItem, json_columns: &[String]) {
if let DbItem::Row(Value::Object(row)) = item {
for column in json_columns {
@@ -823,14 +741,26 @@ fn apply_json_columns(item: &mut DbItem, json_columns: &[String]) {
}
}
pub struct StatementWithParams<'a> {
fn decode_json_values(values: &mut [Value], json_flags: &[bool]) {
debug_assert_eq!(values.len(), json_flags.len());
for (value, decode_as_json) in values.iter_mut().zip(json_flags) {
if *decode_as_json
&& let Value::String(json) = value
&& let Ok(parsed) = serde_json::from_str(json)
{
*value = parsed;
}
}
}
pub struct BoundQuery<'a> {
sql: &'a str,
arguments: AnyArguments<'a>,
has_arguments: bool,
param_values: Vec<Option<String>>,
}
impl<'q> sqlx::Execute<'q, Any> for StatementWithParams<'q> {
impl<'q> sqlx::Execute<'q, Any> for BoundQuery<'q> {
fn sql(&self) -> &'q str {
self.sql
}
+7 -10
View File
@@ -4,14 +4,13 @@ mod csv_import;
pub mod execute_queries;
pub mod migrations;
mod sql;
mod sqlpage_expr;
mod sqlpage_functions;
mod syntax_tree;
mod error_highlighting;
mod sql_to_json;
pub use sql::ParsedSqlFile;
use sql::{DB_PLACEHOLDERS, DbPlaceHolder};
pub use sql::SqlFile;
use sqlx::any::AnyKind;
// SupportedDatabase is defined in this module
@@ -131,12 +130,10 @@ impl std::fmt::Display for Database {
#[inline]
#[must_use]
pub fn make_placeholder(dbms: AnyKind, arg_number: usize) -> String {
if let Some((_, placeholder)) = DB_PLACEHOLDERS.iter().find(|(kind, _)| *kind == dbms) {
match *placeholder {
DbPlaceHolder::PrefixedNumber { prefix } => format!("{prefix}{arg_number}"),
DbPlaceHolder::Positional { placeholder } => placeholder.to_string(),
}
} else {
unreachable!("missing dbms: {dbms:?} in DB_PLACEHOLDERS ({DB_PLACEHOLDERS:?})")
match dbms {
AnyKind::Sqlite => format!("?{arg_number}"),
AnyKind::Postgres => format!("${arg_number}"),
AnyKind::Mssql => format!("@p{arg_number}"),
AnyKind::MySql | AnyKind::Odbc => "?".to_owned(),
}
}
File diff suppressed because it is too large Load Diff
@@ -1,226 +0,0 @@
use sqlparser::ast::{
Expr, Function, FunctionArg, FunctionArgExpr, FunctionArgumentList, FunctionArguments, Ident,
ObjectName, SelectItem, SetExpr, Statement, Value,
};
use super::{DelayedFunctionCall, SqlPageFunctionName, extract_sqlpage_function_name};
use crate::webserver::database::sql::parameter_extraction::{
ParamExtractContext, function_args_to_stmt_params,
};
/// The execution of standalone projected `SQLPage` functions is delayed until after
/// the query has been executed. For instance, `SELECT sqlpage.fetch(x) AS body FROM t`
/// is executed as `SELECT x AS _sqlpage_f0_a0 FROM t`; `sqlpage.fetch` is then
/// called with `_sqlpage_f0_a0` for each returned row.
pub(super) fn extract_delayed_functions_from_query(
stmt: &mut Statement,
) -> Vec<DelayedFunctionCall> {
match stmt {
Statement::Query(q) => {
let is_limited = q.limit_clause.is_some() || q.fetch.is_some();
let SetExpr::Select(s) = q.body.as_mut() else {
return Vec::new();
};
let is_constant_single_row = !is_limited && s.from.is_empty() && s.selection.is_none();
extract_delayed_functions_from_projection(&mut s.projection, is_constant_single_row)
}
_ => Vec::new(),
}
}
fn extract_delayed_functions_from_projection(
select_items: &mut Vec<SelectItem>,
is_constant_single_row: bool,
) -> Vec<DelayedFunctionCall> {
let mut delayed_function_calls = Vec::new();
let mut rewritten_projection = Vec::with_capacity(select_items.len());
for item in std::mem::take(select_items) {
rewrite_select_item(
item,
is_constant_single_row,
&mut rewritten_projection,
&mut delayed_function_calls,
);
}
*select_items = rewritten_projection;
delayed_function_calls
}
fn rewrite_select_item(
item: SelectItem,
is_constant_single_row: bool,
rewritten_projection: &mut Vec<SelectItem>,
delayed_function_calls: &mut Vec<DelayedFunctionCall>,
) {
match item {
SelectItem::ExprWithAlias {
expr: Expr::Function(function),
alias,
} => {
if let Some(func_name) = delayable_sqlpage_function(&function)
&& (!is_constant_single_row || !preserves_null_concat_semantics(&function))
{
let (replacement_items, delayed_call) = rewrite_function_projection(
function,
func_name,
alias.value,
delayed_function_calls.len(),
);
rewritten_projection.extend(replacement_items);
delayed_function_calls.push(delayed_call);
} else {
rewritten_projection.push(SelectItem::ExprWithAlias {
expr: Expr::Function(function),
alias,
});
}
}
SelectItem::UnnamedExpr(Expr::Function(function)) => {
if let Some(func_name) = delayable_sqlpage_function(&function)
&& (!is_constant_single_row || !preserves_null_concat_semantics(&function))
{
let target_col_name = function.to_string();
let (replacement_items, delayed_call) = rewrite_function_projection(
function,
func_name,
target_col_name,
delayed_function_calls.len(),
);
rewritten_projection.extend(replacement_items);
delayed_function_calls.push(delayed_call);
} else {
rewritten_projection.push(SelectItem::UnnamedExpr(Expr::Function(function)));
}
}
item => rewritten_projection.push(item),
}
}
fn delayable_sqlpage_function(function: &Function) -> Option<SqlPageFunctionName> {
let Function {
name: ObjectName(func_name_parts),
args:
FunctionArguments::List(FunctionArgumentList {
args,
duplicate_treatment: None,
..
}),
..
} = function
else {
return None;
};
let func_name = extract_sqlpage_function_name(func_name_parts)?;
if !args.iter().all(function_arg_is_expr) {
log::error!("Unsupported argument to {func_name}: {args:?}");
return None;
}
Some(func_name)
}
fn preserves_null_concat_semantics(function: &Function) -> bool {
let FunctionArguments::List(FunctionArgumentList { args, .. }) = &function.args else {
return false;
};
let mut args = args.clone();
let has_concat = args.iter().any(function_arg_is_concat);
has_concat && function_args_to_stmt_params(&mut args, &ParamExtractContext::default()).is_ok()
}
fn function_arg_is_concat(arg: &FunctionArg) -> bool {
let (FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))
| FunctionArg::Named {
arg: FunctionArgExpr::Expr(expr),
..
}) = arg
else {
return false;
};
match expr {
Expr::BinaryOp {
op: sqlparser::ast::BinaryOperator::StringConcat,
..
} => true,
Expr::Function(Function {
name: ObjectName(parts),
..
}) => {
parts.len() == 1
&& parts[0]
.as_ident()
.is_some_and(|ident| ident.value.eq_ignore_ascii_case("concat"))
}
_ => false,
}
}
fn rewrite_function_projection(
mut function: Function,
func_name: SqlPageFunctionName,
target_col_name: String,
func_idx: usize,
) -> (Vec<SelectItem>, DelayedFunctionCall) {
let Function {
args:
FunctionArguments::List(FunctionArgumentList {
args,
duplicate_treatment: None,
..
}),
..
} = &mut function
else {
unreachable!("delayable_sqlpage_function checked the function shape")
};
let mut argument_col_names = Vec::with_capacity(args.len());
let mut replacement_items = Vec::with_capacity(args.len().max(1));
for (arg_idx, arg) in args.iter_mut().enumerate() {
let argument_col_name = format!("_sqlpage_f{func_idx}_a{arg_idx}");
argument_col_names.push(argument_col_name.clone());
replacement_items.push(SelectItem::ExprWithAlias {
expr: take_function_arg_expr(arg),
alias: Ident::with_quote('"', argument_col_name),
});
}
if replacement_items.is_empty() {
replacement_items.push(SelectItem::ExprWithAlias {
expr: Expr::value(Value::Null),
alias: Ident::with_quote('"', target_col_name.clone()),
});
}
(
replacement_items,
DelayedFunctionCall {
function: func_name,
argument_col_names,
target_col_name,
},
)
}
fn function_arg_is_expr(arg: &FunctionArg) -> bool {
matches!(
arg,
FunctionArg::Unnamed(FunctionArgExpr::Expr(_))
| FunctionArg::Named {
arg: FunctionArgExpr::Expr(_),
..
}
)
}
fn take_function_arg_expr(arg: &mut FunctionArg) -> Expr {
match arg {
FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))
| FunctionArg::Named {
arg: FunctionArgExpr::Expr(expr),
..
} => std::mem::replace(expr, Expr::value(Value::Null)),
_ => unreachable!("function_arg_is_expr was checked before taking arguments"),
}
}
+40
View File
@@ -0,0 +1,40 @@
//! Database-specific parser dialects and bind-placeholder syntax.
use sqlparser::dialect::{
Dialect, DuckDbDialect, GenericDialect, MsSqlDialect, MySqlDialect, OracleDialect,
PostgreSqlDialect, SQLiteDialect, SnowflakeDialect,
};
use sqlx::any::AnyKind;
use crate::webserver::database::SupportedDatabase;
/// Native bind-placeholder forms supported by `sqlx` database backends.
#[derive(Clone, Copy)]
pub(super) enum PlaceholderStyle {
Numbered { prefix: &'static str },
Positional { token: &'static str },
}
/// Returns the `sqlparser` dialect matching the configured database.
pub(super) fn parser_dialect(database: SupportedDatabase) -> Box<dyn Dialect> {
match database {
SupportedDatabase::Duckdb => Box::new(DuckDbDialect {}),
SupportedDatabase::Oracle => Box::new(OracleDialect {}),
SupportedDatabase::Postgres => Box::new(PostgreSqlDialect {}),
SupportedDatabase::Generic => Box::new(GenericDialect {}),
SupportedDatabase::Mssql => Box::new(MsSqlDialect {}),
SupportedDatabase::MySql => Box::new(MySqlDialect {}),
SupportedDatabase::Sqlite => Box::new(SQLiteDialect {}),
SupportedDatabase::Snowflake => Box::new(SnowflakeDialect {}),
}
}
/// Returns the native placeholder syntax emitted into rewritten SQL.
pub(super) fn placeholder_style(kind: AnyKind) -> PlaceholderStyle {
match kind {
AnyKind::Sqlite => PlaceholderStyle::Numbered { prefix: "?" },
AnyKind::Postgres => PlaceholderStyle::Numbered { prefix: "$" },
AnyKind::Mssql => PlaceholderStyle::Numbered { prefix: "@p" },
AnyKind::MySql | AnyKind::Odbc => PlaceholderStyle::Positional { token: "?" },
}
}
@@ -1,591 +0,0 @@
use super::super::{DbInfo, SupportedDatabase};
use super::{is_sqlpage_func, sqlpage_func_name};
use crate::webserver::database::sqlpage_functions::func_call_to_param;
use crate::webserver::database::syntax_tree::StmtParam;
use sqlparser::ast::{
BinaryOperator, CastKind, CharacterLength, DataType, Expr, Function, FunctionArg,
FunctionArgExpr, FunctionArgumentList, FunctionArguments, Ident, ObjectName, ObjectNamePart,
Spanned, Statement, Value, ValueWithSpan, Visit, VisitMut, Visitor, VisitorMut,
};
use sqlx::any::AnyKind;
use std::ops::ControlFlow;
pub(super) struct ParameterExtractor {
pub(super) db_info: DbInfo,
pub(super) parameters: Vec<StmtParam>,
pub(super) extract_error: Option<anyhow::Error>,
}
#[derive(Debug)]
pub(crate) enum DbPlaceHolder {
PrefixedNumber { prefix: &'static str },
Positional { placeholder: &'static str },
}
pub(crate) const DB_PLACEHOLDERS: [(AnyKind, DbPlaceHolder); 5] = [
(
AnyKind::Sqlite,
DbPlaceHolder::PrefixedNumber { prefix: "?" },
),
(
AnyKind::Postgres,
DbPlaceHolder::PrefixedNumber { prefix: "$" },
),
(
AnyKind::MySql,
DbPlaceHolder::Positional { placeholder: "?" },
),
(
AnyKind::Mssql,
DbPlaceHolder::PrefixedNumber { prefix: "@p" },
),
(
AnyKind::Odbc,
DbPlaceHolder::Positional { placeholder: "?" },
),
];
/// For positional parameters, we use a temporary placeholder during parameter extraction,
/// And then replace it with the actual placeholder during statement rewriting.
pub(crate) const TEMP_PLACEHOLDER_PREFIX: &str = "@SQLPAGE_TEMP";
fn get_placeholder_prefix(kind: AnyKind) -> &'static str {
if let Some((_, DbPlaceHolder::PrefixedNumber { prefix })) = DB_PLACEHOLDERS
.iter()
.find(|(placeholder_kind, _prefix)| *placeholder_kind == kind)
{
prefix
} else {
TEMP_PLACEHOLDER_PREFIX
}
}
impl ParameterExtractor {
pub(super) fn extract_parameters(
sql_ast: &mut Statement,
db_info: DbInfo,
) -> anyhow::Result<Vec<StmtParam>> {
let mut this = Self {
db_info,
parameters: vec![],
extract_error: None,
};
let _ = sql_ast.visit(&mut this);
if let Some(e) = this.extract_error {
return Err(e);
}
Ok(this.parameters)
}
fn replace_with_placeholder(&mut self, value: &mut Expr, param: StmtParam) {
let placeholder =
if let Some(existing_idx) = self.parameters.iter().position(|p| *p == param) {
// Parameter already exists, use its index
self.make_placeholder_for_index(existing_idx + 1)
} else {
// New parameter, add it to the list
let placeholder = self.make_placeholder();
log::trace!("Replacing {param} with {placeholder}");
self.parameters.push(param);
placeholder
};
*value = placeholder;
}
fn make_placeholder_for_index(&self, index: usize) -> Expr {
let name = make_tmp_placeholder(self.db_info.kind, index);
let data_type = match self.db_info.database_type {
SupportedDatabase::MySql => DataType::Char(None),
SupportedDatabase::Mssql => DataType::Varchar(Some(CharacterLength::Max)),
SupportedDatabase::Postgres | SupportedDatabase::Sqlite => DataType::Text,
SupportedDatabase::Oracle => DataType::Varchar(Some(CharacterLength::IntegerLength {
length: 4000,
unit: None,
})),
_ => DataType::Varchar(None),
};
let value = Expr::value(Value::Placeholder(name));
Expr::Cast {
expr: Box::new(value),
data_type,
format: None,
kind: CastKind::Cast,
array: false,
}
}
fn make_placeholder(&self) -> Expr {
self.make_placeholder_for_index(self.parameters.len() + 1)
}
pub(super) fn is_own_placeholder(&self, param: &str) -> bool {
let prefix = get_placeholder_prefix(self.db_info.kind);
if let Some(param) = param.strip_prefix(prefix)
&& let Ok(index) = param.parse::<usize>()
{
return index <= self.parameters.len() + 1;
}
false
}
}
struct InvalidFunctionFinder;
impl Visitor for InvalidFunctionFinder {
type Break = (String, Vec<FunctionArg>);
fn pre_visit_expr(&mut self, value: &Expr) -> ControlFlow<Self::Break> {
match value {
Expr::Function(Function {
name: ObjectName(func_name_parts),
args:
FunctionArguments::List(FunctionArgumentList {
args,
duplicate_treatment: None,
..
}),
..
}) if is_sqlpage_func(func_name_parts) => {
let func_name = sqlpage_func_name(func_name_parts);
let arguments = args.clone();
return ControlFlow::Break((func_name.to_string(), arguments));
}
_ => (),
}
ControlFlow::Continue(())
}
}
pub(super) fn validate_function_calls(stmt: &Statement) -> anyhow::Result<()> {
let mut finder = InvalidFunctionFinder;
if let ControlFlow::Break((func_name, mut args)) = stmt.visit(&mut finder) {
let ctx = ParamExtractContext {
parent_func: Some(func_name.clone()),
};
function_args_to_stmt_params(&mut args, &ctx)?;
let args_str = FormatArguments(&args);
let error_msg = format!(
"Invalid SQLPage function call: sqlpage.{func_name}({args_str})\n\n\
Arbitrary SQL expressions as function arguments are not supported.\n\n\
SQLPage functions can either:\n\
1. Run BEFORE the query (to provide input values)\n\
2. Run AFTER the query (to process the results)\n\
But they can't run DURING the query - the database doesn't know how to call them!\n\n\
To fix this, you can either:\n\
1. Store the function argument in a variable first:\n\
SET {func_name}_arg = ...;\n\
SET {func_name}_result = sqlpage.{func_name}(${func_name}_arg);\n\
SELECT * FROM example WHERE xxx = ${func_name}_result;\n\n\
2. Or move the function to the top level to process results:\n\
SELECT sqlpage.{func_name}(...) FROM example;"
);
Err(anyhow::anyhow!(error_msg))
} else {
Ok(())
}
}
/** This is a helper struct to format a list of arguments for an error message. */
struct FormatArguments<'a>(&'a [FunctionArg]);
impl std::fmt::Display for FormatArguments<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut args = self.0.iter();
if let Some(arg) = args.next() {
write!(f, "{arg}")?;
}
for arg in args {
write!(f, ", {arg}")?;
}
Ok(())
}
}
#[derive(Clone, Default)]
pub(crate) struct ParamExtractContext {
pub parent_func: Option<String>,
}
impl ParamExtractContext {
fn with_parent(parent: &str) -> Self {
Self {
parent_func: Some(parent.to_string()),
}
}
fn build_error(&self, e: &ExprToParamError, arguments: &[FunctionArg]) -> SqlPageFunctionError {
let line = e.line.unwrap_or(0);
let func_name = self.parent_func.as_deref().unwrap_or("unknown").to_string();
let arguments_str = FormatArguments(arguments).to_string();
let reason = match &e.kind {
ExprToParamErrorKind::UnsupportedExpr { summary } => {
format!(
"\"{summary}\" is an sql expression, which cannot be passed as a nested sqlpage function argument."
)
}
ExprToParamErrorKind::UnemulatedFunction { name } => {
format!(
"\"{name}\" is not a supported sqlpage function. Only a few basic sql functions like concat or json_object can be used inside sqlpage functions."
)
}
ExprToParamErrorKind::NamedArgs => "Named function arguments are not supported.\n\
Please use positional arguments only."
.to_string(),
};
SqlPageFunctionError {
line,
func_name,
arguments_str,
reason,
}
}
}
#[derive(Debug)]
pub(crate) struct SqlPageFunctionError {
pub line: u64,
pub func_name: String,
pub arguments_str: String,
pub reason: String,
}
impl std::fmt::Display for SqlPageFunctionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Unsupported sqlpage function argument:\n\
sqlpage.{func}({args_str})\n\n\
{reason}\n\n\
SQLPage functions can either:\n\
1. Run BEFORE the query (to provide input values)\n\
2. Run AFTER the query (to process the results)\n\
But they can't run DURING the query - the database doesn't know how to call them!\n\n\
To fix this, you can either:\n\
1. Store the function argument in a variable first:\n\
SET {func}_arg = ...;\n\
SET {func}_result = sqlpage.{func}(${func}_arg);\n\
SELECT * FROM example WHERE xxx = ${func}_result;\n\n\
2. Or move the function to the top level to process results:\n\
SELECT sqlpage.{func}(...) FROM example;",
func = self.func_name,
args_str = self.arguments_str,
reason = self.reason
)
}
}
impl std::error::Error for SqlPageFunctionError {}
#[derive(Debug)]
struct ExprToParamError {
line: Option<u64>,
kind: ExprToParamErrorKind,
}
#[derive(Debug)]
enum ExprToParamErrorKind {
UnsupportedExpr { summary: String },
UnemulatedFunction { name: String },
NamedArgs,
}
fn expr_summary(expr: &Expr) -> String {
match expr {
Expr::CompoundIdentifier(idents) => {
let s = idents
.iter()
.map(|i| i.value.as_str())
.collect::<Vec<_>>()
.join(".");
format!("column/table reference '{s}'")
}
_ => format!("{expr}"),
}
}
fn function_arg_to_stmt_param(
arg: &mut FunctionArg,
ctx: &ParamExtractContext,
) -> Result<StmtParam, ExprToParamError> {
let expr = function_arg_expr(arg).ok_or(ExprToParamError {
line: None,
kind: ExprToParamErrorKind::NamedArgs,
})?;
expr_to_stmt_param(expr, ctx)
}
pub(crate) fn function_args_to_stmt_params(
arguments: &mut [FunctionArg],
ctx: &ParamExtractContext,
) -> anyhow::Result<Vec<StmtParam>> {
let mut params = Vec::with_capacity(arguments.len());
// We iterate manually so we can pass the entire `arguments` slice to into_error on failure
for arg in arguments.iter_mut() {
match function_arg_to_stmt_param(arg, ctx) {
Ok(p) => params.push(p),
Err(e) => {
let func_err = ctx.build_error(&e, arguments);
return Err(anyhow::Error::new(func_err));
}
}
}
Ok(params)
}
fn emulated_func_args_to_param(
func_name: &str,
args: &mut [FunctionArg],
line: u64,
) -> Result<StmtParam, ExprToParamError> {
let inner = ParamExtractContext::with_parent(func_name);
if func_name.eq_ignore_ascii_case("concat") {
let mut concat_args = Vec::with_capacity(args.len());
for a in args {
concat_args.push(function_arg_to_stmt_param(a, &inner)?);
}
Ok(StmtParam::Concat(concat_args))
} else if func_name.eq_ignore_ascii_case("json_object")
|| func_name.eq_ignore_ascii_case("jsonb_object")
|| func_name.eq_ignore_ascii_case("json_build_object")
|| func_name.eq_ignore_ascii_case("jsonb_build_object")
{
let mut json_obj_args = Vec::with_capacity(args.len());
for a in args {
json_obj_args.push(function_arg_to_stmt_param(a, &inner)?);
}
Ok(StmtParam::JsonObject(json_obj_args))
} else if func_name.eq_ignore_ascii_case("json_array")
|| func_name.eq_ignore_ascii_case("jsonb_array")
|| func_name.eq_ignore_ascii_case("json_build_array")
|| func_name.eq_ignore_ascii_case("jsonb_build_array")
{
let mut json_obj_args = Vec::with_capacity(args.len());
for a in args {
json_obj_args.push(function_arg_to_stmt_param(a, &inner)?);
}
Ok(StmtParam::JsonArray(json_obj_args))
} else if func_name.eq_ignore_ascii_case("coalesce") {
let mut coalesce_args = Vec::with_capacity(args.len());
for a in args {
coalesce_args.push(function_arg_to_stmt_param(a, &inner)?);
}
Ok(StmtParam::Coalesce(coalesce_args))
} else {
Err(ExprToParamError {
line: Some(line),
kind: ExprToParamErrorKind::UnemulatedFunction {
name: func_name.to_string(),
},
})
}
}
fn expr_to_stmt_param(
arg: &mut Expr,
ctx: &ParamExtractContext,
) -> Result<StmtParam, ExprToParamError> {
let line = arg.span().start.line;
match arg {
Expr::Value(ValueWithSpan {
value: Value::Placeholder(placeholder),
..
}) => Ok(map_param(std::mem::take(placeholder))),
Expr::Identifier(ident) => extract_ident_param(ident).ok_or_else(|| ExprToParamError {
line: Some(line),
kind: ExprToParamErrorKind::UnsupportedExpr {
summary: expr_summary(arg),
},
}),
Expr::Function(Function {
name: ObjectName(func_name_parts),
args:
FunctionArguments::List(FunctionArgumentList {
args,
duplicate_treatment: None,
..
}),
..
}) if is_sqlpage_func(func_name_parts) => Ok(func_call_to_param(
sqlpage_func_name(func_name_parts),
args.as_mut_slice(),
ctx,
)),
Expr::Value(ValueWithSpan {
value: Value::SingleQuotedString(param_value),
..
}) => Ok(StmtParam::Literal(std::mem::take(param_value))),
Expr::Value(ValueWithSpan {
value: Value::Number(param_value, _is_long),
..
}) => Ok(StmtParam::Literal(param_value.clone())),
Expr::Value(ValueWithSpan {
value: Value::Null, ..
}) => Ok(StmtParam::Null),
Expr::BinaryOp {
left,
op: BinaryOperator::StringConcat,
right,
} => {
let left = expr_to_stmt_param(left, ctx)?;
let right = expr_to_stmt_param(right, ctx)?;
Ok(StmtParam::Concat(vec![left, right]))
}
Expr::Function(Function {
name: ObjectName(func_name_parts),
args:
FunctionArguments::List(FunctionArgumentList {
args,
duplicate_treatment: None,
..
}),
..
}) if func_name_parts.len() == 1 => {
let func_name = func_name_parts[0]
.as_ident()
.map(|ident| ident.value.as_str())
.unwrap_or_default();
emulated_func_args_to_param(func_name, args.as_mut_slice(), line)
}
_ => Err(ExprToParamError {
line: Some(line),
kind: ExprToParamErrorKind::UnsupportedExpr {
summary: expr_summary(arg),
},
}),
}
}
fn function_arg_expr(arg: &mut FunctionArg) -> Option<&mut Expr> {
match arg {
FunctionArg::Unnamed(FunctionArgExpr::Expr(expr)) => Some(expr),
_ => None,
}
}
#[inline]
#[must_use]
pub(super) fn make_tmp_placeholder(kind: AnyKind, arg_number: usize) -> String {
let prefix = if let Some((_, DbPlaceHolder::PrefixedNumber { prefix })) =
DB_PLACEHOLDERS.iter().find(|(db_typ, _)| *db_typ == kind)
{
prefix
} else {
TEMP_PLACEHOLDER_PREFIX
};
format!("{prefix}{arg_number}")
}
pub(super) fn extract_ident_param(Ident { value, .. }: &mut Ident) -> Option<StmtParam> {
if value.starts_with('$') || value.starts_with(':') {
let name = std::mem::take(value);
Some(map_param(name))
} else {
None
}
}
fn map_param(mut name: String) -> StmtParam {
if name.is_empty() {
return StmtParam::PostOrGet(name);
}
let prefix = name.remove(0);
match prefix {
'$' => StmtParam::PostOrGet(name),
':' => StmtParam::Post(name),
_ => StmtParam::Get(name),
}
}
impl VisitorMut for ParameterExtractor {
type Break = ();
fn pre_visit_expr(&mut self, value: &mut Expr) -> ControlFlow<Self::Break> {
match value {
Expr::Identifier(ident) => {
if let Some(param) = extract_ident_param(ident) {
self.replace_with_placeholder(value, param);
}
}
Expr::Value(ValueWithSpan {
value: Value::Placeholder(param),
..
}) if !self.is_own_placeholder(param) =>
// this check is to avoid recursively replacing placeholders in the form of '?', or '$1', '$2', which we emit ourselves
{
let name = std::mem::take(param);
self.replace_with_placeholder(value, map_param(name));
}
Expr::Function(Function {
name: ObjectName(func_name_parts),
args:
FunctionArguments::List(FunctionArgumentList {
args,
duplicate_treatment: None,
..
}),
filter: None,
null_treatment: None,
over: None,
..
}) if is_sqlpage_func(func_name_parts) => {
let func_name = sqlpage_func_name(func_name_parts);
log::trace!("Handling builtin function: {func_name}");
let arguments = std::mem::take(args);
let ctx = ParamExtractContext {
parent_func: Some(func_name.to_string()),
};
let mut arguments_clone = arguments.clone();
let param = func_call_to_param(func_name, &mut arguments_clone, &ctx);
if let StmtParam::Error(msg) = &param {
log::trace!("Skipping extraction of {func_name} due to: {msg}");
*args = arguments;
return ControlFlow::Continue(());
}
self.replace_with_placeholder(value, param);
}
// Replace 'str1' || 'str2' with CONCAT('str1', 'str2') for MSSQL
Expr::BinaryOp {
left,
op: BinaryOperator::StringConcat,
right,
} if self.db_info.database_type == SupportedDatabase::Mssql => {
let left = std::mem::replace(left.as_mut(), Expr::value(Value::Null));
let right = std::mem::replace(right.as_mut(), Expr::value(Value::Null));
*value = Expr::Function(Function {
name: ObjectName(vec![ObjectNamePart::Identifier(Ident::new("CONCAT"))]),
args: FunctionArguments::List(FunctionArgumentList {
args: vec![
FunctionArg::Unnamed(FunctionArgExpr::Expr(left)),
FunctionArg::Unnamed(FunctionArgExpr::Expr(right)),
],
duplicate_treatment: None,
clauses: Vec::new(),
}),
parameters: FunctionArguments::None,
over: None,
filter: None,
null_treatment: None,
within_group: Vec::new(),
uses_odbc_syntax: false,
});
}
Expr::Cast {
kind: kind @ CastKind::DoubleColon,
..
} if ![
SupportedDatabase::Postgres,
SupportedDatabase::Duckdb,
SupportedDatabase::Snowflake,
SupportedDatabase::Generic,
]
.contains(&self.db_info.database_type) =>
{
log::warn!(
"Casting with '::' is not supported on your database. \
For backwards compatibility with older SQLPage versions, we will transform it to CAST(... AS ...)."
);
*kind = CastKind::Cast;
}
_ => (),
}
ControlFlow::<()>::Continue(())
}
}
+899
View File
@@ -0,0 +1,899 @@
//! Rewrites parsed SQL into database SQL and SQLPage-owned expressions.
use std::ops::ControlFlow;
use std::str::FromStr as _;
use anyhow::{Context as _, anyhow};
use serde_json::Value as JsonValue;
use sqlparser::ast::{
BinaryOperator, CastKind, CharacterLength, DataType, Expr as SqlExpr, Function, FunctionArg,
FunctionArgExpr, FunctionArgumentList, FunctionArguments, Ident, ObjectName, ObjectNamePart,
OrderByKind, SelectItem, SetExpr, Spanned as _, Statement as SqlStatement, Value,
ValueWithSpan, VisitMut, VisitorMut,
};
use sqlparser::tokenizer::Span;
use super::dialect::{PlaceholderStyle, placeholder_style};
use super::statement::{
DatabaseQuery, OutputColumn, Query, QueryBody, SingleRowQuery, SourceLocation, SourceSpan,
};
use super::{extract_json_columns, is_json_expression, is_sqlpage_func};
use crate::webserver::database::sqlpage_expr::{
NoRowInput, RowExpr, RowInputId, SqlPageExpr, StandaloneExpr, VariableRef, VariableSource,
};
use crate::webserver::database::sqlpage_functions::functions::SqlPageFunctionName;
use crate::webserver::database::{DbInfo, SupportedDatabase};
const SQLPAGE_INPUT_PREFIX: &str = "__sqlpage_input_";
#[derive(Debug)]
/// A binding retained with its source position until positional bindings can
/// be ordered as they appear in the rendered SQL.
struct PendingBinding {
value: StandaloneExpr,
span: Span,
sequence: usize,
}
/// Mutable state used while rewriting one database query.
struct QueryRewriter<'a> {
database: &'a DbInfo,
bindings: Vec<PendingBinding>,
row_input_json: Vec<bool>,
private_projection: Vec<SelectItem>,
error: Option<anyhow::Error>,
}
/// Result of partitioning one projected expression.
// Keeping the owned AST inline avoids one heap allocation for every ordinary
// projected expression. The enum is short-lived inside the rewriter.
#[allow(clippy::large_enum_variant)]
enum RewrittenProjection {
Database(SqlExpr),
PerRow(RowExpr),
}
/// Defines how a SQL expression crossing into a SQLPage-owned expression is
/// represented at a particular evaluation site.
trait ExprEnvironment {
type Input;
fn use_database_expr(
rewriter: &mut QueryRewriter<'_>,
expression: SqlExpr,
) -> anyhow::Result<SqlPageExpr<Self::Input>>;
}
/// Rejects database-owned inputs because no returned row is available.
struct StandaloneEnvironment;
/// Projects database-owned inputs into the current returned row.
struct RowEnvironment;
impl ExprEnvironment for StandaloneEnvironment {
type Input = NoRowInput;
fn use_database_expr(
_rewriter: &mut QueryRewriter<'_>,
expression: SqlExpr,
) -> anyhow::Result<StandaloneExpr> {
if let SqlExpr::Function(function) = &expression
&& let [ObjectNamePart::Identifier(name)] = function.name.0.as_slice()
{
return Err(anyhow!(
"{} is not a supported sqlpage function and cannot be evaluated before the query",
name.value
));
}
Err(anyhow!(
"{expression} is a database expression, but its value is required before the query can run"
))
}
}
impl ExprEnvironment for RowEnvironment {
type Input = RowInputId;
fn use_database_expr(
rewriter: &mut QueryRewriter<'_>,
expression: SqlExpr,
) -> anyhow::Result<RowExpr> {
let id = rewriter.add_row_input(expression)?;
Ok(SqlPageExpr::Input(id))
}
}
#[derive(Clone, Copy)]
/// SQL operations whose semantics are implemented by the shared `SQLPage`
/// expression evaluator.
enum EmulatedFunction {
Concat,
Coalesce,
JsonObject,
JsonArray,
}
/// Rewrites one parsed statement into database SQL plus the `SQLPage`
/// expressions evaluated around it.
pub(super) fn rewrite_query(
mut statement: SqlStatement,
database: &DbInfo,
semicolon: bool,
) -> anyhow::Result<Query> {
let source_span = source_span(&statement);
let mut rewriter = QueryRewriter {
database,
bindings: Vec::new(),
row_input_json: Vec::new(),
private_projection: Vec::new(),
error: None,
};
if let Some(single_row) = rewrite_single_row(&mut statement, &mut rewriter)? {
return Ok(Query {
body: QueryBody::SingleRow(single_row),
source_span,
});
}
let computed_columns = rewrite_top_level_projection(&mut statement, &mut rewriter)?;
let _ = statement.visit(&mut rewriter);
if let Some(error) = rewriter.error {
return Err(error);
}
if let SqlStatement::Query(query) = &mut statement
&& let SetExpr::Select(select) = query.body.as_mut()
&& select.projection.is_empty()
{
select.projection.push(SelectItem::ExprWithAlias {
expr: SqlExpr::value(Value::Null),
alias: Ident::with_quote('"', format!("{SQLPAGE_INPUT_PREFIX}anchor")),
});
rewriter.row_input_json.push(false);
}
let json_columns = extract_json_columns(&statement, database.database_type)
.into_iter()
.filter(|name| !name.starts_with(SQLPAGE_INPUT_PREFIX))
.collect();
let bindings = rewriter.finish_bindings();
let sql = format!(
"{statement}{semicolon}",
semicolon = if semicolon { ";" } else { "" }
);
Ok(Query {
body: QueryBody::Database(DatabaseQuery {
sql,
bindings,
row_input_json: rewriter.row_input_json.into_boxed_slice(),
computed_columns: computed_columns.into_boxed_slice(),
json_columns,
}),
source_span,
})
}
/// Removes SQLPage-owned projection expressions from the database projection
/// and appends the private database inputs they require.
fn rewrite_top_level_projection(
statement: &mut SqlStatement,
rewriter: &mut QueryRewriter<'_>,
) -> anyhow::Result<Vec<OutputColumn<RowExpr>>> {
let mut computed_columns = Vec::new();
let SqlStatement::Query(query) = statement else {
return Ok(computed_columns);
};
let SetExpr::Select(select) = query.body.as_mut() else {
return Ok(computed_columns);
};
if select.distinct.is_some() && select.projection.iter().any(select_item_contains_sqlpage) {
anyhow::bail!(
"SQLPage-computed projections cannot be used with SELECT DISTINCT because DISTINCT must be evaluated by the database"
);
}
let mut database_projection = Vec::with_capacity(select.projection.len());
for item in std::mem::take(&mut select.projection) {
match item {
SelectItem::ExprWithAlias { expr, alias } => {
match rewriter.rewrite_projection(expr)? {
RewrittenProjection::Database(expr) => {
database_projection.push(SelectItem::ExprWithAlias { expr, alias });
}
RewrittenProjection::PerRow(value) => {
computed_columns.push(OutputColumn {
name: alias.value,
value,
});
}
}
}
SelectItem::UnnamedExpr(expr) => {
let name = expr.to_string();
match rewriter.rewrite_projection(expr)? {
RewrittenProjection::Database(expr) => {
database_projection.push(SelectItem::UnnamedExpr(expr));
}
RewrittenProjection::PerRow(value) => {
computed_columns.push(OutputColumn { name, value });
}
}
}
item => database_projection.push(item),
}
}
database_projection.append(&mut rewriter.private_projection);
select.projection = database_projection;
let computed_names = computed_columns
.iter()
.map(|column| column.name.as_str())
.collect::<Vec<_>>();
if let Some(order_by) = &query.order_by
&& let OrderByKind::Expressions(expressions) = &order_by.kind
&& expressions.iter().any(|ordering| {
computed_names.iter().any(|name| {
matches!(
&ordering.expr,
SqlExpr::Identifier(identifier) if identifier.value.eq_ignore_ascii_case(name)
)
})
})
{
anyhow::bail!(
"ORDER BY cannot reference a SQLPage-computed column because ordering is performed by the database"
);
}
Ok(computed_columns)
}
/// Rewrites a guaranteed one-row query directly as standalone expressions,
/// avoiding both a database round trip and an intermediate row-expression tree.
fn rewrite_single_row(
statement: &mut SqlStatement,
rewriter: &mut QueryRewriter<'_>,
) -> anyhow::Result<Option<SingleRowQuery>> {
if !has_single_row_shape(statement) {
return Ok(None);
}
let SqlStatement::Query(query) = statement else {
return Ok(None);
};
let SetExpr::Select(select) = query.body.as_mut() else {
return Ok(None);
};
for item in &select.projection {
let SelectItem::ExprWithAlias { expr, .. } = item else {
return Ok(None);
};
if !can_build_standalone(expr)? {
return Ok(None);
}
}
let mut columns = Vec::with_capacity(select.projection.len());
for item in std::mem::take(&mut select.projection) {
let SelectItem::ExprWithAlias { expr, alias } = item else {
unreachable!("projection shape was checked")
};
columns.push(OutputColumn {
name: alias.value,
value: build_sqlpage_expr::<StandaloneEnvironment>(rewriter, expr)?,
});
}
Ok(Some(SingleRowQuery {
columns: columns.into_boxed_slice(),
}))
}
fn has_single_row_shape(statement: &SqlStatement) -> bool {
let SqlStatement::Query(query) = statement else {
return false;
};
if query.with.is_some()
|| query.order_by.is_some()
|| query.limit_clause.is_some()
|| query.fetch.is_some()
|| !query.locks.is_empty()
|| query.for_clause.is_some()
|| query.settings.is_some()
|| query.format_clause.is_some()
|| !query.pipe_operators.is_empty()
{
return false;
}
let SetExpr::Select(select) = query.body.as_ref() else {
return false;
};
select.distinct.is_none()
&& select.top.is_none()
&& select.into.is_none()
&& select.from.is_empty()
&& select.lateral_views.is_empty()
&& select.selection.is_none()
&& select.group_by == sqlparser::ast::GroupByExpr::Expressions(vec![], vec![])
&& select.cluster_by.is_empty()
&& select.distribute_by.is_empty()
&& select.sort_by.is_empty()
&& select.having.is_none()
&& select.named_window.is_empty()
&& select.qualify.is_none()
&& select.prewhere.is_none()
&& select.connect_by.is_empty()
&& select.optimizer_hints.is_empty()
&& select.select_modifiers.is_none()
&& select.exclude.is_none()
}
/// Checks standalone support without consuming or cloning the AST, allowing
/// callers to select a rewrite path before moving any nodes.
fn can_build_standalone(expression: &SqlExpr) -> anyhow::Result<bool> {
match expression {
SqlExpr::Value(ValueWithSpan {
value:
Value::Boolean(_)
| Value::Number(_, _)
| Value::SingleQuotedString(_)
| Value::Null
| Value::Placeholder(_),
..
}) => Ok(true),
SqlExpr::Identifier(identifier) => Ok(variable_from_ident(identifier).is_some()),
SqlExpr::Function(function) => {
if recognize_sqlpage_function(function)?.is_none()
&& emulated_function(function).is_none()
{
return Ok(false);
}
let FunctionArguments::List(arguments) = &function.args else {
return Ok(false);
};
for argument in &arguments.args {
let FunctionArg::Unnamed(FunctionArgExpr::Expr(expression)) = argument else {
return Ok(false);
};
if !can_build_standalone(expression)? {
return Ok(false);
}
}
Ok(true)
}
SqlExpr::BinaryOp {
left,
op: BinaryOperator::StringConcat,
right,
} => Ok(can_build_standalone(left)? && can_build_standalone(right)?),
_ => Ok(false),
}
}
impl QueryRewriter<'_> {
/// Splits a projected expression at SQLPage-supported operations while
/// leaving opaque database operations in the SQL AST.
fn rewrite_projection(&mut self, expression: SqlExpr) -> anyhow::Result<RewrittenProjection> {
match expression {
SqlExpr::Function(function) => {
if recognize_sqlpage_function(&function)?.is_some() {
return build_sqlpage_expr::<RowEnvironment>(self, SqlExpr::Function(function))
.map(RewrittenProjection::PerRow);
}
if let Some(kind) = emulated_function(&function) {
return self.rewrite_emulated_projection(function, kind);
}
let mut expression = SqlExpr::Function(function);
self.rewrite_database_expression(&mut expression)?;
Ok(RewrittenProjection::Database(expression))
}
SqlExpr::BinaryOp {
left,
op: BinaryOperator::StringConcat,
right,
} => {
let left = self.rewrite_projection(*left)?;
let right = self.rewrite_projection(*right)?;
match (left, right) {
(RewrittenProjection::Database(left), RewrittenProjection::Database(right)) => {
Ok(RewrittenProjection::Database(SqlExpr::BinaryOp {
left: Box::new(left),
op: BinaryOperator::StringConcat,
right: Box::new(right),
}))
}
(left, right) => Ok(RewrittenProjection::PerRow(SqlPageExpr::Concat(
vec![
self.projection_into_row_expr(left)?,
self.projection_into_row_expr(right)?,
]
.into_boxed_slice(),
))),
}
}
mut expression => {
self.rewrite_database_expression(&mut expression)?;
Ok(RewrittenProjection::Database(expression))
}
}
}
fn rewrite_emulated_projection(
&mut self,
function: Function,
kind: EmulatedFunction,
) -> anyhow::Result<RewrittenProjection> {
let (arguments, original) = take_expression_arguments(function)?;
let mut rewritten = Vec::with_capacity(arguments.len());
let mut has_per_row = false;
for argument in arguments {
let argument = self.rewrite_projection(argument)?;
has_per_row |= matches!(argument, RewrittenProjection::PerRow(_));
rewritten.push(argument);
}
if !has_per_row {
let arguments = rewritten
.into_iter()
.map(|argument| match argument {
RewrittenProjection::Database(expression) => expression,
RewrittenProjection::PerRow(_) => unreachable!(),
})
.collect();
return Ok(RewrittenProjection::Database(rebuild_function(
original, arguments,
)));
}
let arguments = rewritten
.into_iter()
.map(|argument| self.projection_into_row_expr(argument))
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(RewrittenProjection::PerRow(build_emulated(
kind, arguments,
)?))
}
/// Converts a database-owned projection fragment into a typed row input,
/// while preserving an already per-row expression unchanged.
fn projection_into_row_expr(
&mut self,
projection: RewrittenProjection,
) -> anyhow::Result<RowExpr> {
match projection {
RewrittenProjection::Database(expression) => {
build_sqlpage_expr::<RowEnvironment>(self, expression)
}
RewrittenProjection::PerRow(expression) => Ok(expression),
}
}
fn rewrite_database_expression(&mut self, expression: &mut SqlExpr) -> anyhow::Result<()> {
let _ = expression.visit(self);
self.error.take().map_or(Ok(()), Err)
}
fn add_binding(&mut self, value: StandaloneExpr, span: Span) -> SqlExpr {
let sequence = self.bindings.len();
self.bindings.push(PendingBinding {
value,
span,
sequence,
});
let placeholder = match placeholder_style(self.database.kind) {
PlaceholderStyle::Numbered { prefix } => format!("{prefix}{}", sequence + 1),
PlaceholderStyle::Positional { token } => token.to_owned(),
};
cast_placeholder(placeholder, self.database.database_type)
}
fn add_row_input(&mut self, mut expression: SqlExpr) -> anyhow::Result<RowInputId> {
let decode_as_json = is_json_expression(&expression);
self.rewrite_database_expression(&mut expression)?;
let index = self.row_input_json.len();
let name = format!("{SQLPAGE_INPUT_PREFIX}{index}");
self.private_projection.push(SelectItem::ExprWithAlias {
expr: expression,
alias: Ident::with_quote('"', name),
});
self.row_input_json.push(decode_as_json);
Ok(RowInputId::new(index))
}
/// Finalizes bindings in database placeholder order. Positional backends
/// require lexical ordering because every placeholder is spelled `?`.
fn finish_bindings(&mut self) -> Box<[StandaloneExpr]> {
if matches!(
placeholder_style(self.database.kind),
PlaceholderStyle::Positional { .. }
) {
self.bindings.sort_by_key(|binding| {
(
binding.span.start.line,
binding.span.start.column,
binding.sequence,
)
});
}
std::mem::take(&mut self.bindings)
.into_iter()
.map(|binding| binding.value)
.collect()
}
}
impl VisitorMut for QueryRewriter<'_> {
type Break = ();
fn pre_visit_expr(&mut self, expression: &mut SqlExpr) -> ControlFlow<Self::Break> {
if self.error.is_some() {
return ControlFlow::Break(());
}
let replacement = match expression {
SqlExpr::Value(ValueWithSpan {
value: Value::Placeholder(_),
span,
}) if *span == Span::empty() => None,
SqlExpr::Value(ValueWithSpan {
value: Value::Placeholder(_),
..
})
| SqlExpr::Identifier(_) => match variable_from_expr(expression) {
Some(variable) => {
let span = expression.span();
Some(self.add_binding(SqlPageExpr::Variable(variable), span))
}
None => None,
},
SqlExpr::Function(function) => match recognize_sqlpage_function(function) {
Ok(Some(_)) => {
let span = expression.span();
let owned = std::mem::replace(expression, SqlExpr::value(Value::Null));
match build_sqlpage_expr::<StandaloneEnvironment>(self, owned) {
Ok(value) => Some(self.add_binding(value, span)),
Err(error) => {
self.error = Some(error.context(
"A SQLPage function used by the database could not be evaluated before the query",
));
None
}
}
}
Ok(None) => None,
Err(error) => {
self.error = Some(error);
None
}
},
SqlExpr::BinaryOp {
left,
op: BinaryOperator::StringConcat,
right,
} if self.database.database_type == SupportedDatabase::Mssql => {
let left = std::mem::replace(left.as_mut(), SqlExpr::value(Value::Null));
let right = std::mem::replace(right.as_mut(), SqlExpr::value(Value::Null));
Some(make_function("CONCAT", vec![left, right]))
}
SqlExpr::Cast {
kind: kind @ CastKind::DoubleColon,
..
} if ![
SupportedDatabase::Postgres,
SupportedDatabase::Duckdb,
SupportedDatabase::Snowflake,
SupportedDatabase::Generic,
]
.contains(&self.database.database_type) =>
{
*kind = CastKind::Cast;
None
}
_ => None,
};
if let Some(replacement) = replacement {
*expression = replacement;
}
ControlFlow::Continue(())
}
}
/// Consumes an AST expression into the shared `SQLPage` expression type. The
/// environment determines whether opaque database fragments are illegal or
/// become private row inputs.
fn build_sqlpage_expr<Environment: ExprEnvironment>(
rewriter: &mut QueryRewriter<'_>,
expression: SqlExpr,
) -> anyhow::Result<SqlPageExpr<Environment::Input>> {
match expression {
SqlExpr::Value(ValueWithSpan { value, .. }) => match value {
Value::Placeholder(name) => Ok(SqlPageExpr::Variable(variable_from_placeholder(name))),
Value::SingleQuotedString(text) => Ok(SqlPageExpr::Literal(JsonValue::String(text))),
Value::Number(number, _) => Ok(SqlPageExpr::Literal(JsonValue::Number(
number.parse().context("Invalid numeric SQL literal")?,
))),
Value::Boolean(value) => Ok(SqlPageExpr::Literal(JsonValue::Bool(value))),
Value::Null => Ok(SqlPageExpr::Literal(JsonValue::Null)),
_ => {
Environment::use_database_expr(rewriter, SqlExpr::Value(ValueWithSpan::from(value)))
}
},
SqlExpr::Identifier(identifier) => {
if let Some(variable) = variable_from_ident(&identifier) {
Ok(SqlPageExpr::Variable(variable))
} else {
Environment::use_database_expr(rewriter, SqlExpr::Identifier(identifier))
}
}
SqlExpr::Function(function) => {
if let Some(function_name) = recognize_sqlpage_function(&function)? {
let (arguments, _) = take_expression_arguments(function)?;
let arguments = arguments
.into_iter()
.map(|argument| build_sqlpage_expr::<Environment>(rewriter, argument))
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(SqlPageExpr::Call {
function: function_name,
arguments: arguments.into_boxed_slice(),
})
} else if let Some(kind) = emulated_function(&function) {
let (arguments, _) = take_expression_arguments(function)?;
let arguments = arguments
.into_iter()
.map(|argument| build_sqlpage_expr::<Environment>(rewriter, argument))
.collect::<anyhow::Result<Vec<_>>>()?;
build_emulated(kind, arguments)
} else {
Environment::use_database_expr(rewriter, SqlExpr::Function(function))
}
}
SqlExpr::BinaryOp {
left,
op: BinaryOperator::StringConcat,
right,
} => Ok(SqlPageExpr::Concat(
vec![
build_sqlpage_expr::<Environment>(rewriter, *left)?,
build_sqlpage_expr::<Environment>(rewriter, *right)?,
]
.into_boxed_slice(),
)),
expression => Environment::use_database_expr(rewriter, expression),
}
}
fn build_emulated<Input>(
kind: EmulatedFunction,
arguments: Vec<SqlPageExpr<Input>>,
) -> anyhow::Result<SqlPageExpr<Input>> {
Ok(match kind {
EmulatedFunction::Concat => SqlPageExpr::Concat(arguments.into_boxed_slice()),
EmulatedFunction::Coalesce => SqlPageExpr::Coalesce(arguments.into_boxed_slice()),
EmulatedFunction::JsonArray => SqlPageExpr::JsonArray(arguments.into_boxed_slice()),
EmulatedFunction::JsonObject => {
if !arguments.len().is_multiple_of(2) {
anyhow::bail!("JSON_OBJECT requires an even number of arguments");
}
let mut arguments = arguments.into_iter();
let mut entries = Vec::with_capacity(arguments.len() / 2);
while let Some(key) = arguments.next() {
let value = arguments.next().expect("argument count was checked");
entries.push((key, value));
}
SqlPageExpr::JsonObject(entries.into_boxed_slice())
}
})
}
/// Recognizes and validates an unquoted `sqlpage.<name>` call. A recognized
/// call is either rewritten or rejected and can never reach database SQL.
fn recognize_sqlpage_function(function: &Function) -> anyhow::Result<Option<SqlPageFunctionName>> {
let ObjectName(parts) = &function.name;
if !is_sqlpage_func(parts) {
return Ok(None);
}
let [
ObjectNamePart::Identifier(_),
ObjectNamePart::Identifier(name),
] = parts.as_slice()
else {
unreachable!("is_sqlpage_func checked the name")
};
if function.uses_odbc_syntax
|| !matches!(function.parameters, FunctionArguments::None)
|| function.filter.is_some()
|| function.null_treatment.is_some()
|| function.over.is_some()
|| !function.within_group.is_empty()
{
anyhow::bail!(
"Modifiers are not supported on SQLPage function {}",
function.name
);
}
let FunctionArguments::List(FunctionArgumentList {
duplicate_treatment: None,
clauses,
..
}) = &function.args
else {
anyhow::bail!(
"Unsupported argument syntax for SQLPage function {}",
function.name
);
};
if !clauses.is_empty() {
anyhow::bail!(
"Argument clauses are not supported on SQLPage function {}",
function.name
);
}
Ok(Some(SqlPageFunctionName::from_str(&name.value)?))
}
fn emulated_function(function: &Function) -> Option<EmulatedFunction> {
let [ObjectNamePart::Identifier(name)] = function.name.0.as_slice() else {
return None;
};
if !matches!(function.parameters, FunctionArguments::None)
|| function.filter.is_some()
|| function.null_treatment.is_some()
|| function.over.is_some()
|| !function.within_group.is_empty()
{
return None;
}
match name.value.to_ascii_lowercase().as_str() {
"concat" => Some(EmulatedFunction::Concat),
"coalesce" => Some(EmulatedFunction::Coalesce),
"json_object" | "jsonb_object" | "json_build_object" | "jsonb_build_object" => {
Some(EmulatedFunction::JsonObject)
}
"json_array" | "jsonb_array" | "json_build_array" | "jsonb_build_array" => {
Some(EmulatedFunction::JsonArray)
}
_ => None,
}
}
/// Moves expression arguments out of a function while retaining its emptied
/// AST shell so database-owned functions can be rebuilt without cloning.
fn take_expression_arguments(mut function: Function) -> anyhow::Result<(Vec<SqlExpr>, Function)> {
let FunctionArguments::List(arguments) = &mut function.args else {
anyhow::bail!("Unsupported arguments to {}", function.name);
};
if arguments.duplicate_treatment.is_some() || !arguments.clauses.is_empty() {
anyhow::bail!("Unsupported arguments to {}", function.name);
}
let arguments = std::mem::take(&mut arguments.args)
.into_iter()
.map(|argument| match argument {
FunctionArg::Unnamed(FunctionArgExpr::Expr(expression)) => Ok(expression),
_ => Err(anyhow!(
"Named and wildcard function arguments are not supported"
)),
})
.collect::<anyhow::Result<Vec<_>>>()?;
Ok((arguments, function))
}
fn rebuild_function(mut function: Function, expressions: Vec<SqlExpr>) -> SqlExpr {
let FunctionArguments::List(arguments) = &mut function.args else {
unreachable!()
};
arguments.args = expressions
.into_iter()
.map(|expression| FunctionArg::Unnamed(FunctionArgExpr::Expr(expression)))
.collect();
SqlExpr::Function(function)
}
fn make_function(name: &str, expressions: Vec<SqlExpr>) -> SqlExpr {
SqlExpr::Function(Function {
name: ObjectName(vec![ObjectNamePart::Identifier(Ident::new(name))]),
args: FunctionArguments::List(FunctionArgumentList {
args: expressions
.into_iter()
.map(|expression| FunctionArg::Unnamed(FunctionArgExpr::Expr(expression)))
.collect(),
duplicate_treatment: None,
clauses: Vec::new(),
}),
parameters: FunctionArguments::None,
over: None,
filter: None,
null_treatment: None,
within_group: Vec::new(),
uses_odbc_syntax: false,
})
}
fn variable_from_expr(expression: &SqlExpr) -> Option<VariableRef> {
match expression {
SqlExpr::Value(ValueWithSpan {
value: Value::Placeholder(name),
..
}) => Some(variable_from_placeholder(name.clone())),
SqlExpr::Identifier(identifier) => variable_from_ident(identifier),
_ => None,
}
}
fn variable_from_ident(identifier: &Ident) -> Option<VariableRef> {
if identifier.quote_style.is_some() {
return None;
}
let prefix = identifier.value.chars().next()?;
matches!(prefix, '$' | ':' | '?').then(|| VariableRef {
name: identifier.value[prefix.len_utf8()..].to_owned(),
source: variable_source(prefix),
})
}
fn variable_from_placeholder(mut name: String) -> VariableRef {
let prefix = name.remove(0);
VariableRef {
name,
source: variable_source(prefix),
}
}
fn variable_source(prefix: char) -> VariableSource {
match prefix {
'$' => VariableSource::SetOrUrl,
':' => VariableSource::SetOrForm,
_ => VariableSource::Url,
}
}
/// Wraps a generated placeholder in the backend-specific text cast expected
/// by `SQLPage`'s string-valued binding interface.
fn cast_placeholder(placeholder: String, database: SupportedDatabase) -> SqlExpr {
let data_type = match database {
SupportedDatabase::MySql => DataType::Char(None),
SupportedDatabase::Mssql => DataType::Varchar(Some(CharacterLength::Max)),
SupportedDatabase::Postgres | SupportedDatabase::Sqlite => DataType::Text,
SupportedDatabase::Oracle => DataType::Varchar(Some(CharacterLength::IntegerLength {
length: 4000,
unit: None,
})),
_ => DataType::Varchar(None),
};
SqlExpr::Cast {
expr: Box::new(SqlExpr::value(Value::Placeholder(placeholder))),
data_type,
format: None,
kind: CastKind::Cast,
array: false,
}
}
fn source_span(value: &impl sqlparser::ast::Spanned) -> SourceSpan {
let span = value.span();
SourceSpan {
start: SourceLocation {
line: usize::try_from(span.start.line).unwrap_or(0),
column: usize::try_from(span.start.column).unwrap_or(0),
},
end: SourceLocation {
line: usize::try_from(span.end.line).unwrap_or(0),
column: usize::try_from(span.end.column).unwrap_or(0),
},
}
}
fn select_item_contains_sqlpage(item: &SelectItem) -> bool {
struct Finder(bool);
impl sqlparser::ast::Visitor for Finder {
type Break = ();
fn pre_visit_expr(&mut self, expression: &SqlExpr) -> ControlFlow<Self::Break> {
if let SqlExpr::Function(function) = expression
&& is_sqlpage_func(&function.name.0)
{
self.0 = true;
return ControlFlow::Break(());
}
ControlFlow::Continue(())
}
}
let mut finder = Finder(false);
let _ = sqlparser::ast::Visit::visit(item, &mut finder);
finder.0
}
+95
View File
@@ -0,0 +1,95 @@
//! Immutable SQL-file statements consumed by the executor.
use std::path::PathBuf;
use super::super::csv_import::CsvImport;
use super::super::sqlpage_expr::{RowExpr, StandaloneExpr};
use super::super::sqlpage_functions::functions::SqlPageFunctionName;
/// A parsed and rewritten SQL file ready for repeated execution.
#[derive(Default)]
pub struct SqlFile {
pub(in crate::webserver::database) statements: Box<[FileStatement]>,
pub source_path: PathBuf,
}
/// One statement in a SQL file.
#[derive(Debug)]
pub(in crate::webserver::database) enum FileStatement {
Query(Query),
SetVariable { target: VariableName, value: Query },
CsvImport(CsvImport),
Error(anyhow::Error),
}
/// A query and its original source location.
#[derive(Debug, PartialEq)]
pub(in crate::webserver::database) struct Query {
pub body: QueryBody,
pub source_span: SourceSpan,
}
/// The legal ways `SQLPage` obtains rows.
///
/// Keeping output expressions inside each variant prevents a synthetic row
/// from containing an expression that requires a database row input.
#[derive(Debug, PartialEq)]
pub(in crate::webserver::database) enum QueryBody {
Database(DatabaseQuery),
SingleRow(SingleRowQuery),
}
/// A statement executed by the configured database.
#[derive(Debug, PartialEq)]
pub(in crate::webserver::database) struct DatabaseQuery {
pub sql: String,
/// Evaluated once, in placeholder order, before executing `sql`.
pub bindings: Box<[StandaloneExpr]>,
/// JSON decoding flags for private columns appended to the projection.
/// The slice length is the private-column count.
pub row_input_json: Box<[bool]>,
/// Evaluated once for every returned database row.
pub computed_columns: Box<[OutputColumn<RowExpr>]>,
pub json_columns: Box<[String]>,
}
impl DatabaseQuery {
/// Whether row evaluation needs the request's existing connection and
/// must therefore wait until the database stream is closed.
pub fn must_buffer_rows(&self) -> bool {
self.computed_columns
.iter()
.any(|column| column.value.contains_function(SqlPageFunctionName::run_sql))
}
}
/// Exactly one row generated without querying the database.
#[derive(Debug, PartialEq)]
pub(in crate::webserver::database) struct SingleRowQuery {
pub columns: Box<[OutputColumn<StandaloneExpr>]>,
}
/// A named SQLPage-owned output expression.
#[derive(Debug, PartialEq, Eq)]
pub(in crate::webserver::database) struct OutputColumn<Expr> {
pub name: String,
pub value: Expr,
}
/// A validated variable name used as the target of a `SET` statement.
#[derive(Debug, PartialEq, Eq)]
pub(in crate::webserver::database) struct VariableName(pub String);
/// A location in the SQL source.
#[derive(Debug, PartialEq, Clone, Copy)]
pub(in crate::webserver::database) struct SourceSpan {
pub start: SourceLocation,
pub end: SourceLocation,
}
/// A line and column in the SQL source.
#[derive(Debug, PartialEq, Clone, Copy)]
pub(in crate::webserver::database) struct SourceLocation {
pub line: usize,
pub column: usize,
}
+40
View File
@@ -9,6 +9,7 @@ use sqlx::postgres::types::PgRange;
use sqlx::{Column, Row, TypeInfo, ValueRef};
use sqlx::{Decode, Type};
#[cfg(test)]
pub fn row_to_json(row: &AnyRow) -> Value {
use Value::Object;
@@ -22,6 +23,32 @@ pub fn row_to_json(row: &AnyRow) -> Value {
Object(map)
}
/// Decodes a row into user-visible columns and a private trailing input suffix.
///
/// Every SQL value is decoded exactly once. Private values are addressed by
/// ordinal, so their generated SQL aliases cannot collide with user columns.
pub fn row_to_json_with_inputs(
row: &AnyRow,
input_count: usize,
) -> anyhow::Result<(Value, Vec<Value>)> {
let columns = row.columns();
let public_count = columns
.len()
.checked_sub(input_count)
.ok_or_else(|| anyhow::anyhow!("The query returned fewer columns than SQLPage expected"))?;
let mut map = Map::new();
let mut inputs = Vec::with_capacity(input_count);
for column in &columns[..public_count] {
let key = canonical_col_name(column);
let value = sql_to_json(row, column);
map = add_value_to_map(map, (key, value));
}
for column in &columns[public_count..] {
inputs.push(sql_to_json(row, column));
}
Ok((Value::Object(map), inputs))
}
fn canonical_col_name(col: &AnyColumn) -> String {
// Some databases fold all unquoted identifiers to uppercase but SQLPage uses lowercase property names
if matches!(col.type_info().0, AnyTypeInfoKind::Odbc(_))
@@ -206,6 +233,19 @@ mod tests {
Ok(())
}
#[actix_web::test]
async fn private_inputs_are_split_by_ordinal() -> anyhow::Result<()> {
let db_url = test_database_url();
let mut connection = sqlx::AnyConnection::connect(&db_url).await?;
let row = sqlx::query("SELECT 1 AS __sqlpage_input_0, 2 AS __sqlpage_input_0")
.fetch_one(&mut connection)
.await?;
let (public, inputs) = row_to_json_with_inputs(&row, 1)?;
expect_json_object_equal(&public, &serde_json::json!({ "__sqlpage_input_0": 1 }));
assert_eq!(inputs, [serde_json::json!(2)]);
Ok(())
}
#[actix_web::test]
async fn test_postgres_types() -> anyhow::Result<()> {
let Some(db_url) = db_specific_test("postgres") else {
+319
View File
@@ -0,0 +1,319 @@
//! Expressions evaluated by `SQLPage` instead of the database.
//!
//! The input type records whether an expression may read values from a
//! returned database row. The expression implementation and evaluator remain
//! shared between standalone and per-row expressions.
use std::borrow::Cow;
use anyhow::Context as _;
use serde_json::Value;
use super::execute_queries::DbConn;
use super::sqlpage_functions::functions::SqlPageFunctionName;
use crate::webserver::http_request_info::ExecutionContext;
use crate::webserver::single_or_vec::SingleOrVec;
/// An expression evaluated by `SQLPage`.
///
/// Nodes are not shared automatically because function calls may be
/// effectful. `Input` identifies values supplied by the evaluation site.
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum SqlPageExpr<Input> {
Literal(Value),
Variable(VariableRef),
Input(Input),
Call {
function: SqlPageFunctionName,
arguments: Box<[Self]>,
},
Concat(Box<[Self]>),
Coalesce(Box<[Self]>),
JsonObject(Box<[(Self, Self)]>),
JsonArray(Box<[Self]>),
}
/// Uninhabited input type for expressions that cannot read a database row.
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum NoRowInput {}
/// Identifies one private value projected by the database for `SQLPage`.
///
/// The index is private and this type is intentionally not `Clone` or `Copy`.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct RowInputId(usize);
impl RowInputId {
pub(super) fn new(index: usize) -> Self {
Self(index)
}
pub(super) fn index(&self) -> usize {
self.0
}
}
/// An expression that can be evaluated without a returned database row.
pub(crate) type StandaloneExpr = SqlPageExpr<NoRowInput>;
/// An expression evaluated once for each returned database row.
pub(crate) type RowExpr = SqlPageExpr<RowInputId>;
#[derive(Debug, PartialEq, Eq)]
/// A reference to request or previously assigned `SQLPage` state.
pub(crate) struct VariableRef {
pub name: String,
pub source: VariableSource,
}
#[derive(Debug, PartialEq, Eq)]
/// Lookup precedence implied by `SQLPage`'s three variable syntaxes.
pub(crate) enum VariableSource {
/// Read only from URL parameters (`?name`).
Url,
/// Prefer a `SET` value, then read a URL parameter (`$name`).
SetOrUrl,
/// Prefer a `SET` value, then read a form field (`:name`).
SetOrForm,
}
/// A possibly borrowed value produced by `SQLPage` expression evaluation.
pub(crate) enum SqlPageValue<'a> {
Null,
Text(Cow<'a, str>),
/// A number, boolean, array, or object.
Json(Cow<'a, Value>),
}
impl<'a> SqlPageValue<'a> {
pub(crate) fn into_function_argument(self) -> Option<Cow<'a, str>> {
match self {
Self::Null => None,
Self::Text(text) => Some(text),
Self::Json(value) => Some(Cow::Owned(value.to_string())),
}
}
pub(crate) fn into_json(self) -> Value {
match self {
Self::Null => Value::Null,
Self::Text(Cow::Borrowed(text)) => Value::String(text.to_owned()),
Self::Text(Cow::Owned(text)) => Value::String(text),
Self::Json(Cow::Borrowed(value)) => value.clone(),
Self::Json(Cow::Owned(value)) => value,
}
}
}
/// Supplies external inputs to a `SQLPage` expression.
pub(crate) trait ExprInputs<Input> {
fn take(&mut self, input: &Input) -> anyhow::Result<SqlPageValue<'static>>;
}
/// Input provider for standalone expressions.
pub(crate) struct NoInputs;
impl ExprInputs<NoRowInput> for NoInputs {
fn take(&mut self, input: &NoRowInput) -> anyhow::Result<SqlPageValue<'static>> {
match *input {}
}
}
/// Private values decoded from one returned database row.
pub(crate) struct RowInputs(Vec<Option<Value>>);
impl RowInputs {
pub(crate) fn new(values: Vec<Value>) -> Self {
Self(values.into_iter().map(Some).collect())
}
}
impl ExprInputs<RowInputId> for RowInputs {
fn take(&mut self, input: &RowInputId) -> anyhow::Result<SqlPageValue<'static>> {
let value = self
.0
.get_mut(input.index())
.and_then(Option::take)
.with_context(|| {
format!(
"Row input {} is missing or was already consumed",
input.index()
)
})?;
Ok(value_to_sqlpage_value(value))
}
}
fn value_to_sqlpage_value(value: Value) -> SqlPageValue<'static> {
match value {
Value::Null => SqlPageValue::Null,
Value::String(text) => SqlPageValue::Text(Cow::Owned(text)),
value => SqlPageValue::Json(Cow::Owned(value)),
}
}
impl VariableRef {
fn evaluate<'a>(&self, request: &'a ExecutionContext) -> SqlPageValue<'a> {
let value = match self.source {
VariableSource::Url => request
.url_params
.get(&self.name)
.map(SingleOrVec::as_json_str),
VariableSource::SetOrForm => {
if let Some(value) = request.set_variables.borrow().get(&self.name) {
return value.as_ref().map_or(SqlPageValue::Null, |value| {
SqlPageValue::Text(Cow::Owned(value.as_json_str().into_owned()))
});
}
request
.post_variables
.get(&self.name)
.map(SingleOrVec::as_json_str)
}
VariableSource::SetOrUrl => {
if let Some(value) = request.set_variables.borrow().get(&self.name) {
return value.as_ref().map_or(SqlPageValue::Null, |value| {
SqlPageValue::Text(Cow::Owned(value.as_json_str().into_owned()))
});
}
let url_value = request.url_params.get(&self.name);
if request.post_variables.contains_key(&self.name) {
if url_value.is_some() {
log::warn!(
"Deprecation warning! There is both a URL parameter named '{}' and a form field named '{}'. SQLPage is using the URL parameter for ${}. Please use :{} to reference the form field explicitly.",
self.name,
self.name,
self.name,
self.name,
);
} else {
log::warn!(
"Deprecation warning! ${} was used to reference a form field value (a POST variable). This now uses only URL parameters. Please use :{} instead.",
self.name,
self.name,
);
}
}
url_value.map(SingleOrVec::as_json_str)
}
};
value.map_or(SqlPageValue::Null, SqlPageValue::Text)
}
}
impl<Input> SqlPageExpr<Input> {
/// Evaluates this expression from left to right.
pub(crate) async fn evaluate<'a>(
&'a self,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
inputs: &mut impl ExprInputs<Input>,
) -> anyhow::Result<SqlPageValue<'a>> {
match self {
Self::Literal(value) => Ok(match value {
Value::Null => SqlPageValue::Null,
Value::String(text) => SqlPageValue::Text(Cow::Borrowed(text)),
value => SqlPageValue::Json(Cow::Borrowed(value)),
}),
Self::Variable(variable) => Ok(variable.evaluate(request)),
Self::Input(input) => inputs.take(input).map(SqlPageValue::into_lifetime),
Self::Call {
function,
arguments,
} => {
let mut values = Vec::with_capacity(arguments.len());
for argument in arguments {
values.push(
Box::pin(argument.evaluate(request, db_connection, inputs))
.await?
.into_function_argument(),
);
}
let result = function
.evaluate(request, db_connection, values)
.await
.with_context(|| format!("Error in function call {function}"))?;
Ok(result.map_or(SqlPageValue::Null, SqlPageValue::Text))
}
Self::Concat(arguments) => {
let mut result = String::new();
for argument in arguments {
let value = Box::pin(argument.evaluate(request, db_connection, inputs)).await?;
let Some(value) = value.into_function_argument() else {
return Ok(SqlPageValue::Null);
};
result.push_str(&value);
}
Ok(SqlPageValue::Text(Cow::Owned(result)))
}
Self::Coalesce(arguments) => {
for argument in arguments {
let value = Box::pin(argument.evaluate(request, db_connection, inputs)).await?;
if !matches!(value, SqlPageValue::Null) {
return Ok(value);
}
}
Ok(SqlPageValue::Null)
}
Self::JsonObject(entries) => {
let mut object = serde_json::Map::with_capacity(entries.len());
for (key, value) in entries {
let key = Box::pin(key.evaluate(request, db_connection, inputs))
.await?
.into_function_argument()
.context("JSON object keys cannot be NULL")?
.into_owned();
let value = Box::pin(value.evaluate(request, db_connection, inputs))
.await?
.into_json();
object.insert(key, value);
}
Ok(SqlPageValue::Json(Cow::Owned(Value::Object(object))))
}
Self::JsonArray(elements) => {
let mut array = Vec::with_capacity(elements.len());
for element in elements {
array.push(
Box::pin(element.evaluate(request, db_connection, inputs))
.await?
.into_json(),
);
}
Ok(SqlPageValue::Json(Cow::Owned(Value::Array(array))))
}
}
}
pub(crate) fn contains_function(&self, expected: SqlPageFunctionName) -> bool {
match self {
Self::Call {
function,
arguments,
} => {
*function == expected
|| arguments
.iter()
.any(|argument| argument.contains_function(expected))
}
Self::Concat(arguments) | Self::Coalesce(arguments) | Self::JsonArray(arguments) => {
arguments
.iter()
.any(|argument| argument.contains_function(expected))
}
Self::JsonObject(entries) => entries.iter().any(|(key, value)| {
key.contains_function(expected) || value.contains_function(expected)
}),
Self::Literal(_) | Self::Variable(_) | Self::Input(_) => false,
}
}
}
impl SqlPageValue<'static> {
fn into_lifetime<'a>(self) -> SqlPageValue<'a> {
match self {
Self::Null => SqlPageValue::Null,
Self::Text(text) => SqlPageValue::Text(text),
Self::Json(value) => SqlPageValue::Json(value),
}
}
}
@@ -57,7 +57,7 @@ impl ::std::str::FromStr for SqlPageFunctionName {
SqlPageFunctionName::ALL
.iter()
.copied()
.find(|function| function.name() == name)
.find(|function| function.name().eq_ignore_ascii_case(name))
.ok_or_else(|| {
anyhow::anyhow!(
"Unknown function {name:?}. Supported functions:\n{}",
@@ -2,20 +2,3 @@ mod function_traits;
pub(super) mod functions;
mod http_fetch_request;
mod url_parameters;
use sqlparser::ast::FunctionArg;
use super::sql::ParamExtractContext;
use super::syntax_tree::SqlPageFunctionCall;
use super::syntax_tree::StmtParam;
pub(super) fn func_call_to_param(
func_name: &str,
arguments: &mut [FunctionArg],
ctx: &ParamExtractContext,
) -> StmtParam {
SqlPageFunctionCall::from_func_call(func_name, arguments, ctx).map_or_else(
|e| StmtParam::Error(format!("{e:#}")),
StmtParam::FunctionCall,
)
}
-321
View File
@@ -1,321 +0,0 @@
/// This module contains the syntax tree for sqlpage statement parameters.
/// In a query like `SELECT sqlpage.some_function($my_param)`,
/// The stored database statement will be just `SELECT $1`,
/// and the `StmtParam` will contain a the following tree:
///
/// ```text
/// StmtParam::FunctionCall(
/// SqlPageFunctionCall {
/// function: SqlPageFunctionName::some_function,
/// arguments: vec![StmtParam::Get("$my_param")]
/// }
/// )
/// ```
use std::borrow::Cow;
use std::str::FromStr;
use sqlparser::ast::FunctionArg;
use crate::webserver::http_request_info::ExecutionContext;
use crate::webserver::single_or_vec::SingleOrVec;
use super::{
execute_queries::DbConn, sql::ParamExtractContext, sql::function_args_to_stmt_params,
sqlpage_functions::functions::SqlPageFunctionName,
};
use anyhow::Context as _;
/// Represents a parameter to a SQL statement.
/// Objects of this type are created during SQL parsing.
/// Every time a SQL statement is executed, the parameters are evaluated to produce the actual values that are passed to the database.
/// Parameter evaluation can involve asynchronous operations, and extracting values from the request.
#[derive(Debug, PartialEq, Eq, Clone)]
pub(crate) enum StmtParam {
Get(String),
Post(String),
PostOrGet(String),
Error(String),
Literal(String),
Null,
Concat(Vec<StmtParam>),
Coalesce(Vec<StmtParam>),
JsonObject(Vec<StmtParam>),
JsonArray(Vec<StmtParam>),
FunctionCall(SqlPageFunctionCall),
}
impl std::fmt::Display for StmtParam {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StmtParam::Get(name) => write!(f, "?{name}"),
StmtParam::Post(name) => write!(f, ":{name}"),
StmtParam::PostOrGet(name) => write!(f, "${name}"),
StmtParam::Literal(x) => write!(f, "'{}'", x.replace('\'', "''")),
StmtParam::Null => write!(f, "NULL"),
StmtParam::Concat(items) => {
write!(f, "CONCAT(")?;
for item in items {
write!(f, "{item}, ")?;
}
write!(f, ")")
}
StmtParam::Coalesce(items) => {
write!(f, "COALESCE(")?;
for item in items {
write!(f, "{item}, ")?;
}
write!(f, ")")
}
StmtParam::JsonObject(items) => {
write!(f, "JSON_OBJECT(")?;
for item in items {
write!(f, "{item}, ")?;
}
write!(f, ")")
}
StmtParam::JsonArray(items) => {
write!(f, "JSON_ARRAY(")?;
for item in items {
write!(f, "{item}, ")?;
}
write!(f, ")")
}
StmtParam::FunctionCall(call) => write!(f, "{call}"),
StmtParam::Error(x) => {
if let Some((i, _)) = x.char_indices().nth(21) {
write!(f, "## {}... ##", &x[..i])
} else {
write!(f, "## {x} ##")
}
}
}
}
}
/// Represents a call to a `sqlpage.` function.
/// Objects of this type are created during SQL parsing and used to evaluate the function at runtime.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SqlPageFunctionCall {
pub function: SqlPageFunctionName,
pub arguments: Vec<StmtParam>,
}
impl SqlPageFunctionCall {
pub fn from_func_call(
func_name: &str,
arguments: &mut [FunctionArg],
ctx: &ParamExtractContext,
) -> anyhow::Result<Self> {
let function = SqlPageFunctionName::from_str(func_name)?;
let arguments = function_args_to_stmt_params(arguments, ctx)?;
Ok(Self {
function,
arguments,
})
}
pub async fn evaluate<'a>(
&self,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
let mut params = Vec::with_capacity(self.arguments.len());
for param in &self.arguments {
params.push(Box::pin(extract_req_param(param, request, db_connection)).await?);
}
log::trace!("Starting function call to {self}");
let result = self
.function
.evaluate(request, db_connection, params)
.await?;
log::trace!(
"Function call to {self} returned: {}",
result.as_deref().unwrap_or("NULL")
);
Ok(result)
}
}
impl std::fmt::Display for SqlPageFunctionCall {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}(", self.function)?;
// interleave the arguments with commas
let mut it = self.arguments.iter();
if let Some(x) = it.next() {
write!(f, "{x}")?;
}
for x in it {
write!(f, ", {x}")?;
}
write!(f, ")")
}
}
/// Extracts the value of a parameter from the request.
/// Returns `Ok(None)` when NULL should be used as the parameter value.
pub(super) async fn extract_req_param<'a>(
param: &StmtParam,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
Ok(match param {
// sync functions
StmtParam::Get(x) => request.url_params.get(x).map(SingleOrVec::as_json_str),
StmtParam::Post(x) => {
if let Some(val) = request.set_variables.borrow().get(x) {
val.as_ref()
.map(|v| Cow::Owned(v.as_json_str().into_owned()))
} else {
request.post_variables.get(x).map(SingleOrVec::as_json_str)
}
}
StmtParam::PostOrGet(x) => {
if let Some(val) = request.set_variables.borrow().get(x) {
val.as_ref()
.map(|v| Cow::Owned(v.as_json_str().into_owned()))
} else {
let url_val = request.url_params.get(x);
if request.post_variables.contains_key(x) {
if url_val.is_some() {
log::warn!(
"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). \
This now uses only URL parameters. Please use :{x} instead."
);
}
}
url_val.map(SingleOrVec::as_json_str)
}
}
StmtParam::Error(x) => anyhow::bail!("{x}"),
StmtParam::Literal(x) => Some(Cow::Owned(x.clone())),
StmtParam::Null => None,
StmtParam::Concat(args) => concat_params(&args[..], request, db_connection).await?,
StmtParam::JsonObject(args) => {
json_object_params(&args[..], request, db_connection).await?
}
StmtParam::JsonArray(args) => json_array_params(&args[..], request, db_connection).await?,
StmtParam::Coalesce(args) => coalesce_params(&args[..], request, db_connection).await?,
StmtParam::FunctionCall(func) => {
func.evaluate(request, db_connection)
.await
.with_context(|| {
format!(
"Error in function call {func}.\nExpected {:#}",
func.function
)
})?
}
})
}
async fn concat_params<'a>(
args: &[StmtParam],
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
let mut result = String::new();
for arg in args {
let Some(arg) = Box::pin(extract_req_param(arg, request, db_connection)).await? else {
return Ok(None);
};
result.push_str(&arg);
}
Ok(Some(Cow::Owned(result)))
}
async fn coalesce_params<'a>(
args: &[StmtParam],
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
for arg in args {
if let Some(arg) = Box::pin(extract_req_param(arg, request, db_connection)).await? {
return Ok(Some(arg));
}
}
Ok(None)
}
async fn json_object_params<'a>(
args: &[StmtParam],
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
use serde::{Serializer, ser::SerializeMap};
let mut result = Vec::new();
let mut ser = serde_json::Serializer::new(&mut result);
let mut map_ser = ser.serialize_map(Some(args.len()))?;
let mut it = args.iter();
while let Some(key) = it.next() {
let key = Box::pin(extract_req_param(key, request, db_connection)).await?;
map_ser.serialize_key(&key)?;
let val = it
.next()
.ok_or_else(|| anyhow::anyhow!("Odd number of arguments in JSON_OBJECT"))?;
match val {
StmtParam::JsonObject(args) => {
let raw_json = Box::pin(json_object_params(args, request, db_connection)).await?;
let obj = cow_to_raw_json(raw_json.as_ref());
map_ser.serialize_value(&obj)?;
}
StmtParam::JsonArray(args) => {
let raw_json = Box::pin(json_array_params(args, request, db_connection)).await?;
let obj = cow_to_raw_json(raw_json.as_ref());
map_ser.serialize_value(&obj)?;
}
val => {
let evaluated = Box::pin(extract_req_param(val, request, db_connection)).await?;
map_ser.serialize_value(&evaluated)?;
}
}
}
map_ser.end()?;
Ok(Some(Cow::Owned(String::from_utf8(result)?)))
}
async fn json_array_params<'a>(
args: &[StmtParam],
request: &'a ExecutionContext,
db_connection: &mut DbConn,
) -> anyhow::Result<Option<Cow<'a, str>>> {
use serde::{Serializer, ser::SerializeSeq};
let mut result = Vec::new();
let mut ser = serde_json::Serializer::new(&mut result);
let mut seq_ser = ser.serialize_seq(Some(args.len()))?;
for element in args {
match element {
StmtParam::JsonObject(args) => {
let raw_json = json_object_params(args, request, db_connection).await?;
let obj = cow_to_raw_json(raw_json.as_ref());
seq_ser.serialize_element(&obj)?;
}
StmtParam::JsonArray(args) => {
let raw_json = Box::pin(json_array_params(args, request, db_connection)).await?;
let obj = cow_to_raw_json(raw_json.as_ref());
seq_ser.serialize_element(&obj)?;
}
element => {
let evaluated =
Box::pin(extract_req_param(element, request, db_connection)).await?;
seq_ser.serialize_element(&evaluated)?;
}
}
}
seq_ser.end()?;
Ok(Some(Cow::Owned(String::from_utf8(result)?)))
}
fn cow_to_raw_json<'a>(
raw_json: Option<&'a impl AsRef<str>>,
) -> Option<&'a serde_json::value::RawValue> {
raw_json
.map(AsRef::as_ref)
.map(serde_json::from_str::<&'a serde_json::value::RawValue>)
.map(Result::unwrap)
}
+2 -2
View File
@@ -9,7 +9,7 @@ use crate::webserver::database::execute_queries::stop_at_first_error;
use crate::webserver::database::{DbItem, execute_queries::stream_query_results_with_conn};
use crate::webserver::http_request_info::extract_request_info;
use crate::webserver::server_timing::ServerTiming;
use crate::{AppConfig, AppState, DEFAULT_404_FILE, ParsedSqlFile};
use crate::{AppConfig, AppState, DEFAULT_404_FILE, SqlFile};
use actix_web::dev::{ServiceFactory, ServiceRequest, fn_service};
use actix_web::error::{ErrorBadRequest, ErrorInternalServerError};
use actix_web::http::header::Accept;
@@ -209,7 +209,7 @@ enum ResponseWithWriter<S> {
async fn render_sql(
srv_req: &mut ServiceRequest,
sql_file: Arc<ParsedSqlFile>,
sql_file: Arc<SqlFile>,
server_timing: ServerTiming,
) -> actix_web::Result<HttpResponse> {
let app_state = srv_req
+3 -3
View File
@@ -77,7 +77,7 @@
//! ```
use crate::filesystem::{FileAccess, FileSystem};
use crate::webserver::database::ParsedSqlFile;
use crate::webserver::database::SqlFile;
use crate::{AppState, file_cache::FileCache};
use RoutingAction::{CustomNotFound, Execute, NotFound, Redirect, Serve};
use awc::http::uri::PathAndQuery;
@@ -109,14 +109,14 @@ pub trait RoutingConfig {
}
pub(crate) struct AppFileStore<'a> {
cache: &'a FileCache<ParsedSqlFile>,
cache: &'a FileCache<SqlFile>,
filesystem: &'a FileSystem,
app_state: &'a AppState,
}
impl<'a> AppFileStore<'a> {
pub fn new(
cache: &'a FileCache<ParsedSqlFile>,
cache: &'a FileCache<SqlFile>,
filesystem: &'a FileSystem,
app_state: &'a AppState,
) -> Self {
@@ -1 +1,9 @@
select 'It%20works%20%21' as expected, sqlpage.url_encode(sqlpage.read_file_as_text('tests/it_works.txt')) as actual;
select 'It%20works%20%21' as expected,
sqlpage.url_encode(sqlpage.read_file_as_text(path)) as actual
from (select 'tests/it_works.txt' as path) paths;
select '%2Fvalue' as expected,
coalesce(sqlpage.url_encode('/' || value), '') as actual
from (select 'value' as value) value_rows;