Skip to main content

postrust_core/plan/
read_plan.rs

1//! Read (SELECT) query planning.
2
3use super::types::*;
4use crate::api_request::{ApiRequest, JoinType, QualifiedIdentifier, Range, SelectItem};
5use crate::error::{Error, Result};
6use crate::schema_cache::{Relationship, SchemaCache, Table};
7use serde::{Deserialize, Serialize};
8
9/// A read plan for a single table/view.
10#[derive(Clone, Debug, Serialize, Deserialize)]
11pub struct ReadPlan {
12    /// Columns to select
13    pub select: Vec<CoercibleSelectField>,
14    /// Source table
15    pub from: QualifiedIdentifier,
16    /// Table alias
17    pub from_alias: Option<String>,
18    /// WHERE conditions
19    pub where_clauses: Vec<CoercibleLogicTree>,
20    /// ORDER BY terms
21    pub order: Vec<CoercibleOrderTerm>,
22    /// Pagination range
23    pub range: Range,
24    /// Relation name (for embedding)
25    pub rel_name: String,
26    /// Relationship to parent (if embedded)
27    pub rel_to_parent: Option<Relationship>,
28    /// Join conditions
29    pub rel_join_conds: Vec<JoinCondition>,
30    /// Join type
31    pub rel_join_type: Option<JoinType>,
32    /// Embedded relations to select
33    pub rel_select: Vec<RelSelectField>,
34    /// Nesting depth
35    pub depth: u32,
36}
37
38impl ReadPlan {
39    /// Create a read plan from an API request.
40    pub fn from_request(
41        request: &ApiRequest,
42        table: &Table,
43        schema_cache: &SchemaCache,
44    ) -> Result<Self> {
45        let qi = table.qualified_identifier();
46
47        // Build select fields
48        let select = build_select_fields(&request.query_params.select, table)?;
49
50        // Build where clauses from filters
51        let where_clauses = build_where_clauses(request, table)?;
52
53        // Build order terms
54        let order = build_order_terms(request, table)?;
55
56        // Build relation selects for embedding
57        let rel_select = build_relation_selects(&request.query_params.select, table, schema_cache)?;
58
59        Ok(Self {
60            select,
61            from: qi,
62            from_alias: None,
63            where_clauses,
64            order,
65            range: resolve_top_level_range(request),
66            rel_name: table.name.clone(),
67            rel_to_parent: None,
68            rel_join_conds: vec![],
69            rel_join_type: None,
70            rel_select,
71            depth: 0,
72        })
73    }
74
75    /// Create a read plan for returning mutation results.
76    pub fn for_mutation(
77        request: &ApiRequest,
78        table: &Table,
79        schema_cache: &SchemaCache,
80    ) -> Result<Self> {
81        let mut plan = Self::from_request(request, table, schema_cache)?;
82        // For mutations, we select from the CTE result
83        plan.from_alias = Some("pgrst_mutation_result".to_string());
84        Ok(plan)
85    }
86
87    /// Check if this plan has any where clauses.
88    pub fn has_where(&self) -> bool {
89        !self.where_clauses.is_empty()
90    }
91
92    /// Check if this plan has any order terms.
93    pub fn has_order(&self) -> bool {
94        !self.order.is_empty()
95    }
96
97    /// Check if this plan has pagination.
98    pub fn has_pagination(&self) -> bool {
99        self.range.limit.is_some() || self.range.offset > 0
100    }
101}
102
103/// Build select fields from select items.
104fn build_select_fields(items: &[SelectItem], table: &Table) -> Result<Vec<CoercibleSelectField>> {
105    if items.is_empty() {
106        // Default: select all columns
107        return Ok(table
108            .columns
109            .iter()
110            .map(|(name, col)| CoercibleSelectField::simple(name, &col.data_type))
111            .collect());
112    }
113
114    let mut fields = Vec::new();
115
116    for item in items {
117        match item {
118            SelectItem::Field {
119                field,
120                aggregate,
121                aggregate_cast,
122                cast,
123                alias,
124            } => {
125                let column = table
126                    .get_column(&field.name)
127                    .ok_or_else(|| Error::ColumnNotFound(field.name.clone()))?;
128
129                fields.push(CoercibleSelectField {
130                    field: CoercibleField::from_field(field, &column.data_type),
131                    aggregate: aggregate.clone(),
132                    aggregate_cast: aggregate_cast.clone(),
133                    cast: cast.clone(),
134                    alias: alias.clone(),
135                });
136            }
137            // Relations are handled separately
138            SelectItem::Relation { .. } | SelectItem::SpreadRelation { .. } => {}
139        }
140    }
141
142    Ok(fields)
143}
144
145/// Resolve the effective top-level range.
146///
147/// The `Range` header supplies the base range; the `limit` and `offset` query
148/// parameters take precedence over it when present, matching PostgREST. The
149/// server-configured `max_rows` is then applied as a ceiling, so a request that
150/// asks for no limit -- or for more rows than the server permits -- cannot pull
151/// an unbounded result set into memory.
152fn resolve_top_level_range(request: &ApiRequest) -> Range {
153    let mut range = request.top_level_range.clone();
154
155    if let Some(from_params) = request.query_params.ranges.get("") {
156        if from_params.limit.is_some() {
157            range.limit = from_params.limit;
158        }
159        if from_params.offset != 0 {
160            range.offset = from_params.offset;
161        }
162    }
163
164    if let Some(max_rows) = request.max_rows {
165        range.limit = Some(match range.limit {
166            Some(requested) => requested.min(max_rows),
167            None => max_rows,
168        });
169    }
170
171    range
172}
173
174/// Build where clauses from request filters.
175fn build_where_clauses(request: &ApiRequest, table: &Table) -> Result<Vec<CoercibleLogicTree>> {
176    // `nominal_type` (the underlying `udt_name`) is used rather than `data_type`
177    // because it is always castable: `information_schema` reports arrays as
178    // `ARRAY` and enums as `USER-DEFINED`, neither valid in a `::type` cast.
179    let type_resolver = |name: &str| -> String {
180        table
181            .get_column(name)
182            .map(|c| c.nominal_type.clone())
183            .unwrap_or_else(|| "text".to_string())
184    };
185
186    let mut clauses = Vec::new();
187
188    // Add root filters
189    for filter in &request.query_params.filters_root {
190        let pg_type = type_resolver(&filter.field.name);
191        clauses.push(CoercibleLogicTree::Stmt(CoercibleFilter::from_filter(
192            filter, &pg_type,
193        )));
194    }
195
196    // Add logic trees
197    for (path, tree) in &request.query_params.logic {
198        if path.is_empty() {
199            clauses.push(CoercibleLogicTree::from_logic_tree(tree, type_resolver));
200        }
201    }
202
203    Ok(clauses)
204}
205
206/// Build order terms from request.
207fn build_order_terms(request: &ApiRequest, table: &Table) -> Result<Vec<CoercibleOrderTerm>> {
208    let mut terms = Vec::new();
209
210    for (path, order_terms) in &request.query_params.order {
211        if path.is_empty() {
212            for term in order_terms {
213                let field_name = match term {
214                    crate::api_request::OrderTerm::Field { field, .. } => &field.name,
215                    crate::api_request::OrderTerm::Relation { field, .. } => &field.name,
216                };
217
218                let pg_type = table
219                    .get_column(field_name)
220                    .map(|c| c.data_type.as_str())
221                    .unwrap_or("text");
222
223                terms.push(CoercibleOrderTerm::from_order_term(term, pg_type));
224            }
225        }
226    }
227
228    Ok(terms)
229}
230
231/// Build relation select fields for embedding.
232fn build_relation_selects(
233    items: &[SelectItem],
234    table: &Table,
235    schema_cache: &SchemaCache,
236) -> Result<Vec<RelSelectField>> {
237    let mut rel_selects = Vec::new();
238
239    for item in items {
240        match item {
241            SelectItem::Relation {
242                relation,
243                alias,
244                hint: _,
245                join_type,
246                select: _,
247            } => {
248                // Verify relationship exists
249                let _rel = schema_cache
250                    .find_relationship(&table.qualified_identifier(), relation, &table.schema)
251                    .ok_or_else(|| Error::RelationshipNotFound(relation.clone()))?;
252
253                rel_selects.push(RelSelectField {
254                    name: relation.clone(),
255                    agg_alias: alias
256                        .clone()
257                        .unwrap_or_else(|| format!("pgrst_{}", relation)),
258                    join_type: join_type.clone().unwrap_or_default(),
259                    is_spread: false,
260                });
261            }
262            SelectItem::SpreadRelation {
263                relation,
264                hint: _,
265                join_type,
266            } => {
267                let _rel = schema_cache
268                    .find_relationship(&table.qualified_identifier(), relation, &table.schema)
269                    .ok_or_else(|| Error::RelationshipNotFound(relation.clone()))?;
270
271                rel_selects.push(RelSelectField {
272                    name: relation.clone(),
273                    agg_alias: format!("pgrst_spread_{}", relation),
274                    join_type: join_type.clone().unwrap_or_default(),
275                    is_spread: true,
276                });
277            }
278            _ => {}
279        }
280    }
281
282    Ok(rel_selects)
283}
284
285/// A tree of read plans (for nested embedding).
286#[derive(Clone, Debug)]
287pub struct ReadPlanTree {
288    /// Root plan
289    pub root: ReadPlan,
290    /// Child plans (embedded resources)
291    pub children: Vec<ReadPlanTree>,
292}
293
294impl ReadPlanTree {
295    /// Create an empty tree.
296    pub fn empty() -> Self {
297        Self {
298            root: ReadPlan {
299                select: vec![],
300                from: QualifiedIdentifier::unqualified(""),
301                from_alias: None,
302                where_clauses: vec![],
303                order: vec![],
304                range: Range::default(),
305                rel_name: String::new(),
306                rel_to_parent: None,
307                rel_join_conds: vec![],
308                rel_join_type: None,
309                rel_select: vec![],
310                depth: 0,
311            },
312            children: vec![],
313        }
314    }
315
316    /// Create a leaf tree (no children).
317    pub fn leaf(plan: ReadPlan) -> Self {
318        Self {
319            root: plan,
320            children: vec![],
321        }
322    }
323
324    /// Add a child tree.
325    pub fn add_child(&mut self, child: ReadPlanTree) {
326        self.children.push(child);
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn test_read_plan_tree_empty() {
336        let tree = ReadPlanTree::empty();
337        assert!(tree.root.select.is_empty());
338        assert!(tree.children.is_empty());
339    }
340}