Skip to main content

prax_query/inputs/
write_payload.rs

1//! Runtime payload shapes for typed write inputs.
2//!
3//! Phase 5a wires the codegen-emitted `<Model>CreateInput` and
4//! `<Model>UpdateInput` structs into the existing `CreateOperation` /
5//! `UpdateOperation` / `UpdateManyOperation` / `UpsertOperation`
6//! builders. The translation is purely additive: each input lowers
7//! to a list of column-keyed assignments that the builder appends
8//! to its existing internal state.
9//!
10//! - `CreateInput::Data` is fixed to [`CreatePayload`] — the same
11//!   `Vec<(column, value)>` shape that the existing
12//!   `CreateOperation::set_many` already accepts.
13//! - `UpdateInput::Data` is fixed to [`UpdatePayload`] — a list of
14//!   `(column, WriteOp)` pairs that carry the atomic-operator
15//!   information `IntFieldUpdate { increment, decrement, multiply,
16//!   divide, set }`, `StringNullableFieldUpdate { unset }`, etc.
17//!   express.
18//!
19//! Nested writes (relation operators inside `data:`) are deferred to
20//! phase 5b and have no payload here — phase 5a's codegen rejects
21//! relation keys with a clear "phase 5b" diagnostic before any
22//! lowering reaches the runtime.
23
24use crate::filter::FilterValue;
25
26/// Flat column-value payload for a single-row create.
27///
28/// Each tuple is `(column_name, value)`. Codegen emits this as the
29/// `Data` associated type for every per-model `<Model>CreateInput`
30/// in phase 5a.
31pub type CreatePayload = Vec<(String, FilterValue)>;
32
33/// Flat column-operator payload for an update.
34///
35/// Each tuple is `(column_name, WriteOp)`. Codegen emits this as the
36/// `Data` associated type for every per-model `<Model>UpdateInput`
37/// in phase 5a. Atomic operators (`increment`, `decrement`,
38/// `multiply`, `divide`) are preserved as distinct variants so the
39/// SQL builder can emit `col = col + $n` rather than `col = $n`.
40pub type UpdatePayload = Vec<(String, WriteOp)>;
41
42/// One scalar update operator applied to a column.
43///
44/// Mirrors the `*FieldUpdate` wrapper structs in
45/// [`crate::inputs::scalar_update`]. Each wrapper lowers exactly one
46/// of its set-only / increment / decrement / multiply / divide / unset
47/// fields to the matching variant here.
48#[derive(Debug, Clone, PartialEq)]
49pub enum WriteOp {
50    /// `col = value` — the default form.
51    Set(FilterValue),
52    /// `col = col + value` — numeric scalars only.
53    Increment(FilterValue),
54    /// `col = col - value` — numeric scalars only.
55    Decrement(FilterValue),
56    /// `col = col * value` — numeric scalars only.
57    Multiply(FilterValue),
58    /// `col = col / value` — numeric scalars only.
59    Divide(FilterValue),
60    /// `col = NULL` — nullable scalars only.
61    Unset,
62}
63
64impl WriteOp {
65    /// True when the operator targets a numeric column.
66    ///
67    /// Used by the SQL emitter to pick between `col = $n` (Set/Unset)
68    /// and `col = col <op> $n` (the arithmetic variants).
69    pub fn is_arithmetic(&self) -> bool {
70        matches!(
71            self,
72            WriteOp::Increment(_)
73                | WriteOp::Decrement(_)
74                | WriteOp::Multiply(_)
75                | WriteOp::Divide(_)
76        )
77    }
78
79    /// Render this operator as a SET-clause fragment.
80    ///
81    /// Given the column name and a `1..` placeholder offset, returns
82    /// the textual fragment (`col = $1`, `col = col + $1`, `col = NULL`)
83    /// and the value to push to the parameter list. `Unset` returns
84    /// `None` for the value — the caller skips the parameter slot.
85    pub fn to_set_fragment(
86        &self,
87        column: &str,
88        placeholder: &str,
89    ) -> (String, Option<FilterValue>) {
90        match self {
91            WriteOp::Set(v) => (format!("{column} = {placeholder}"), Some(v.clone())),
92            WriteOp::Increment(v) => (
93                format!("{column} = {column} + {placeholder}"),
94                Some(v.clone()),
95            ),
96            WriteOp::Decrement(v) => (
97                format!("{column} = {column} - {placeholder}"),
98                Some(v.clone()),
99            ),
100            WriteOp::Multiply(v) => (
101                format!("{column} = {column} * {placeholder}"),
102                Some(v.clone()),
103            ),
104            WriteOp::Divide(v) => (
105                format!("{column} = {column} / {placeholder}"),
106                Some(v.clone()),
107            ),
108            WriteOp::Unset => (format!("{column} = NULL"), None),
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn write_op_set_fragment() {
119        let op = WriteOp::Set(FilterValue::Int(7));
120        let (frag, val) = op.to_set_fragment("age", "$1");
121        assert_eq!(frag, "age = $1");
122        assert_eq!(val, Some(FilterValue::Int(7)));
123    }
124
125    #[test]
126    fn write_op_increment_fragment() {
127        let op = WriteOp::Increment(FilterValue::Int(1));
128        let (frag, val) = op.to_set_fragment("count", "$2");
129        assert_eq!(frag, "count = count + $2");
130        assert_eq!(val, Some(FilterValue::Int(1)));
131    }
132
133    #[test]
134    fn write_op_unset_skips_param() {
135        let op = WriteOp::Unset;
136        let (frag, val) = op.to_set_fragment("name", "$1");
137        assert_eq!(frag, "name = NULL");
138        assert!(val.is_none());
139    }
140
141    #[test]
142    fn write_op_is_arithmetic() {
143        assert!(WriteOp::Increment(FilterValue::Int(1)).is_arithmetic());
144        assert!(WriteOp::Decrement(FilterValue::Int(1)).is_arithmetic());
145        assert!(WriteOp::Multiply(FilterValue::Int(1)).is_arithmetic());
146        assert!(WriteOp::Divide(FilterValue::Int(1)).is_arithmetic());
147        assert!(!WriteOp::Set(FilterValue::Int(1)).is_arithmetic());
148        assert!(!WriteOp::Unset.is_arithmetic());
149    }
150}