1use 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
12const 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#[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#[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}