1mod call_plan;
7mod mutate_plan;
8mod read_plan;
9mod types;
10
11pub use call_plan::{CallParams, CallPlan};
12pub use mutate_plan::MutatePlan;
13pub use read_plan::{resolve_top_level_range, ReadPlan, ReadPlanTree};
14pub use types::*;
15
16use crate::api_request::{Action, ApiRequest, DbAction, QualifiedIdentifier};
17use crate::error::{Error, Result};
18use crate::schema_cache::{Routine, SchemaCache};
19use std::collections::HashSet;
20
21#[derive(Clone, Debug)]
23pub enum ActionPlan {
24 Db(DbActionPlan),
26 Info(InfoPlan),
28}
29
30#[derive(Clone, Debug)]
32pub enum DbActionPlan {
33 Read(ReadPlanTree),
35 MutateRead {
37 mutate: MutatePlan,
38 read: Option<ReadPlanTree>,
39 },
40 Call {
42 call: CallPlan,
43 read: Option<ReadPlanTree>,
44 },
45}
46
47#[derive(Clone, Debug)]
49pub enum InfoPlan {
50 RelationInfo(QualifiedIdentifier),
52 RoutineInfo(QualifiedIdentifier),
54 OpenApiSpec,
56}
57
58pub fn create_action_plan(request: &ApiRequest, schema_cache: &SchemaCache) -> Result<ActionPlan> {
60 match &request.action {
61 Action::Db(db_action) => {
62 if matches!(db_action, DbAction::SchemaRead { .. }) {
64 return Ok(ActionPlan::Info(InfoPlan::OpenApiSpec));
65 }
66 validate_embed_paths(request)?;
67 let plan = create_db_plan(request, db_action, schema_cache)?;
68 Ok(ActionPlan::Db(plan))
69 }
70 Action::RelationInfo(qi) => Ok(ActionPlan::Info(InfoPlan::RelationInfo(qi.clone()))),
71 Action::RoutineInfo { qi, .. } => Ok(ActionPlan::Info(InfoPlan::RoutineInfo(qi.clone()))),
72 Action::SchemaInfo => Ok(ActionPlan::Info(InfoPlan::OpenApiSpec)),
73 }
74}
75
76fn nearest_overload(
83 qi: &QualifiedIdentifier,
84 routines: &[crate::schema_cache::Routine],
85 supplied: &[String],
86) -> Option<String> {
87 const MIN_SIMILARITY: f64 = 0.0;
99
100 let listed = |names: &mut Vec<String>| -> String {
103 names.sort();
104 format!("({})", names.join(", "))
105 };
106 let asked = listed(&mut supplied.to_vec());
107
108 let candidates: Vec<String> = routines
109 .iter()
110 .map(|routine| listed(&mut routine.params.iter().map(|p| p.name.clone()).collect()))
111 .collect();
112
113 crate::schema_cache::closest(
114 candidates.iter().map(String::as_str),
115 &asked,
116 MIN_SIMILARITY,
117 )
118 .map(|params| format!("{}{}", qi, params))
119}
120
121fn validate_embed_paths(request: &ApiRequest) -> Result<()> {
130 use crate::api_request::SelectItem;
131
132 fn descend<'a>(items: &'a [SelectItem], segment: &str) -> Option<&'a [SelectItem]> {
135 items.iter().find_map(|item| match item {
136 SelectItem::Relation {
137 relation,
138 alias,
139 select,
140 ..
141 } if relation == segment || alias.as_deref() == Some(segment) => {
142 Some(select.as_slice())
143 }
144 SelectItem::SpreadRelation {
145 relation, select, ..
146 } if relation == segment => Some(select.as_slice()),
147 _ => None,
148 })
149 }
150
151 let check = |path: &[String]| -> Result<()> {
152 let mut items = request.query_params.select.as_slice();
153 for segment in path {
154 items = descend(items, segment)
155 .ok_or_else(|| Error::NotAnEmbeddedResource(segment.clone()))?;
156 }
157 Ok(())
158 };
159
160 for (path, _) in &request.query_params.filters {
161 check(path)?;
162 }
163 for (path, _) in &request.query_params.order {
164 check(path)?;
165 }
166 for (path, _) in &request.query_params.logic {
167 check(path)?;
168 }
169 for path in request.query_params.ranges.keys() {
170 if !path.is_empty() {
171 check(&path.split('.').map(String::from).collect::<Vec<_>>())?;
172 }
173 }
174
175 Ok(())
176}
177
178fn supplied_arguments(request: &ApiRequest) -> Vec<String> {
186 use crate::api_request::Payload;
187
188 let is_post = matches!(
196 request.action,
197 Action::Db(DbAction::Routine {
198 invoke_method: crate::api_request::InvokeMethod::Inv,
199 ..
200 })
201 );
202 if is_post {
203 if let Some(columns) = &request.query_params.columns {
204 let mut names: Vec<String> = columns.iter().cloned().collect();
205 names.sort();
206 return names;
207 }
208 }
209
210 let sorted = |mut names: Vec<String>| -> Vec<String> {
213 names.sort();
214 names.dedup();
215 names
216 };
217
218 match &request.payload {
219 Some(Payload::ProcessedJson { keys, .. }) => return sorted(keys.iter().cloned().collect()),
220 Some(Payload::ProcessedUrlEncoded { data, .. }) => {
221 return sorted(data.iter().map(|(name, _)| name.clone()).collect())
222 }
223 Some(Payload::RawJson(_)) | Some(Payload::RawPayload(_)) => return Vec::new(),
225 None if is_post => return Vec::new(),
226 _ => {}
227 }
228
229 let embedded: HashSet<String> = request
232 .query_params
233 .filters
234 .iter()
235 .map(|(path, filter)| {
236 let mut key = path.join(".");
237 key.push('.');
238 key.push_str(&filter.field.name);
239 key
240 })
241 .collect();
242
243 let mut names: Vec<String> = Vec::new();
244 for (name, value) in &request.query_params.params {
245 if crate::api_request::value_is_filter(value) || embedded.contains(name) {
246 continue;
247 }
248 if !names.contains(name) {
249 names.push(name.clone());
250 }
251 }
252 names.sort();
253 names
254}
255
256fn select_overload<'a>(
274 routines: &'a [Routine],
275 request: &ApiRequest,
276) -> Result<Option<&'a Routine>> {
277 let supplied = supplied_arguments(request);
278 let is_post = matches!(
279 request.action,
280 Action::Db(DbAction::Routine {
281 invoke_method: crate::api_request::InvokeMethod::Inv,
282 ..
283 })
284 );
285
286 let (named, unnamed): (Vec<&Routine>, Vec<&Routine>) =
287 routines
288 .iter()
289 .fold((Vec::new(), Vec::new()), |mut acc, r| {
290 if matches_params(r, &supplied, is_post, &request.content_media_type) {
291 acc.0.push(r);
292 } else if takes_whole_body(r, is_post, &request.content_media_type) {
293 acc.1.push(r);
294 }
295 acc
296 });
297
298 let candidates = match named.is_empty() {
299 true => unnamed,
300 false => named,
301 };
302
303 match candidates.len() {
304 0 => Ok(None),
305 1 => Ok(candidates.first().copied()),
306 _ => Err(Error::AmbiguousFunction {
307 candidates: candidates.iter().map(|r| signature_of(r)).collect(),
308 }),
309 }
310}
311
312fn matches_params(
318 routine: &Routine,
319 supplied: &[String],
320 is_post: bool,
321 content: &crate::api_request::MediaType,
322) -> bool {
323 if routine.params.is_empty() {
324 return supplied.is_empty() && !(is_post && is_raw_body_type(content));
328 }
329
330 let declares = |name: &String, required: bool| {
331 routine
332 .params
333 .iter()
334 .any(|p| &p.name == name && p.required == required)
335 };
336
337 routine
338 .params
339 .iter()
340 .filter(|p| p.required)
341 .all(|p| supplied.contains(&p.name))
342 && supplied
343 .iter()
344 .all(|name| declares(name, true) || declares(name, false))
345}
346
347fn takes_whole_body(
353 routine: &Routine,
354 is_post: bool,
355 content: &crate::api_request::MediaType,
356) -> bool {
357 use crate::api_request::MediaType;
358
359 if !is_post {
360 return false;
361 }
362 let [param] = routine.params.as_slice() else {
363 return false;
364 };
365 if !param.name.is_empty() {
366 return false;
367 }
368 matches!(
369 (content, param.param_type.as_str()),
370 (MediaType::ApplicationJson, "json" | "jsonb")
371 | (MediaType::TextPlain, "text")
372 | (MediaType::TextXml, "xml")
373 | (MediaType::OctetStream, "bytea")
374 )
375}
376
377fn single_param_type(content: &crate::api_request::MediaType) -> Option<String> {
383 use crate::api_request::MediaType;
384 match content {
385 MediaType::ApplicationJson => Some(crate::error::JSON_PARAM.to_string()),
386 MediaType::TextPlain => Some("text".to_string()),
387 MediaType::TextXml => Some("xml".to_string()),
388 MediaType::OctetStream => Some("bytea".to_string()),
389 _ => None,
390 }
391}
392
393fn is_raw_body_type(content: &crate::api_request::MediaType) -> bool {
395 use crate::api_request::MediaType;
396 matches!(
397 content,
398 MediaType::TextPlain | MediaType::TextXml | MediaType::OctetStream
399 )
400}
401
402fn signature_of(routine: &Routine) -> String {
404 format!(
405 "{}.{}({})",
406 routine.schema,
407 routine.name,
408 routine
409 .params
410 .iter()
411 .map(|p| format!("{} => {}", p.name, p.param_type))
412 .collect::<Vec<_>>()
413 .join(", ")
414 )
415}
416
417fn create_db_plan(
419 request: &ApiRequest,
420 action: &DbAction,
421 schema_cache: &SchemaCache,
422) -> Result<DbActionPlan> {
423 match action {
424 DbAction::RelationRead { qi, .. } => {
425 let table = schema_cache.require_table(qi)?;
426 let read_plan = ReadPlan::from_request(request, table, schema_cache)?;
427 Ok(DbActionPlan::Read(ReadPlanTree::leaf(read_plan)))
428 }
429
430 DbAction::RelationMut { qi, mutation } => {
431 let table = schema_cache.require_table(qi)?;
432 let mutate_plan = MutatePlan::from_request(request, table, mutation, schema_cache)?;
433
434 let read_plan = if request.preferences.representation.needs_body() {
435 let rp = ReadPlan::for_mutation(request, table, schema_cache)?;
436 Some(ReadPlanTree::leaf(rp))
437 } else {
438 None
439 };
440
441 Ok(DbActionPlan::MutateRead {
442 mutate: mutate_plan,
443 read: read_plan,
444 })
445 }
446
447 DbAction::Routine { qi, invoke_method } => {
448 let supplied = supplied_arguments(request);
449
450 let single_param = match invoke_method {
455 crate::api_request::InvokeMethod::Inv => {
456 single_param_type(&request.content_media_type)
457 }
458 _ => None,
459 };
460
461 let not_found = |candidate: Option<String>| Error::FunctionNotFound {
464 name: qi.to_string(),
465 params: supplied.clone(),
466 candidate,
467 single_param: single_param.clone(),
468 };
469
470 let routines = schema_cache
473 .get_routines(qi)
474 .ok_or_else(|| not_found(schema_cache.similar_routine(qi)))?;
475
476 let routine = select_overload(routines, request)?.ok_or_else(|| {
477 not_found(nearest_overload(qi, routines, &supplied))
482 })?;
483
484 let call_plan = CallPlan::from_request(request, routine)?;
485
486 let read = schema_cache
496 .returned_table(routine)
497 .map(|table| ReadPlan::from_request(request, table, schema_cache))
498 .transpose()?
499 .map(ReadPlanTree::leaf);
500
501 Ok(DbActionPlan::Call {
502 call: call_plan,
503 read,
504 })
505 }
506
507 DbAction::SchemaRead { .. } => {
508 unreachable!("SchemaRead should be handled in create_action_plan")
510 }
511 }
512}
513
514impl crate::api_request::PreferRepresentation {
515 pub fn needs_body(&self) -> bool {
517 matches!(self, Self::Full)
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 #[test]
526 fn test_info_plan() {
527 let qi = QualifiedIdentifier::new("public", "users");
528 let plan = ActionPlan::Info(InfoPlan::RelationInfo(qi.clone()));
529
530 match plan {
531 ActionPlan::Info(InfoPlan::RelationInfo(q)) => {
532 assert_eq!(q.name, "users");
533 }
534 _ => panic!("Expected RelationInfo"),
535 }
536 }
537}