sqlpage 0.45.0

Build data user interfaces entirely in SQL. A web server that takes .sql files and formats the query result using pre-made configurable professional-looking components.
use std::borrow::Cow;

use anyhow::Context;
use futures_util::StreamExt;
use tracing::Instrument;

use crate::{
    filesystem::FileAccess,
    webserver::{
        database::execute_queries::DbConn, http_request_info::ExecutionContext,
        request_variables::SetVariablesMap,
    },
};

pub(super) async fn run_sql<'a>(
    request: &'a ExecutionContext,
    db_connection: &mut DbConn,
    sql_file_path: Option<Cow<'a, str>>,
    variables: Option<Cow<'a, str>>,
) -> anyhow::Result<Option<Cow<'a, str>>> {
    use serde::ser::{SerializeSeq, Serializer};
    let Some(sql_file_path) = sql_file_path else {
        log::debug!("run_sql: first argument is NULL, returning NULL");
        return Ok(None);
    };
    let run_sql_span = tracing::info_span!(
        "sqlpage.file",
        otel.name = format!("SQL {sql_file_path}"),
        code.file.path = %sql_file_path,
    );
    let app_state = &request.app_state;
    let sql_file = app_state
        .sql_file_cache
        .get(
            app_state,
            FileAccess::privileged(std::path::Path::new(sql_file_path.as_ref())),
        )
        .instrument(run_sql_span.clone())
        .await
        .with_context(|| format!("run_sql: invalid path {sql_file_path:?}"))?;
    let tmp_req = if let Some(variables) = variables {
        let variables: SetVariablesMap = serde_json::from_str(&variables).with_context(|| {
            format!("run_sql(\'{sql_file_path}\', \'{variables}\'): the second argument should be a JSON object with string keys and values")
        })?;
        request.fork_with_variables(variables)
    } else {
        request.fork()
    };
    let max_recursion_depth = app_state.config.max_recursion_depth;
    if tmp_req.clone_depth > max_recursion_depth {
        anyhow::bail!(
            "Too many nested inclusions. run_sql can include a file that includes another file, but the depth is limited to {max_recursion_depth} levels. \n\
        Executing sqlpage.run_sql('{sql_file_path}') would exceed this limit. \n\
        This is to prevent infinite loops and stack overflows.\n\
        Make sure that your SQL file does not try to run itself, directly or through a chain of other files.\n\
        If you need to include more files, you can increase max_recursion_depth in the configuration file.\
        "
        );
    }
    let mut results_stream =
        crate::webserver::database::execute_queries::stream_query_results_boxed(
            &sql_file,
            &tmp_req,
            db_connection,
        );
    let mut json_results_bytes = Vec::new();
    let mut json_encoder = serde_json::Serializer::new(&mut json_results_bytes);
    let mut seq = json_encoder.serialize_seq(None)?;
    while let Some(db_item) = results_stream.next().instrument(run_sql_span.clone()).await {
        use crate::webserver::database::DbItem::{Error, FinishedQuery, Row};
        match db_item {
            Row(row) => {
                log::debug!("run_sql: row: {row:?}");
                seq.serialize_element(&row)?;
            }
            FinishedQuery => log::trace!("run_sql: Finished query"),
            Error(err) => {
                return Err(err.context(format!("run_sql: unable to run {sql_file_path:?}")));
            }
        }
    }
    seq.end()?;
    Ok(Some(Cow::Owned(String::from_utf8(json_results_bytes)?)))
}