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, 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    /// Extra column equalities on the hop, ANDed with the key. Never set on a
31    /// [`ManyToMany`](JoinKind::ManyToMany) join.
32    #[serde(default, skip_serializing_if = "Vec::is_empty")]
33    pub on: Vec<ColumnPair>,
34    /// Whether a sink indexes the folded-in rows. `false` still builds them
35    /// into the stored document, but maps the whole subtree as one disabled
36    /// object: nothing under it is searchable, filterable or sortable, and it
37    /// costs one mapping field.
38    #[serde(
39        default = "super::field::enabled",
40        skip_serializing_if = "super::field::is_enabled"
41    )]
42    pub indexed: bool,
43}
44
45/// Hashes `indexed` only when turned off and `on` only when non-empty, so a
46/// schema that never opts in keeps the content hash (and so the physical
47/// index) it had before the fields existed.
48impl Hash for Join {
49    fn hash<H: Hasher>(&self, state: &mut H) {
50        let Self {
51            table,
52            kind,
53            primary_key,
54            nullable,
55            filters,
56            order_by,
57            limit,
58            fields,
59            on,
60            indexed,
61        } = self;
62        table.hash(state);
63        kind.hash(state);
64        primary_key.hash(state);
65        nullable.hash(state);
66        filters.hash(state);
67        order_by.hash(state);
68        limit.hash(state);
69        fields.hash(state);
70        if !indexed {
71            "indexed".hash(state);
72        }
73        hash_on("on", on, state);
74    }
75}
76
77/// One extra equality on a relation hop: `local` is a column of the table
78/// before the hop (the parent row), `related` a column of the hop's table.
79/// SQL equality, so a null on either side matches nothing.
80#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub struct ColumnPair {
82    /// The column on the table before the hop.
83    pub local: common::ColumnName,
84    /// The column on the hop's own table.
85    pub related: common::ColumnName,
86}
87
88/// Hashes a hop's `on` pairs only when there are any, so a relation without
89/// them keeps its content hash. `tag` keeps a `via` hop's pairs apart from the
90/// aggregate's own, which would otherwise hash to the same bytes.
91pub(super) fn hash_on<H: Hasher>(tag: &str, on: &[ColumnPair], state: &mut H) {
92    if !on.is_empty() {
93        tag.hash(state);
94        on.hash(state);
95    }
96}
97
98/// The relationship a join expresses. The verb carries both the cardinality
99/// and — the part that matters — **which table holds the key**:
100///
101/// - [`BelongsTo`](Self::BelongsTo): *this* row points at the related row
102///   (`column` is on the parent table) → a single object.
103/// - [`HasOne`](Self::HasOne) / [`HasMany`](Self::HasMany): the related rows
104///   point back at this one (`foreign_key` is on the related table) → a single
105///   object / a nested array.
106/// - [`ManyToMany`](Self::ManyToMany): both sides are keyed through a junction
107///   table → a nested array.
108#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum JoinKind {
111    BelongsTo { column: common::ColumnName },
112    HasOne { foreign_key: common::ColumnName },
113    HasMany { foreign_key: common::ColumnName },
114    ManyToMany { through: Through },
115}
116
117impl JoinKind {
118    /// Whether this join folds in many rows (a nested array) rather than one
119    /// (an object).
120    pub fn is_to_many(&self) -> bool {
121        matches!(self, JoinKind::HasMany { .. } | JoinKind::ManyToMany { .. })
122    }
123}
124
125/// How an aggregate's related rows tie back to the parent — a direct FK on the
126/// aggregated table, or a junction table. (Joins carry their key inside
127/// [`JoinKind`]; aggregates are inherently over-many, so `belongs_to` has no
128/// aggregate counterpart.)
129#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
130#[serde(rename_all = "snake_case")]
131pub enum AggregateKey {
132    Direct(common::ColumnName),
133    Through(Through),
134}
135
136/// A junction table linking two sides of a many-to-many relation.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct Through {
139    pub table: common::TableName,
140    pub left_key: common::ColumnName,
141    pub right_key: common::ColumnName,
142    /// Conditions on the junction rows themselves (e.g. skip cancelled order
143    /// lines), as opposed to the relation's own `filters` on the target table.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub filters: Option<Vec<Filter>>,
146}
147
148/// Hashes `filters` only when non-empty, so a schema that never opts in keeps
149/// the content hash (and so the physical index) it had before the field existed.
150impl Hash for Through {
151    fn hash<H: Hasher>(&self, state: &mut H) {
152        let Self {
153            table,
154            left_key,
155            right_key,
156            filters,
157        } = self;
158        table.hash(state);
159        left_key.hash(state);
160        right_key.hash(state);
161        if let Some(filters) = filters.as_deref().filter(|f| !f.is_empty()) {
162            filters.hash(state);
163        }
164    }
165}
166
167#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
168pub struct OrderBy {
169    pub column: common::ColumnName,
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub direction: Option<Direction>,
172}
173
174#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(rename_all = "snake_case")]
176pub enum Direction {
177    Asc,
178    Desc,
179}