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}