Skip to main content

postrust_core/plan/
mod.rs

1//! Query planning module.
2//!
3//! Converts parsed API requests into execution plans that can be
4//! translated to SQL queries.
5
6mod 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::{ReadPlan, ReadPlanTree};
14pub use types::*;
15
16use crate::api_request::{Action, ApiRequest, DbAction, QualifiedIdentifier};
17use crate::error::{Error, Result};
18use crate::schema_cache::SchemaCache;
19
20/// The execution plan for an API request.
21#[derive(Clone, Debug)]
22pub enum ActionPlan {
23    /// Plan that requires database access
24    Db(DbActionPlan),
25    /// Plan that doesn't need database (OPTIONS, OpenAPI)
26    Info(InfoPlan),
27}
28
29/// Database action plan.
30#[derive(Clone, Debug)]
31pub enum DbActionPlan {
32    /// Read operation (SELECT)
33    Read(ReadPlanTree),
34    /// Mutation operation (INSERT/UPDATE/DELETE)
35    MutateRead {
36        mutate: MutatePlan,
37        read: Option<ReadPlanTree>,
38    },
39    /// RPC call
40    Call {
41        call: CallPlan,
42        read: Option<ReadPlanTree>,
43    },
44}
45
46/// Info-only plan (no database access needed).
47#[derive(Clone, Debug)]
48pub enum InfoPlan {
49    /// OPTIONS on a table
50    RelationInfo(QualifiedIdentifier),
51    /// OPTIONS on a function
52    RoutineInfo(QualifiedIdentifier),
53    /// OpenAPI spec
54    OpenApiSpec,
55}
56
57/// Create an action plan from an API request.
58pub fn create_action_plan(request: &ApiRequest, schema_cache: &SchemaCache) -> Result<ActionPlan> {
59    match &request.action {
60        Action::Db(db_action) => {
61            // SchemaRead is a special case - it returns OpenAPI spec, not a DB query
62            if matches!(db_action, DbAction::SchemaRead { .. }) {
63                return Ok(ActionPlan::Info(InfoPlan::OpenApiSpec));
64            }
65            let plan = create_db_plan(request, db_action, schema_cache)?;
66            Ok(ActionPlan::Db(plan))
67        }
68        Action::RelationInfo(qi) => Ok(ActionPlan::Info(InfoPlan::RelationInfo(qi.clone()))),
69        Action::RoutineInfo { qi, .. } => Ok(ActionPlan::Info(InfoPlan::RoutineInfo(qi.clone()))),
70        Action::SchemaInfo => Ok(ActionPlan::Info(InfoPlan::OpenApiSpec)),
71    }
72}
73
74/// Create a database action plan.
75fn create_db_plan(
76    request: &ApiRequest,
77    action: &DbAction,
78    schema_cache: &SchemaCache,
79) -> Result<DbActionPlan> {
80    match action {
81        DbAction::RelationRead { qi, .. } => {
82            let table = schema_cache.require_table(qi)?;
83            let read_plan = ReadPlan::from_request(request, table, schema_cache)?;
84            Ok(DbActionPlan::Read(ReadPlanTree::leaf(read_plan)))
85        }
86
87        DbAction::RelationMut { qi, mutation } => {
88            let table = schema_cache.require_table(qi)?;
89            let mutate_plan = MutatePlan::from_request(request, table, mutation)?;
90
91            let read_plan = if request.preferences.representation.needs_body() {
92                let rp = ReadPlan::for_mutation(request, table, schema_cache)?;
93                Some(ReadPlanTree::leaf(rp))
94            } else {
95                None
96            };
97
98            Ok(DbActionPlan::MutateRead {
99                mutate: mutate_plan,
100                read: read_plan,
101            })
102        }
103
104        DbAction::Routine {
105            qi,
106            invoke_method: _,
107        } => {
108            let routines = schema_cache
109                .get_routines(qi)
110                .ok_or_else(|| Error::FunctionNotFound(qi.to_string()))?;
111
112            let routine = routines
113                .first()
114                .ok_or_else(|| Error::FunctionNotFound(qi.to_string()))?;
115
116            let call_plan = CallPlan::from_request(request, routine)?;
117
118            Ok(DbActionPlan::Call {
119                call: call_plan,
120                read: None,
121            })
122        }
123
124        DbAction::SchemaRead { .. } => {
125            // This case is handled in create_action_plan before calling create_db_plan
126            unreachable!("SchemaRead should be handled in create_action_plan")
127        }
128    }
129}
130
131impl crate::api_request::PreferRepresentation {
132    /// Check if response body is needed.
133    pub fn needs_body(&self) -> bool {
134        matches!(self, Self::Full)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_info_plan() {
144        let qi = QualifiedIdentifier::new("public", "users");
145        let plan = ActionPlan::Info(InfoPlan::RelationInfo(qi.clone()));
146
147        match plan {
148            ActionPlan::Info(InfoPlan::RelationInfo(q)) => {
149                assert_eq!(q.name, "users");
150            }
151            _ => panic!("Expected RelationInfo"),
152        }
153    }
154}