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;