Skip to main content

cratestack_sql/
relation_scope.rs

1//! What a relation subquery may see of its *related* table.
2//!
3//! A relation filter (`EXISTS (SELECT 1 FROM related WHERE ...)`) or a
4//! relation sort (`(SELECT related.col FROM related WHERE ... LIMIT 1)`)
5//! reads the related table directly. Unless that subquery applies the
6//! related model's own read policy and soft-delete filter, a caller can
7//! test and order by values of rows they cannot read — rows `?include=`
8//! correctly reports as `null` / absent (GHSA-p55v-6xv5-93p3).
9//!
10//! Every public constructor of a relation hop, relation filter or
11//! relation sort therefore takes a [`RelatedReadScope`]; there is no
12//! default. Generated code passes the related model's
13//! [`ModelDescriptor::related_read_scope`]. Hand-written code that
14//! genuinely wants the raw table passes [`RelatedReadScope::Unscoped`],
15//! which is the named escape hatch and never the fallback.
16
17use cratestack_policy::ReadPolicy;
18
19use crate::descriptor::ModelDescriptor;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum RelatedReadScope {
23    /// Apply the related model's list-slot read policy (`@@allow`/`@@deny`
24    /// on `read`/`list`) and its `@@soft_delete` filter inside the
25    /// subquery — exactly what `find_many` on that model applies, which
26    /// is also what `?include=` goes through. A related row the caller
27    /// cannot read then behaves as if it did not exist: a to-one filter
28    /// (including `ne` and `isNull`) does not match it, `none`/`every`
29    /// over hidden-only children are vacuously true, and a relation sort
30    /// key reads as `NULL`.
31    ///
32    /// An empty `allow` slice renders `FALSE` (default deny), the same as
33    /// a direct read of a model with no read rule.
34    Policy {
35        allow: &'static [ReadPolicy],
36        deny: &'static [ReadPolicy],
37        /// `Some("deleted_at")` when the related model is `@@soft_delete`.
38        soft_delete_column: Option<&'static str>,
39    },
40    /// Escape hatch: read the related table raw — no policy, no
41    /// soft-delete filter. For trusted server code that deliberately wants
42    /// every related row regardless of the caller. The embedded (rusqlite)
43    /// backend ignores the scope either way. Generated client code (which
44    /// never renders SQL) also uses it. Never use it for a filter or sort
45    /// whose values a caller controls.
46    Unscoped,
47}
48
49impl<M, PK> ModelDescriptor<M, PK> {
50    /// The scope a relation subquery into this model must apply: the same
51    /// list-slot policies and soft-delete filter `find_many` applies.
52    pub const fn related_read_scope(&self) -> RelatedReadScope {
53        RelatedReadScope::Policy {
54            allow: self.read_allow_policies,
55            deny: self.read_deny_policies,
56            soft_delete_column: self.soft_delete_column,
57        }
58    }
59}