Skip to main content

cratestack_sql/
order_catalog.rs

1//! Runtime dotted-key resolution for REST `?orderBy=`/`?sort=` keys that
2//! cross to-one relations (`"author.profile.nickname"`).
3//!
4//! Mirrors the type-space-to-value-space move `relation_path` already made
5//! for the typed builder (cratestack#253) — one `OrderCatalog` per model,
6//! carrying only that model's own scalar columns and its own to-one
7//! relation edges, rather than a pre-enumerated list of every dotted path
8//! through the graph. [`resolve_order_target`] walks a key hop by hop
9//! against these catalogs at request time, so codegen for the REST
10//! dispatch surface stays linear in `models × fields` however densely
11//! models are to-one-connected (cratestack#256 — the same exponential
12//! shape as #252, but in the REST string-key match arms rather than the
13//! typed builder's path types).
14
15use crate::relation_path::RelationHop;
16
17/// One model's order-by surface: its own sortable scalar columns
18/// (`(api_name, sql_column)`) and its own to-one relation edges. Exactly
19/// one `OrderCatalog` is emitted per model, regardless of how many
20/// distinct relation paths pass through it.
21pub struct OrderCatalog {
22    pub scalars: &'static [(&'static str, &'static str)],
23    pub relations: &'static [OrderRelationEdge],
24}
25
26/// One to-one relation edge out of a model. `target` points at the
27/// related model's own catalog so [`resolve_order_target`] can keep
28/// walking further segments; to-many relations are never represented
29/// here (mirroring the codegen's existing to-one-only walk), so a key
30/// that names one simply fails to resolve.
31pub struct OrderRelationEdge {
32    pub api_name: &'static str,
33    pub hop: RelationHop,
34    pub target: &'static OrderCatalog,
35}
36
37/// A dotted sort key resolved down to the relation hops to traverse plus
38/// the terminal scalar column, ready for [`crate::OrderClause::relation_path`]
39/// (each hop carries the related model's read scope).
40pub struct ResolvedOrderTarget {
41    pub hops: Vec<RelationHop>,
42    pub column: &'static str,
43}
44
45/// Walk `key` (dot-separated, e.g. `"author.profile.nickname"`) through
46/// `catalog`, following to-one relation edges one segment at a time and
47/// resolving the final segment against the current model's scalar
48/// columns.
49///
50/// Returns `None` for an unknown field, a relation segment with no
51/// matching edge (including any to-many hop, which is never present in
52/// the catalog), or a key whose last segment names a relation instead of
53/// a scalar — every one of which the caller reports as the same
54/// "unsupported sort field" validation error as any other bad key.
55pub fn resolve_order_target(
56    catalog: &'static OrderCatalog,
57    key: &str,
58) -> Option<ResolvedOrderTarget> {
59    let mut hops = Vec::new();
60    let mut current = catalog;
61    let mut segments = key.split('.').peekable();
62
63    loop {
64        let segment = segments.next()?;
65        if segments.peek().is_none() {
66            return current
67                .scalars
68                .iter()
69                .find(|(name, _)| *name == segment)
70                .map(|(_, column)| ResolvedOrderTarget { hops, column });
71        }
72        let edge = current
73            .relations
74            .iter()
75            .find(|edge| edge.api_name == segment)?;
76        hops.push(edge.hop);
77        current = edge.target;
78    }
79}
80
81#[cfg(test)]
82#[path = "order_catalog_tests.rs"]
83mod tests;