keelson_models/set.rs
1use keelson_core::expr::Expr;
2use keelson_core::{ToValue, Value};
3
4/// One field of a generated `Setter`: unset, `NULL`, or a value — three states,
5/// distinguished by type.
6///
7/// The three states are the whole point (bob's `omit`/`null`/`value`): an
8/// **unset** field does not appear in the `INSERT` or `UPDATE` at all — the
9/// column keeps its database default on insert and its current value on update
10/// — while **`Null`** writes SQL `NULL` explicitly. Collapsing the first two
11/// into `Option` would make "leave it alone" and "erase it" the same call
12/// site, which is precisely the bug this type exists to prevent.
13///
14/// `Default` is [`Unset`](Set::Unset), which is what makes the struct-update
15/// spelling work:
16///
17/// ```ignore
18/// users::table().insert(users::Setter {
19/// name: set("Stephen"),
20/// email: null(),
21/// ..Default::default() // every other column: untouched
22/// })
23/// ```
24///
25/// `Null` is representable for every column, `NOT NULL` ones included — the
26/// constraint violation is the database's to report, exactly as it is for raw
27/// SQL. Encoding nullability in the `Setter`'s types was considered and
28/// rejected: it would split `Set<T>` into two vocabularies and make every
29/// generated field's type depend on a constraint the schema can change,
30/// for a check the engine performs anyway.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum Set<T> {
33 /// Not mentioned: the field contributes nothing to the statement.
34 #[default]
35 Unset,
36 /// Explicitly `NULL`, bound as an argument.
37 Null,
38 /// A value, bound as an argument.
39 Value(T),
40}
41
42/// A set field: `set("Stephen")` is `Set::Value("Stephen".into())`.
43///
44/// Takes `impl Into<T>` so the ergonomic literals work — `set("Stephen")` for
45/// a `String` column — while the target type still comes from the `Setter`
46/// field, so `set` stays as typed as the column it lands in.
47pub fn set<T>(value: impl Into<T>) -> Set<T> {
48 Set::Value(value.into())
49}
50
51/// An explicit SQL `NULL` — [`Set::Null`], spelled the way the sketch spells
52/// it.
53pub fn null<T>() -> Set<T> {
54 Set::Null
55}
56
57impl<T> Set<T> {
58 /// Whether the field is [`Unset`](Set::Unset) and so contributes nothing.
59 pub fn is_unset(&self) -> bool {
60 matches!(self, Set::Unset)
61 }
62}
63
64impl<T: ToValue> Set<T> {
65 /// The bound expression this field contributes: `None` when unset, a bound
66 /// `NULL` for [`Set::Null`], a bound argument for a value.
67 ///
68 /// Always a placeholder, never an inline literal — the same binding-only
69 /// contract every mapped type follows (`docs/type-mappings.md`).
70 pub fn into_expr(self) -> Option<Expr> {
71 match self {
72 Set::Unset => None,
73 Set::Null => Some(Expr::Arg(Value::Null)),
74 Set::Value(v) => Some(Expr::Arg(v.to_value())),
75 }
76 }
77
78 /// Contribute this field to an `INSERT`'s column and value lists — one
79 /// call per field is the shape generated `insert_query` bodies take.
80 pub fn push_into(
81 self,
82 column: &'static str,
83 columns: &mut Vec<&'static str>,
84 values: &mut Vec<Expr>,
85 ) {
86 if let Some(e) = self.into_expr() {
87 columns.push(column);
88 values.push(e);
89 }
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn default_is_unset_so_struct_update_syntax_leaves_fields_out() {
99 #[derive(Default)]
100 struct Setter {
101 name: Set<String>,
102 email: Set<String>,
103 age: Set<i32>,
104 }
105
106 let s = Setter {
107 name: set("Stephen"),
108 email: null(),
109 ..Default::default()
110 };
111 assert_eq!(s.name, Set::Value("Stephen".to_owned()));
112 assert_eq!(s.email, Set::Null);
113 assert_eq!(s.age, Set::Unset);
114 assert!(s.age.is_unset());
115 }
116
117 #[test]
118 fn the_three_states_produce_nothing_null_and_a_bound_value() {
119 assert!(Set::<i32>::Unset.into_expr().is_none());
120 assert!(matches!(
121 null::<i32>().into_expr(),
122 Some(Expr::Arg(Value::Null))
123 ));
124 assert!(matches!(
125 set::<i32>(7).into_expr(),
126 Some(Expr::Arg(Value::I32(7)))
127 ));
128 }
129
130 #[test]
131 fn push_into_skips_unset_fields_entirely() {
132 let mut cols = Vec::new();
133 let mut vals = Vec::new();
134 set::<String>("ada").push_into("name", &mut cols, &mut vals);
135 Set::<String>::Unset.push_into("email", &mut cols, &mut vals);
136 null::<i32>().push_into("age", &mut cols, &mut vals);
137 assert_eq!(cols, vec!["name", "age"]);
138 assert_eq!(vals.len(), 2, "the unset column binds nothing at all");
139 }
140}