Skip to main content

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}