Skip to main content

kernel/config/
join.rs

1use std::hash::{Hash, Hasher};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{Field, common};
6
7use super::Filter;
8
9/// Folds rows from a related `table` into the document. The [`kind`](Self::kind)
10/// names the relationship — which side carries the key, and whether one row or
11/// many fold in; `filters`, `order_by`, and `limit` narrow and shape the rows
12/// that come back.
13#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
14pub struct Join {
15    pub table: common::TableName,
16    pub kind: JoinKind,
17    pub primary_key: common::ColumnName,
18    /// Whether the folded-in object may be absent. Only meaningful for a to-one
19    /// join (`belongs_to`/`has_one`); a to-many join is always a non-null array.
20    /// A to-one join defaults to nullable unless the schema marks it `required`.
21    #[serde(default)]
22    pub nullable: bool,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub filters: Option<Vec<Filter>>,
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub order_by: Option<Vec<OrderBy>>,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub limit: Option<u64>,
29    pub fields: Vec<Field>,
30}
31
32/// The relationship a join expresses. The verb carries both the cardinality
33/// and — the part that matters — **which table holds the key**:
34///
35/// - [`BelongsTo`](Self::BelongsTo): *this* row points at the related row
36///   (`column` is on the parent table) → a single object.
37/// - [`HasOne`](Self::HasOne) / [`HasMany`](Self::HasMany): the related rows
38///   point back at this one (`foreign_key` is on the related table) → a single
39///   object / a nested array.
40/// - [`ManyToMany`](Self::ManyToMany): both sides are keyed through a junction
41///   table → a nested array.
42#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum JoinKind {
45    BelongsTo { column: common::ColumnName },
46    HasOne { foreign_key: common::ColumnName },
47    HasMany { foreign_key: common::ColumnName },
48    ManyToMany { through: Through },
49}
50
51impl JoinKind {
52    /// Whether this join folds in many rows (a nested array) rather than one
53    /// (an object).
54    pub fn is_to_many(&self) -> bool {
55        matches!(self, JoinKind::HasMany { .. } | JoinKind::ManyToMany { .. })
56    }
57}
58
59/// How an aggregate's related rows tie back to the parent — a direct FK on the
60/// aggregated table, or a junction table. (Joins carry their key inside
61/// [`JoinKind`]; aggregates are inherently over-many, so `belongs_to` has no
62/// aggregate counterpart.)
63#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum AggregateKey {
66    Direct(common::ColumnName),
67    Through(Through),
68}
69
70/// A junction table linking two sides of a many-to-many relation.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Through {
73    pub table: common::TableName,
74    pub left_key: common::ColumnName,
75    pub right_key: common::ColumnName,
76    /// Conditions on the junction rows themselves (e.g. skip cancelled order
77    /// lines), as opposed to the relation's own `filters` on the target table.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub filters: Option<Vec<Filter>>,
80}
81
82/// Hashes `filters` only when non-empty, so a schema that never opts in keeps
83/// the content hash (and so the physical index) it had before the field existed.
84impl Hash for Through {
85    fn hash<H: Hasher>(&self, state: &mut H) {
86        let Self {
87            table,
88            left_key,
89            right_key,
90            filters,
91        } = self;
92        table.hash(state);
93        left_key.hash(state);
94        right_key.hash(state);
95        if let Some(filters) = filters.as_deref().filter(|f| !f.is_empty()) {
96            filters.hash(state);
97        }
98    }
99}
100
101#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
102pub struct OrderBy {
103    pub column: common::ColumnName,
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub direction: Option<Direction>,
106}
107
108#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum Direction {
111    Asc,
112    Desc,
113}