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 mut select = build_select_fields(&request.query_params.select, table)?;
49
50        // A schema may declare how a value of one of its domains is written on
51        // the wire -- a `color` as `"#01E240"` rather than as the integer it is
52        // stored as. The cast it declared does the rendering, in the database,
53        // which is also the only place that knows about it.
54        for field in select.iter_mut() {
55            if field.field.transform.is_some() || field.aggregate.is_some() {
56                continue;
57            }
58            let Some(column) = table.get_column(&field.field.name) else {
59                continue;
60            };
61            if let Some(function) =
62                schema_cache.representation(column.representation_type(), "json")
63            {
64                field.field.transform = Some(function.to_string());
65            }
66        }
67
68        // `Prefer: timezone` changes how a `timestamptz` reads, and only
69        // PostgreSQL knows the session's zone -- this process would render
70        // every one of them in UTC. Handing the rendering to the database is
71        // what PostgREST does for every value; here it is done only where the
72        // answer depends on it.
73        if request.preferences.timezone.is_some() {
74            for field in select.iter_mut() {
75                if field.cast.is_none()
76                    && field.aggregate.is_none()
77                    && field.field.json_path.is_empty()
78                    && matches!(
79                        field.field.ir_type.as_str(),
80                        "timestamptz"
81                            | "timetz"
82                            | "timestamp with time zone"
83                            | "time with time zone"
84                    )
85                {
86                    field.field.transform = Some("to_jsonb".to_string());
87                }
88            }
89        }
90
91        // Build where clauses from filters
92        let mut where_clauses = build_where_clauses(request, table)?;
93        for clause in where_clauses.iter_mut() {
94            attach_representation_tree(clause, table, schema_cache);
95        }
96
97        // Build order terms
98        let order = build_order_terms(request, table)?;
99
100        // Build relation selects for embedding
101        let rel_select = build_relation_selects(&request.query_params.select, table, schema_cache)?;
102
103        Ok(Self {
104            select,
105            from: qi,
106            from_alias: None,
107            where_clauses,
108            order,
109            range: resolve_top_level_range(request),
110            rel_name: table.name.clone(),
111            rel_to_parent: None,
112            rel_join_conds: vec![],
113            rel_join_type: None,
114            rel_select,
115            depth: 0,
116        })
117    }
118
119    /// Create a read plan for returning mutation results.
120    pub fn for_mutation(
121        request: &ApiRequest,
122        table: &Table,
123        schema_cache: &SchemaCache,
124    ) -> Result<Self> {
125        let mut plan = Self::from_request(request, table, schema_cache)?;
126
127        // The rows read are the ones the mutation returned, not the table's.
128        // The filters chose which rows to affect and have already done their
129        // work; applying them again here would be harmless for an update and
130        // wrong for a delete, whose rows are gone by then.
131        plan.from = QualifiedIdentifier::unqualified(crate::query::MUTATION_RESULT);
132        plan.from_alias = None;
133        plan.where_clauses.clear();
134
135        // A computed field is a function of the row it is read from, and the
136        // rows here are the statement's result rather than the table's.
137        for field in plan.select.iter_mut() {
138            if let Some(computed) = field.field.computed.as_mut() {
139                computed.relation = crate::query::MUTATION_RESULT.to_string();
140            }
141        }
142
143        Ok(plan)
144    }
145
146    /// Check if this plan has any where clauses.
147    pub fn has_where(&self) -> bool {
148        !self.where_clauses.is_empty()
149    }
150
151    /// Check if this plan has any order terms.
152    pub fn has_order(&self) -> bool {
153        !self.order.is_empty()
154    }
155
156    /// Check if this plan has pagination.
157    pub fn has_pagination(&self) -> bool {
158        self.range.limit.is_some() || self.range.offset > 0
159    }
160}
161
162/// Build select fields from select items.
163fn build_select_fields(items: &[SelectItem], table: &Table) -> Result<Vec<CoercibleSelectField>> {
164    if items.is_empty() {
165        // Default: select all columns
166        return Ok(table
167            .columns
168            .iter()
169            .map(|(name, col)| CoercibleSelectField::simple(name, &col.data_type))
170            .collect());
171    }
172
173    let mut fields = Vec::new();
174
175    for item in items {
176        match item {
177            // `*` stands for every column, and may sit alongside others.
178            SelectItem::Field { field, .. } if field.name == "*" => {
179                fields.extend(
180                    table
181                        .columns
182                        .iter()
183                        .map(|(name, col)| CoercibleSelectField::simple(name, &col.data_type)),
184                );
185            }
186            SelectItem::Field {
187                field,
188                aggregate,
189                aggregate_cast,
190                cast,
191                alias,
192            } => {
193                // A bare `count` means COUNT(*) -- PostgREST's original
194                // spelling, still accepted and, unlike `count()`, not gated
195                // behind db-aggregates-enabled. A table that really has a
196                // column of that name wins, which is why this is decided here
197                // rather than in the parser.
198                let legacy_count = aggregate.is_none()
199                    && field.name == "count"
200                    && field.json_path.is_empty()
201                    && table.get_column("count").is_none();
202
203                // `count()` has no column behind it, so there is nothing to
204                // resolve and no type to carry.
205                // A name the table has no column for may still be a function
206                // of its row type, which reads as a column.
207                let computed = match table.get_column(&field.name) {
208                    Some(_) => None,
209                    // A computed column that takes anything besides the row
210                    // is not callable here: this surface has nowhere to write
211                    // an argument and no session document to supply.
212                    None => table
213                        .get_computed_column(&field.name)
214                        .filter(|c| !c.takes_arguments),
215                };
216
217                let pg_type = if field.name.is_empty() || legacy_count {
218                    String::new()
219                } else if let Some(computed) = computed {
220                    computed.data_type.clone()
221                } else {
222                    table
223                        .get_column(&field.name)
224                        .ok_or_else(|| Error::ColumnNotFound(field.name.clone()))?
225                        .data_type
226                        .clone()
227                };
228
229                let (field, aggregate) = if legacy_count {
230                    (
231                        &crate::api_request::Field::simple(""),
232                        &Some(crate::api_request::AggregateFunction::Count),
233                    )
234                } else {
235                    (field, aggregate)
236                };
237
238                let mut resolved = CoercibleField::from_field(field, &pg_type);
239                resolved.computed = computed.map(|computed| crate::plan::ComputedRef {
240                    function: computed.function.clone(),
241                    relation: table.name.clone(),
242                    row_type: table.qualified_identifier(),
243                });
244
245                fields.push(CoercibleSelectField {
246                    field: resolved,
247                    aggregate: aggregate.clone(),
248                    aggregate_cast: aggregate_cast.clone(),
249                    cast: cast.clone(),
250                    alias: alias.clone(),
251                });
252            }
253            // Relations are handled separately
254            SelectItem::Relation { .. } | SelectItem::SpreadRelation { .. } => {}
255        }
256    }
257
258    Ok(fields)
259}
260
261/// Resolve the effective top-level range.
262///
263/// The `Range` header supplies the base range; the `limit` and `offset` query
264/// parameters take precedence over it when present, matching PostgREST. The
265/// server-configured `max_rows` is then applied as a ceiling, so a request that
266/// asks for no limit -- or for more rows than the server permits -- cannot pull
267/// an unbounded result set into memory.
268pub fn resolve_top_level_range(request: &ApiRequest) -> Range {
269    let mut range = request.top_level_range.clone();
270
271    if let Some(from_params) = request.query_params.ranges.get("") {
272        if from_params.limit.is_some() {
273            range.limit = from_params.limit;
274        }
275        if from_params.offset_explicit {
276            range.offset = from_params.offset;
277        }
278    }
279
280    if let Some(max_rows) = request.max_rows {
281        range.limit = Some(match range.limit {
282            Some(requested) => requested.min(max_rows),
283            None => max_rows,
284        });
285    }
286
287    range
288}
289
290/// Build where clauses from request filters.
291fn build_where_clauses(request: &ApiRequest, table: &Table) -> Result<Vec<CoercibleLogicTree>> {
292    // `nominal_type` (the underlying `udt_name`) is used rather than `data_type`
293    // because it is always castable: `information_schema` reports arrays as
294    // `ARRAY` and enums as `USER-DEFINED`, neither valid in a `::type` cast.
295    let type_resolver = |name: &str| -> String {
296        table
297            .get_column(name)
298            .map(|c| c.nominal_type.clone())
299            .unwrap_or_else(|| "text".to_string())
300    };
301
302    // `?clients=is.null` names an embedded resource, not a column: it asks
303    // whether the embed matched. That is decided where the embed exists, which
304    // is above this query, so it is left out here. A real column of the same
305    // name wins, since then the filter means what it usually means.
306    let embedded: std::collections::HashSet<&str> = request
307        .query_params
308        .select
309        .iter()
310        .filter_map(|item| match item {
311            SelectItem::Relation {
312                relation, alias, ..
313            } => Some(alias.as_deref().unwrap_or(relation)),
314            SelectItem::SpreadRelation { relation, .. } => Some(relation.as_str()),
315            SelectItem::Field { .. } => None,
316        })
317        .filter(|name| table.get_column(name).is_none())
318        .collect();
319
320    let mut clauses = Vec::new();
321
322    // Add root filters
323    for filter in &request.query_params.filters_root {
324        if embedded.contains(filter.field.name.as_str()) {
325            continue;
326        }
327        let pg_type = type_resolver(&filter.field.name);
328        clauses.push(CoercibleLogicTree::Stmt(CoercibleFilter::from_filter(
329            filter, &pg_type,
330        )));
331    }
332
333    // Add logic trees.
334    //
335    // One made entirely of embedded resource names is about whether those
336    // embeds matched, which nothing here can answer -- the embeds are not in
337    // scope until they have been joined. It is applied out there instead.
338    let embeds = crate::api_request::embedded_names(&request.query_params.select);
339    for (path, tree) in &request.query_params.logic {
340        if path.is_empty() && !tree.names_only(&embeds) {
341            clauses.push(CoercibleLogicTree::from_logic_tree(tree, type_resolver));
342        }
343    }
344
345    for clause in clauses.iter_mut() {
346        attach_computed_tree(clause, table);
347    }
348
349    Ok(clauses)
350}
351
352/// Point a filter's value at the function that parses it.
353///
354/// The mirror of the output representation: where a schema declares a cast
355/// from `text` to one of its domains, a filter value written in the domain's
356/// own spelling is read by that cast rather than by PostgreSQL's input
357/// function for the underlying type.
358fn attach_representation(field: &mut CoercibleField, table: &Table, schema_cache: &SchemaCache) {
359    let Some(column) = table.get_column(&field.name) else {
360        return;
361    };
362    if let Some(function) = schema_cache.representation("text", column.representation_type()) {
363        field.transform = Some(function.to_string());
364    }
365}
366
367/// Walk a logic tree, giving every filter in it its input representation.
368fn attach_representation_tree(
369    tree: &mut CoercibleLogicTree,
370    table: &Table,
371    schema_cache: &SchemaCache,
372) {
373    match tree {
374        CoercibleLogicTree::Expr { children, .. } => {
375            for child in children {
376                attach_representation_tree(child, table, schema_cache);
377            }
378        }
379        CoercibleLogicTree::Stmt(filter) => {
380            attach_representation(&mut filter.field, table, schema_cache)
381        }
382        CoercibleLogicTree::NullEmbed { .. } => {}
383    }
384}
385
386/// Point a field at the function behind it, where the table has no such column.
387///
388/// A filter or an order term names a field the same way a select does, so a
389/// computed field has to be recognised in all three -- `?always_true=is.true`
390/// and `?order=anti_id.desc` are as much a part of PostgREST's contract as
391/// `?select=always_true`.
392fn attach_computed(field: &mut CoercibleField, table: &Table) {
393    if table.get_column(&field.name).is_some() {
394        return;
395    }
396    if let Some(computed) = table
397        .get_computed_column(&field.name)
398        .filter(|c| !c.takes_arguments)
399    {
400        field.ir_type = computed.data_type.clone();
401        field.base_type = computed.data_type.clone();
402        field.computed = Some(crate::plan::ComputedRef {
403            function: computed.function.clone(),
404            relation: table.name.clone(),
405            row_type: table.qualified_identifier(),
406        });
407    }
408}
409
410/// Walk a logic tree, resolving every field it names.
411fn attach_computed_tree(tree: &mut CoercibleLogicTree, table: &Table) {
412    match tree {
413        CoercibleLogicTree::Expr { children, .. } => {
414            for child in children {
415                attach_computed_tree(child, table);
416            }
417        }
418        CoercibleLogicTree::Stmt(filter) => attach_computed(&mut filter.field, table),
419        CoercibleLogicTree::NullEmbed { .. } => {}
420    }
421}
422
423/// Build order terms from request.
424fn build_order_terms(request: &ApiRequest, table: &Table) -> Result<Vec<CoercibleOrderTerm>> {
425    let mut terms = Vec::new();
426
427    for (path, order_terms) in &request.query_params.order {
428        if path.is_empty() {
429            for term in order_terms {
430                let field_name = match term {
431                    crate::api_request::OrderTerm::Field { field, .. } => &field.name,
432                    crate::api_request::OrderTerm::Relation { field, .. } => &field.name,
433                };
434
435                let pg_type = table
436                    .get_column(field_name)
437                    .map(|c| c.data_type.as_str())
438                    .unwrap_or("text");
439
440                let mut resolved = CoercibleOrderTerm::from_order_term(term, pg_type);
441                attach_computed(&mut resolved.field, table);
442                terms.push(resolved);
443            }
444        }
445    }
446
447    Ok(terms)
448}
449
450/// Build relation select fields for embedding.
451fn build_relation_selects(
452    items: &[SelectItem],
453    table: &Table,
454    schema_cache: &SchemaCache,
455) -> Result<Vec<RelSelectField>> {
456    let mut rel_selects = Vec::new();
457
458    for item in items {
459        match item {
460            SelectItem::Relation {
461                relation,
462                alias,
463                hint,
464                join_type,
465                select: _,
466            } => {
467                // Verify relationship exists
468                let _rel = schema_cache
469                    .find_relationship(
470                        &table.qualified_identifier(),
471                        relation,
472                        hint.as_deref(),
473                        &table.schema,
474                    )?
475                    .ok_or_else(|| {
476                        schema_cache.relationship_not_found(
477                            &table.qualified_identifier(),
478                            relation,
479                            hint.as_deref(),
480                            &table.schema,
481                        )
482                    })?;
483
484                rel_selects.push(RelSelectField {
485                    name: relation.clone(),
486                    agg_alias: alias
487                        .clone()
488                        .unwrap_or_else(|| format!("pgrst_{}", relation)),
489                    join_type: join_type.clone().unwrap_or_default(),
490                    is_spread: false,
491                });
492            }
493            SelectItem::SpreadRelation {
494                relation,
495                hint,
496                join_type,
497                select: _,
498            } => {
499                let _rel = schema_cache
500                    .find_relationship(
501                        &table.qualified_identifier(),
502                        relation,
503                        hint.as_deref(),
504                        &table.schema,
505                    )?
506                    .ok_or_else(|| {
507                        schema_cache.relationship_not_found(
508                            &table.qualified_identifier(),
509                            relation,
510                            hint.as_deref(),
511                            &table.schema,
512                        )
513                    })?;
514
515                rel_selects.push(RelSelectField {
516                    name: relation.clone(),
517                    agg_alias: format!("pgrst_spread_{}", relation),
518                    join_type: join_type.clone().unwrap_or_default(),
519                    is_spread: true,
520                });
521            }
522            _ => {}
523        }
524    }
525
526    Ok(rel_selects)
527}
528
529/// A tree of read plans (for nested embedding).
530#[derive(Clone, Debug)]
531pub struct ReadPlanTree {
532    /// Root plan
533    pub root: ReadPlan,
534    /// Child plans (embedded resources)
535    pub children: Vec<ReadPlanTree>,
536}
537
538impl ReadPlanTree {
539    /// Create an empty tree.
540    pub fn empty() -> Self {
541        Self {
542            root: ReadPlan {
543                select: vec![],
544                from: QualifiedIdentifier::unqualified(""),
545                from_alias: None,
546                where_clauses: vec![],
547                order: vec![],
548                range: Range::default(),
549                rel_name: String::new(),
550                rel_to_parent: None,
551                rel_join_conds: vec![],
552                rel_join_type: None,
553                rel_select: vec![],
554                depth: 0,
555            },
556            children: vec![],
557        }
558    }
559
560    /// Create a leaf tree (no children).
561    pub fn leaf(plan: ReadPlan) -> Self {
562        Self {
563            root: plan,
564            children: vec![],
565        }
566    }
567
568    /// Add a child tree.
569    pub fn add_child(&mut self, child: ReadPlanTree) {
570        self.children.push(child);
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    #[test]
579    fn test_read_plan_tree_empty() {
580        let tree = ReadPlanTree::empty();
581        assert!(tree.root.select.is_empty());
582        assert!(tree.children.is_empty());
583    }
584}