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,
39 read: Option<ReadPlanTree>,
42 },
43 Call {
45 call: CallPlan,
47 read: Option<ReadPlanTree>,
49 },
50}
51
52#[derive(Clone, Debug)]
54pub enum InfoPlan {
55 RelationInfo(QualifiedIdentifier),
57 RoutineInfo(QualifiedIdentifier),
59 OpenApiSpec,
61}
62
63pub fn create_action_plan(request: &ApiRequest, schema_cache: &SchemaCache) -> Result<ActionPlan> {
65 match &request.action {
66 Action::Db(db_action) => {
67 if matches!(db_action, DbAction::SchemaRead { .. }) {
69 return Ok(ActionPlan::Info(InfoPlan::OpenApiSpec));
70 }
71 validate_embed_paths(request)?;
72 let plan = create_db_plan(request, db_action, schema_cache)?;
73 Ok(ActionPlan::Db(plan))
74 }
75 Action::RelationInfo(qi) => Ok(ActionPlan::Info(InfoPlan::RelationInfo(qi.clone()))),
76 Action::RoutineInfo { qi, .. } => Ok(ActionPlan::Info(InfoPlan::RoutineInfo(qi.clone()))),
77 Action::SchemaInfo => Ok(ActionPlan::Info(InfoPlan::OpenApiSpec)),
78 }
79}
80
81fn nearest_overload(
88 qi: &QualifiedIdentifier,
89 routines: &[crate::schema_cache::Routine],
90 supplied: &[String],
91) -> Option<String> {
92 const MIN_SIMILARITY: f64 = 0.0;
104
105 let listed = |names: &mut Vec<String>| -> String {
108 names.sort();
109 format!("({})", names.join(", "))
110 };
111 let asked = listed(&mut supplied.to_vec());
112
113 let candidates: Vec<String> = routines
114 .iter()
115 .map(|routine| listed(&mut routine.params.iter().map(|p| p.name.clone()).collect()))
116 .collect();
117
118 crate::schema_cache::closest(
119 candidates.iter().map(String::as_str),
120 &asked,
121 MIN_SIMILARITY,
122 )
123 .map(|params| format!("{}{}", qi, params))
124}
125
126fn validate_embed_paths(request: &ApiRequest) -> Result<()> {
135 use crate::api_request::SelectItem;
136
137 fn descend<'a>(items: &'a [SelectItem], segment: &str) -> Option<&'a [SelectItem]> {
140 items.iter().find_map(|item| match item {
141 SelectItem::Relation {
142 relation,
143 alias,
144 select,
145 ..
146 } if relation == segment || alias.as_deref() == Some(segment) => {
147 Some(select.as_slice())
148 }
149 SelectItem::SpreadRelation {
150 relation, select, ..
151 } if relation == segment => Some(select.as_slice()),
152 _ => None,
153 })
154 }
155
156 let check = |path: &[String]| -> Result<()> {
157 let mut items = request.query_params.select.as_slice();
158 for segment in path {
159 items = descend(items, segment)
160 .ok_or_else(|| Error::NotAnEmbeddedResource(segment.clone()))?;
161 }
162 Ok(())
163 };
164
165 for (path, _) in &request.query_params.filters {
166 check(path)?;
167 }
168 for (path, _) in &request.query_params.order {
169 check(path)?;
170 }
171 for (path, _) in &request.query_params.logic {
172 check(path)?;
173 }
174 for path in request.query_params.ranges.keys() {
175 if !path.is_empty() {
176 check(&path.split('.').map(String::from).collect::<Vec<_>>())?;
177 }
178 }
179
180 Ok(())
181}
182
183fn supplied_arguments(request: &ApiRequest) -> Vec<String> {
191 use crate::api_request::Payload;
192
193 let is_post = matches!(
201 request.action,
202 Action::Db(DbAction::Routine {
203 invoke_method: crate::api_request::InvokeMethod::Inv,
204 ..
205 })
206 );
207 if is_post {
208 if let Some(columns) = &request.query_params.columns {
209 let mut names: Vec<String> = columns.iter().cloned().collect();
210 names.sort();
211 return names;
212 }
213 }
214
215 let sorted = |mut names: Vec<String>| -> Vec<String> {
218 names.sort();
219 names.dedup();
220 names
221 };
222
223 match &request.payload {
224 Some(Payload::ProcessedJson { keys, .. }) => return sorted(keys.iter().cloned().collect()),
225 Some(Payload::ProcessedUrlEncoded { data, .. }) => {
226 return sorted(data.iter().map(|(name, _)| name.clone()).collect())
227 }
228 Some(Payload::RawJson(_)) | Some(Payload::RawPayload(_)) => return Vec::new(),
230 None if is_post => return Vec::new(),
231 _ => {}
232 }
233
234 let embedded: HashSet<String> = request
237 .query_params
238 .filters
239 .iter()
240 .map(|(path, filter)| {
241 let mut key = path.join(".");
242 key.push('.');
243 key.push_str(&filter.field.name);
244 key
245 })
246 .collect();
247
248 let mut names: Vec<String> = Vec::new();
249 for (name, value) in &request.query_params.params {
250 if crate::api_request::value_is_filter(value) || embedded.contains(name) {
251 continue;
252 }
253 if !names.contains(name) {
254 names.push(name.clone());
255 }
256 }
257 names.sort();
258 names
259}
260
261fn select_overload<'a>(
279 routines: &'a [Routine],
280 request: &ApiRequest,
281) -> Result<Option<&'a Routine>> {
282 let supplied = supplied_arguments(request);
283 let is_post = matches!(
284 request.action,
285 Action::Db(DbAction::Routine {
286 invoke_method: crate::api_request::InvokeMethod::Inv,
287 ..
288 })
289 );
290
291 let (named, unnamed): (Vec<&Routine>, Vec<&Routine>) =
292 routines
293 .iter()
294 .fold((Vec::new(), Vec::new()), |mut acc, r| {
295 if matches_params(r, &supplied, is_post, &request.content_media_type) {
296 acc.0.push(r);
297 } else if takes_whole_body(r, is_post, &request.content_media_type) {
298 acc.1.push(r);
299 }
300 acc
301 });
302
303 let candidates = match named.is_empty() {
304 true => unnamed,
305 false => named,
306 };
307
308 match candidates.len() {
309 0 => Ok(None),
310 1 => Ok(candidates.first().copied()),
311 _ => Err(Error::AmbiguousFunction {
312 candidates: candidates.iter().map(|r| signature_of(r)).collect(),
313 }),
314 }
315}
316
317fn matches_params(
323 routine: &Routine,
324 supplied: &[String],
325 is_post: bool,
326 content: &crate::api_request::MediaType,
327) -> bool {
328 if routine.params.is_empty() {
329 return supplied.is_empty() && !(is_post && is_raw_body_type(content));
333 }
334
335 let declares = |name: &String, required: bool| {
336 routine
337 .params
338 .iter()
339 .any(|p| &p.name == name && p.required == required)
340 };
341
342 routine
343 .params
344 .iter()
345 .filter(|p| p.required)
346 .all(|p| supplied.contains(&p.name))
347 && supplied
348 .iter()
349 .all(|name| declares(name, true) || declares(name, false))
350}
351
352fn takes_whole_body(
358 routine: &Routine,
359 is_post: bool,
360 content: &crate::api_request::MediaType,
361) -> bool {
362 use crate::api_request::MediaType;
363
364 if !is_post {
365 return false;
366 }
367 let [param] = routine.params.as_slice() else {
368 return false;
369 };
370 if !param.name.is_empty() {
371 return false;
372 }
373 matches!(
374 (content, param.param_type.as_str()),
375 (MediaType::ApplicationJson, "json" | "jsonb")
376 | (MediaType::TextPlain, "text")
377 | (MediaType::TextXml, "xml")
378 | (MediaType::OctetStream, "bytea")
379 )
380}
381
382fn single_param_type(content: &crate::api_request::MediaType) -> Option<String> {
388 use crate::api_request::MediaType;
389 match content {
390 MediaType::ApplicationJson => Some(crate::error::JSON_PARAM.to_string()),
391 MediaType::TextPlain => Some("text".to_string()),
392 MediaType::TextXml => Some("xml".to_string()),
393 MediaType::OctetStream => Some("bytea".to_string()),
394 _ => None,
395 }
396}
397
398fn is_raw_body_type(content: &crate::api_request::MediaType) -> bool {
400 use crate::api_request::MediaType;
401 matches!(
402 content,
403 MediaType::TextPlain | MediaType::TextXml | MediaType::OctetStream
404 )
405}
406
407fn signature_of(routine: &Routine) -> String {
409 format!(
410 "{}.{}({})",
411 routine.schema,
412 routine.name,
413 routine
414 .params
415 .iter()
416 .map(|p| format!("{} => {}", p.name, p.param_type))
417 .collect::<Vec<_>>()
418 .join(", ")
419 )
420}
421
422fn create_db_plan(
424 request: &ApiRequest,
425 action: &DbAction,
426 schema_cache: &SchemaCache,
427) -> Result<DbActionPlan> {
428 match action {
429 DbAction::RelationRead { qi, .. } => {
430 let table = schema_cache.require_table(qi)?;
431 let read_plan = ReadPlan::from_request(request, table, schema_cache)?;
432 Ok(DbActionPlan::Read(ReadPlanTree::leaf(read_plan)))
433 }
434
435 DbAction::RelationMut { qi, mutation } => {
436 let table = schema_cache.require_table(qi)?;
437 let mutate_plan = MutatePlan::from_request(request, table, mutation, schema_cache)?;
438
439 let read_plan = if request.preferences.representation.needs_body() {
440 let rp = ReadPlan::for_mutation(request, table, schema_cache)?;
441 Some(ReadPlanTree::leaf(rp))
442 } else {
443 None
444 };
445
446 Ok(DbActionPlan::MutateRead {
447 mutate: mutate_plan,
448 read: read_plan,
449 })
450 }
451
452 DbAction::Routine { qi, invoke_method } => {
453 let supplied = supplied_arguments(request);
454
455 let single_param = match invoke_method {
460 crate::api_request::InvokeMethod::Inv => {
461 single_param_type(&request.content_media_type)
462 }
463 _ => None,
464 };
465
466 let not_found = |candidate: Option<String>| Error::FunctionNotFound {
469 name: qi.to_string(),
470 params: supplied.clone(),
471 candidate,
472 single_param: single_param.clone(),
473 };
474
475 let routines = schema_cache
478 .get_routines(qi)
479 .ok_or_else(|| not_found(schema_cache.similar_routine(qi)))?;
480
481 let routine = select_overload(routines, request)?.ok_or_else(|| {
482 not_found(nearest_overload(qi, routines, &supplied))
487 })?;
488
489 let call_plan = CallPlan::from_request(request, routine)?;
490
491 let read = schema_cache
501 .returned_table(routine)
502 .map(|table| ReadPlan::from_request(request, table, schema_cache))
503 .transpose()?
504 .map(ReadPlanTree::leaf);
505
506 Ok(DbActionPlan::Call {
507 call: call_plan,
508 read,
509 })
510 }
511
512 DbAction::SchemaRead { .. } => {
513 unreachable!("SchemaRead should be handled in create_action_plan")
515 }
516 }
517}
518
519impl crate::api_request::PreferRepresentation {
520 pub fn needs_body(&self) -> bool {
522 matches!(self, Self::Full)
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
531 fn test_info_plan() {
532 let qi = QualifiedIdentifier::new("public", "users");
533 let plan = ActionPlan::Info(InfoPlan::RelationInfo(qi.clone()));
534
535 match plan {
536 ActionPlan::Info(InfoPlan::RelationInfo(q)) => {
537 assert_eq!(q.name, "users");
538 }
539 _ => panic!("Expected RelationInfo"),
540 }
541 }
542}