Skip to main content

sofie_db/
joins.rs

1//! A subquery in the select list, so conditions and orderings on the base stay unambiguous.
2
3use std::fmt::Write as _;
4
5use sofie_core::{Cast, SqlValue, Uuid};
6
7use crate::aggregate::Table;
8use crate::condition::Kind;
9use crate::selection::Selection;
10use crate::{Aggregate, Column, Dialect, Id, Selected};
11
12/// So a join to the base's own table still reads the base's row outside the subquery.
13const ALIAS: &str = "joined";
14
15#[derive(Clone, Debug)]
16pub(crate) struct Join {
17    via: &'static str,
18    table: (&'static str, &'static str),
19    column: String,
20    hidden: Vec<&'static str>,
21    owned_by: fn(Uuid) -> Option<Kind>,
22}
23
24/// Null where there is none, where it is hidden, or, in an owned area, another principal's.
25#[must_use]
26pub fn joined<B: Aggregate, J: Aggregate, T: Cast>(
27    via: Column<B, Id<J>>,
28    field: Column<J, T>,
29) -> Vec<Selected> {
30    T::COLUMNS
31        .iter()
32        .map(|column| {
33            Selected(Selection::Joined(Join {
34                via: via.name(),
35                table: (J::MODULE, J::NAME),
36                column: crate::sql::column_name(field.name(), column),
37                hidden: crate::aggregate::table::<J>().hiding().collect(),
38                owned_by: |owner| J::owned_by(owner).map(|condition| condition.kind),
39            }))
40        })
41        .collect()
42}
43
44/// The join reads null for a missing, hidden or other principal's record, so a joined field is
45/// an `Option`: of the column's type, or the column's own `Option`.
46#[diagnostic::on_unimplemented(
47    message = "a joined field must be an `Option` of the joined column's type",
48    note = "the joined record may be missing, hidden or another principal's"
49)]
50pub trait JoinedField<T> {}
51
52impl<T: Cast> JoinedField<T> for Option<T> {}
53
54impl<T: Cast> JoinedField<Option<T>> for Option<T> {}
55
56pub const fn joined_field<J, T, F: JoinedField<T>>(_field: Column<J, T>) {}
57
58impl Join {
59    #[cfg(feature = "live")]
60    pub(crate) fn table(&self) -> (&'static str, &'static str) {
61        self.table
62    }
63
64    pub(crate) fn render(
65        &self,
66        base: &Table,
67        dialect: &dyn Dialect,
68        owner: Option<Uuid>,
69        params: &mut Vec<SqlValue>,
70    ) -> String {
71        let alias = dialect.quote(ALIAS);
72        let (schema, name) = self.table;
73        let mut sql = format!(
74            "(SELECT {alias}.{} FROM {} AS {alias} WHERE {alias}.{} = {}.{}",
75            dialect.quote(&self.column),
76            dialect.qualified(schema, name),
77            dialect.quote("id"),
78            base.qualified(dialect),
79            dialect.quote(self.via),
80        );
81        for column in &self.hidden {
82            let _ = write!(sql, " AND {alias}.{} IS NULL", dialect.quote(column));
83        }
84        if let Some(condition) = owner.and_then(self.owned_by) {
85            sql.push_str(" AND ");
86            condition.render_into(dialect, params, &mut sql);
87        }
88        sql.push(')');
89        sql
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::aggregate::table;
97    use crate::aggregate::tests::{NUMBERED, Note};
98
99    #[test]
100    fn a_joined_field_is_an_option_of_the_column_or_the_column_s_own_option() {
101        let title: Column<Note, String> = Column::new("title");
102        let subtitle: Column<Note, Option<String>> = Column::new("subtitle");
103
104        joined_field::<_, _, Option<String>>(title);
105        joined_field::<_, _, Option<String>>(subtitle);
106    }
107
108    #[test]
109    fn a_joined_record_its_aggregate_hides_reads_as_none() {
110        let via: Column<Note, Id<Note>> = Column::new("parent");
111        let title: Column<Note, String> = Column::new("title");
112        let Some(Selected(Selection::Joined(join))) = joined(via, title).pop() else {
113            panic!("a joined column is a join");
114        };
115
116        let sql = join.render(table::<Note>(), &NUMBERED, None, &mut Vec::new());
117
118        assert!(
119            sql.ends_with(" AND \"joined\".\"deleted_at\" IS NULL)"),
120            "{sql}"
121        );
122    }
123}