mod call_plan;
mod mutate_plan;
mod read_plan;
mod types;
pub use call_plan::{CallParams, CallPlan};
pub use mutate_plan::MutatePlan;
pub use read_plan::{resolve_top_level_range, ReadPlan, ReadPlanTree};
pub use types::*;
use crate::api_request::{Action, ApiRequest, DbAction, QualifiedIdentifier};
use crate::error::{Error, Result};
use crate::schema_cache::{Routine, SchemaCache};
use std::collections::HashSet;
#[derive(Clone, Debug)]
pub enum ActionPlan {
Db(DbActionPlan),
Info(InfoPlan),
}
#[derive(Clone, Debug)]
pub enum DbActionPlan {
Read(ReadPlanTree),
MutateRead {
mutate: MutatePlan,
read: Option<ReadPlanTree>,
},
Call {
call: CallPlan,
read: Option<ReadPlanTree>,
},
}
#[derive(Clone, Debug)]
pub enum InfoPlan {
RelationInfo(QualifiedIdentifier),
RoutineInfo(QualifiedIdentifier),
OpenApiSpec,
}
pub fn create_action_plan(request: &ApiRequest, schema_cache: &SchemaCache) -> Result<ActionPlan> {
match &request.action {
Action::Db(db_action) => {
if matches!(db_action, DbAction::SchemaRead { .. }) {
return Ok(ActionPlan::Info(InfoPlan::OpenApiSpec));
}
validate_embed_paths(request)?;
let plan = create_db_plan(request, db_action, schema_cache)?;
Ok(ActionPlan::Db(plan))
}
Action::RelationInfo(qi) => Ok(ActionPlan::Info(InfoPlan::RelationInfo(qi.clone()))),
Action::RoutineInfo { qi, .. } => Ok(ActionPlan::Info(InfoPlan::RoutineInfo(qi.clone()))),
Action::SchemaInfo => Ok(ActionPlan::Info(InfoPlan::OpenApiSpec)),
}
}
fn nearest_overload(
qi: &QualifiedIdentifier,
routines: &[crate::schema_cache::Routine],
supplied: &[String],
) -> Option<String> {
const MIN_SIMILARITY: f64 = 0.0;
let listed = |names: &mut Vec<String>| -> String {
names.sort();
format!("({})", names.join(", "))
};
let asked = listed(&mut supplied.to_vec());
let candidates: Vec<String> = routines
.iter()
.map(|routine| listed(&mut routine.params.iter().map(|p| p.name.clone()).collect()))
.collect();
crate::schema_cache::closest(
candidates.iter().map(String::as_str),
&asked,
MIN_SIMILARITY,
)
.map(|params| format!("{}{}", qi, params))
}
fn validate_embed_paths(request: &ApiRequest) -> Result<()> {
use crate::api_request::SelectItem;
fn descend<'a>(items: &'a [SelectItem], segment: &str) -> Option<&'a [SelectItem]> {
items.iter().find_map(|item| match item {
SelectItem::Relation {
relation,
alias,
select,
..
} if relation == segment || alias.as_deref() == Some(segment) => {
Some(select.as_slice())
}
SelectItem::SpreadRelation {
relation, select, ..
} if relation == segment => Some(select.as_slice()),
_ => None,
})
}
let check = |path: &[String]| -> Result<()> {
let mut items = request.query_params.select.as_slice();
for segment in path {
items = descend(items, segment)
.ok_or_else(|| Error::NotAnEmbeddedResource(segment.clone()))?;
}
Ok(())
};
for (path, _) in &request.query_params.filters {
check(path)?;
}
for (path, _) in &request.query_params.order {
check(path)?;
}
for (path, _) in &request.query_params.logic {
check(path)?;
}
for path in request.query_params.ranges.keys() {
if !path.is_empty() {
check(&path.split('.').map(String::from).collect::<Vec<_>>())?;
}
}
Ok(())
}
fn supplied_arguments(request: &ApiRequest) -> Vec<String> {
use crate::api_request::Payload;
let is_post = matches!(
request.action,
Action::Db(DbAction::Routine {
invoke_method: crate::api_request::InvokeMethod::Inv,
..
})
);
if is_post {
if let Some(columns) = &request.query_params.columns {
let mut names: Vec<String> = columns.iter().cloned().collect();
names.sort();
return names;
}
}
let sorted = |mut names: Vec<String>| -> Vec<String> {
names.sort();
names.dedup();
names
};
match &request.payload {
Some(Payload::ProcessedJson { keys, .. }) => return sorted(keys.iter().cloned().collect()),
Some(Payload::ProcessedUrlEncoded { data, .. }) => {
return sorted(data.iter().map(|(name, _)| name.clone()).collect())
}
Some(Payload::RawJson(_)) | Some(Payload::RawPayload(_)) => return Vec::new(),
None if is_post => return Vec::new(),
_ => {}
}
let embedded: HashSet<String> = request
.query_params
.filters
.iter()
.map(|(path, filter)| {
let mut key = path.join(".");
key.push('.');
key.push_str(&filter.field.name);
key
})
.collect();
let mut names: Vec<String> = Vec::new();
for (name, value) in &request.query_params.params {
if crate::api_request::value_is_filter(value) || embedded.contains(name) {
continue;
}
if !names.contains(name) {
names.push(name.clone());
}
}
names.sort();
names
}
fn select_overload<'a>(
routines: &'a [Routine],
request: &ApiRequest,
) -> Result<Option<&'a Routine>> {
let supplied = supplied_arguments(request);
let is_post = matches!(
request.action,
Action::Db(DbAction::Routine {
invoke_method: crate::api_request::InvokeMethod::Inv,
..
})
);
let (named, unnamed): (Vec<&Routine>, Vec<&Routine>) =
routines
.iter()
.fold((Vec::new(), Vec::new()), |mut acc, r| {
if matches_params(r, &supplied, is_post, &request.content_media_type) {
acc.0.push(r);
} else if takes_whole_body(r, is_post, &request.content_media_type) {
acc.1.push(r);
}
acc
});
let candidates = match named.is_empty() {
true => unnamed,
false => named,
};
match candidates.len() {
0 => Ok(None),
1 => Ok(candidates.first().copied()),
_ => Err(Error::AmbiguousFunction {
candidates: candidates.iter().map(|r| signature_of(r)).collect(),
}),
}
}
fn matches_params(
routine: &Routine,
supplied: &[String],
is_post: bool,
content: &crate::api_request::MediaType,
) -> bool {
if routine.params.is_empty() {
return supplied.is_empty() && !(is_post && is_raw_body_type(content));
}
let declares = |name: &String, required: bool| {
routine
.params
.iter()
.any(|p| &p.name == name && p.required == required)
};
routine
.params
.iter()
.filter(|p| p.required)
.all(|p| supplied.contains(&p.name))
&& supplied
.iter()
.all(|name| declares(name, true) || declares(name, false))
}
fn takes_whole_body(
routine: &Routine,
is_post: bool,
content: &crate::api_request::MediaType,
) -> bool {
use crate::api_request::MediaType;
if !is_post {
return false;
}
let [param] = routine.params.as_slice() else {
return false;
};
if !param.name.is_empty() {
return false;
}
matches!(
(content, param.param_type.as_str()),
(MediaType::ApplicationJson, "json" | "jsonb")
| (MediaType::TextPlain, "text")
| (MediaType::TextXml, "xml")
| (MediaType::OctetStream, "bytea")
)
}
fn single_param_type(content: &crate::api_request::MediaType) -> Option<String> {
use crate::api_request::MediaType;
match content {
MediaType::ApplicationJson => Some(crate::error::JSON_PARAM.to_string()),
MediaType::TextPlain => Some("text".to_string()),
MediaType::TextXml => Some("xml".to_string()),
MediaType::OctetStream => Some("bytea".to_string()),
_ => None,
}
}
fn is_raw_body_type(content: &crate::api_request::MediaType) -> bool {
use crate::api_request::MediaType;
matches!(
content,
MediaType::TextPlain | MediaType::TextXml | MediaType::OctetStream
)
}
fn signature_of(routine: &Routine) -> String {
format!(
"{}.{}({})",
routine.schema,
routine.name,
routine
.params
.iter()
.map(|p| format!("{} => {}", p.name, p.param_type))
.collect::<Vec<_>>()
.join(", ")
)
}
fn create_db_plan(
request: &ApiRequest,
action: &DbAction,
schema_cache: &SchemaCache,
) -> Result<DbActionPlan> {
match action {
DbAction::RelationRead { qi, .. } => {
let table = schema_cache.require_table(qi)?;
let read_plan = ReadPlan::from_request(request, table, schema_cache)?;
Ok(DbActionPlan::Read(ReadPlanTree::leaf(read_plan)))
}
DbAction::RelationMut { qi, mutation } => {
let table = schema_cache.require_table(qi)?;
let mutate_plan = MutatePlan::from_request(request, table, mutation, schema_cache)?;
let read_plan = if request.preferences.representation.needs_body() {
let rp = ReadPlan::for_mutation(request, table, schema_cache)?;
Some(ReadPlanTree::leaf(rp))
} else {
None
};
Ok(DbActionPlan::MutateRead {
mutate: mutate_plan,
read: read_plan,
})
}
DbAction::Routine { qi, invoke_method } => {
let supplied = supplied_arguments(request);
let single_param = match invoke_method {
crate::api_request::InvokeMethod::Inv => {
single_param_type(&request.content_media_type)
}
_ => None,
};
let not_found = |candidate: Option<String>| Error::FunctionNotFound {
name: qi.to_string(),
params: supplied.clone(),
candidate,
single_param: single_param.clone(),
};
let routines = schema_cache
.get_routines(qi)
.ok_or_else(|| not_found(schema_cache.similar_routine(qi)))?;
let routine = select_overload(routines, request)?.ok_or_else(|| {
not_found(nearest_overload(qi, routines, &supplied))
})?;
let call_plan = CallPlan::from_request(request, routine)?;
let read = schema_cache
.returned_table(routine)
.map(|table| ReadPlan::from_request(request, table, schema_cache))
.transpose()?
.map(ReadPlanTree::leaf);
Ok(DbActionPlan::Call {
call: call_plan,
read,
})
}
DbAction::SchemaRead { .. } => {
unreachable!("SchemaRead should be handled in create_action_plan")
}
}
}
impl crate::api_request::PreferRepresentation {
pub fn needs_body(&self) -> bool {
matches!(self, Self::Full)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_info_plan() {
let qi = QualifiedIdentifier::new("public", "users");
let plan = ActionPlan::Info(InfoPlan::RelationInfo(qi.clone()));
match plan {
ActionPlan::Info(InfoPlan::RelationInfo(q)) => {
assert_eq!(q.name, "users");
}
_ => panic!("Expected RelationInfo"),
}
}
}