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