pub mod builder;
pub mod fragment;
pub mod pre_query;
pub mod sql_builder;
pub mod statements;
pub use sql_builder::{SqlBuilder, SqlParam};
use crate::backend::SqlDialect;
use crate::config::AppConfig;
use crate::plan::{ActionPlan, CrudPlan, DbActionPlan};
#[derive(Debug)]
pub struct MainQuery {
pub tx_vars: Option<SqlBuilder>,
pub pre_req: Option<SqlBuilder>,
pub mutation: Option<SqlBuilder>,
pub main: Option<SqlBuilder>,
}
impl MainQuery {
pub fn empty() -> Self {
Self {
tx_vars: None,
pre_req: None,
mutation: None,
main: None,
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn main_query(
action_plan: &ActionPlan,
config: &AppConfig,
dialect: &dyn SqlDialect,
method: &str,
path: &str,
role: Option<&str>,
headers_json: Option<&str>,
cookies_json: Option<&str>,
claims_json: Option<&str>,
) -> MainQuery {
let tx_vars = Some(pre_query::tx_var_query(
config,
dialect,
method,
path,
role,
headers_json,
cookies_json,
claims_json,
));
let pre_req = config.db_pre_request.as_ref().map(pre_query::pre_req_query);
let (mutation, main) = match action_plan {
ActionPlan::Db(db_plan) => match db_plan {
DbActionPlan::DbCrud { plan, .. } => {
let (m, q) = build_crud_query(plan, config, dialect);
(m, Some(q))
}
DbActionPlan::MayUseDb(_inspect) => {
(None, None)
}
},
ActionPlan::NoDb(_) => {
(None, None)
}
};
MainQuery {
tx_vars,
pre_req,
mutation,
main,
}
}
fn build_crud_query(
plan: &CrudPlan,
config: &AppConfig,
dialect: &dyn SqlDialect,
) -> (Option<SqlBuilder>, SqlBuilder) {
match plan {
CrudPlan::WrappedReadPlan {
read_plan,
headers_only,
handler,
..
} => (
None,
statements::main_read(
read_plan,
None,
config.db_max_rows,
*headers_only,
Some(&handler.0),
dialect,
),
),
CrudPlan::MutateReadPlan {
read_plan,
mutate_plan,
handler,
..
} => {
let return_representation = !mutate_plan.returning().is_empty();
if dialect.supports_dml_cte() {
(
None,
statements::main_write(
mutate_plan,
read_plan,
return_representation,
Some(&handler.0),
dialect,
),
)
} else {
let (mutation, agg) = statements::main_write_split(
mutate_plan,
read_plan,
return_representation,
Some(&handler.0),
dialect,
);
(Some(mutation), agg)
}
}
CrudPlan::CallReadPlan {
call_plan, handler, ..
} => (
None,
statements::main_call(
call_plan,
None,
config.db_max_rows,
Some(&handler.0),
dialect,
),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api_request::types::{InvokeMethod, Mutation, Payload};
use crate::plan::TxMode;
use crate::plan::call_plan::{CallArgs, CallParams, CallPlan};
use crate::plan::mutate_plan::{InsertPlan, MutatePlan};
use crate::plan::read_plan::{ReadPlan, ReadPlanTree};
use crate::plan::types::*;
use crate::schema_cache::media_handler::{MediaHandler, ResolvedHandler};
use crate::test_helpers::TestPgDialect;
use crate::types::identifiers::QualifiedIdentifier;
use crate::types::media::MediaType;
use bytes::Bytes;
use smallvec::SmallVec;
fn dialect() -> &'static dyn SqlDialect {
&TestPgDialect
}
fn test_qi() -> QualifiedIdentifier {
QualifiedIdentifier::new("test_api", "users")
}
fn test_config() -> AppConfig {
let mut config = AppConfig::default();
config.db_schemas = vec!["test_api".to_string()];
config.db_anon_role = Some("web_anon".to_string());
config
}
fn select_field(name: &str) -> CoercibleSelectField {
CoercibleSelectField {
field: CoercibleField::unknown(name.into(), SmallVec::new()),
agg_function: None,
agg_cast: None,
cast: None,
alias: None,
}
}
fn default_handler() -> ResolvedHandler {
(MediaHandler::BuiltinOvAggJson, MediaType::ApplicationJson)
}
fn make_read_plan() -> ActionPlan {
let mut rp = ReadPlan::root(test_qi());
rp.select = vec![select_field("id"), select_field("name")];
let tree = ReadPlanTree::leaf(rp);
ActionPlan::Db(DbActionPlan::DbCrud {
is_explain: false,
plan: CrudPlan::WrappedReadPlan {
read_plan: tree,
tx_mode: TxMode::default_mode(),
handler: default_handler(),
media: MediaType::ApplicationJson,
headers_only: false,
qi: test_qi(),
},
})
}
fn make_mutate_plan() -> ActionPlan {
let rp = ReadPlan::root(test_qi());
let tree = ReadPlanTree::leaf(rp);
let mutate = MutatePlan::Insert(InsertPlan {
into: test_qi(),
columns: vec![CoercibleField::from_column(
"name".into(),
SmallVec::new(),
"text".into(),
)],
body: Payload::RawJSON(Bytes::from(r#"[{"name":"Alice"}]"#)),
on_conflict: None,
where_: vec![],
returning: vec![select_field("id"), select_field("name")],
pk_cols: vec!["id".into()],
apply_defaults: false,
});
ActionPlan::Db(DbActionPlan::DbCrud {
is_explain: false,
plan: CrudPlan::MutateReadPlan {
read_plan: tree,
mutate_plan: mutate,
tx_mode: TxMode::default_mode(),
handler: default_handler(),
media: MediaType::ApplicationJson,
mutation: Mutation::MutationCreate,
qi: test_qi(),
},
})
}
fn make_call_plan() -> ActionPlan {
use crate::schema_cache::routine::{ReturnType, Routine, Volatility};
use smallvec::smallvec;
let rp = ReadPlan::root(test_qi());
let tree = ReadPlanTree::leaf(rp);
let call = CallPlan {
qi: QualifiedIdentifier::new("test_api", "get_time"),
params: CallParams::KeyParams(vec![]),
args: CallArgs::JsonArgs(None),
scalar: true,
set_of_scalar: false,
filter_fields: vec![],
returning: vec![],
};
ActionPlan::Db(DbActionPlan::DbCrud {
is_explain: false,
plan: CrudPlan::CallReadPlan {
read_plan: tree,
call_plan: call,
tx_mode: TxMode::default_mode(),
proc: Routine {
schema: "test_api".into(),
name: "get_time".into(),
params: smallvec![],
return_type: ReturnType::Single(crate::schema_cache::routine::PgType::Scalar(
QualifiedIdentifier::new("pg_catalog", "timestamptz"),
)),
is_variadic: false,
volatility: Volatility::Stable,
description: None,
executable: true,
},
handler: default_handler(),
media: MediaType::ApplicationJson,
inv_method: InvokeMethod::InvRead(false),
qi: QualifiedIdentifier::new("test_api", "get_time"),
},
})
}
#[test]
fn test_main_query_read() {
let plan = make_read_plan();
let config = test_config();
let mq = main_query(
&plan,
&config,
dialect(),
"GET",
"/users",
None,
None,
None,
None,
);
assert!(mq.tx_vars.is_some());
assert!(mq.pre_req.is_none()); assert!(mq.main.is_some());
let main_sql = mq.main.unwrap().sql().to_string();
assert!(main_sql.contains("dbrst_source"));
assert!(main_sql.contains("\"users\""));
}
#[test]
fn test_main_query_mutate() {
let plan = make_mutate_plan();
let config = test_config();
let mq = main_query(
&plan,
&config,
dialect(),
"POST",
"/users",
None,
None,
None,
None,
);
assert!(mq.main.is_some());
let main_sql = mq.main.unwrap().sql().to_string();
assert!(main_sql.contains("INSERT INTO"));
}
#[test]
fn test_main_query_call() {
let plan = make_call_plan();
let config = test_config();
let mq = main_query(
&plan,
&config,
dialect(),
"POST",
"/rpc/get_time",
None,
None,
None,
None,
);
assert!(mq.main.is_some());
let main_sql = mq.main.unwrap().sql().to_string();
assert!(main_sql.contains("get_time"));
}
#[test]
fn test_main_query_with_pre_request() {
let plan = make_read_plan();
let mut config = test_config();
config.db_pre_request = Some(QualifiedIdentifier::new("test_api", "check_request"));
let mq = main_query(
&plan,
&config,
dialect(),
"GET",
"/users",
None,
None,
None,
None,
);
assert!(mq.pre_req.is_some());
let pre_sql = mq.pre_req.unwrap().sql().to_string();
assert!(pre_sql.contains("check_request"));
}
#[test]
fn test_main_query_info_plan() {
let plan = ActionPlan::NoDb(crate::plan::InfoPlan::SchemaInfoPlan);
let config = test_config();
let mq = main_query(
&plan,
&config,
dialect(),
"OPTIONS",
"/",
None,
None,
None,
None,
);
assert!(mq.main.is_none());
assert!(mq.tx_vars.is_some());
}
#[test]
fn test_main_query_empty() {
let mq = MainQuery::empty();
assert!(mq.tx_vars.is_none());
assert!(mq.pre_req.is_none());
assert!(mq.main.is_none());
}
}