toasty_core/stmt/assignments.rs
1use crate::stmt::{Node, Statement, Visit, VisitMut};
2
3use super::{Expr, Projection};
4
5use std::{collections::BTreeMap, ops};
6
7/// An ordered map of field assignments for an [`Update`](super::Update) statement.
8///
9/// Each entry maps a field projection (identifying which field to change) to an
10/// [`Assignment`] (how to change it). The entries are ordered by projection,
11/// allowing range queries over prefixes.
12///
13/// # Examples
14///
15/// ```ignore
16/// use toasty_core::stmt::{Assignments, Expr, Projection};
17///
18/// let mut assignments = Assignments::default();
19/// assert!(assignments.is_empty());
20///
21/// assignments.set(Projection::single(0), Expr::null());
22/// assert_eq!(assignments.len(), 1);
23/// ```
24#[derive(Clone, Debug, Default, PartialEq)]
25pub struct Assignments {
26 /// Map from field projection to assignment. The projection may reference an
27 /// application-level model field or a lowered table column. Supports both
28 /// single-step (e.g., `[0]`) and multi-step projections (e.g., `[0, 1]`
29 /// for nested fields).
30 assignments: BTreeMap<Projection, Assignment>,
31
32 /// Reasons the builder API recorded for assignments Toasty does not
33 /// support. Builder combinators such as `stmt::patch` are infallible, so
34 /// one that cannot express its operation records a reason here instead of
35 /// an entry. Verification reports the first reason as an
36 /// `unsupported_feature` error before the statement is planned, rejecting
37 /// the statement as a whole.
38 unsupported: Vec<String>,
39}
40
41/// A field assignment within an [`Update`](super::Update) statement.
42///
43/// Each variant carries the expression providing the value for the operation.
44/// Multiple operations on the same field are represented via [`Batch`](Assignment::Batch).
45///
46/// # Examples
47///
48/// ```ignore
49/// use toasty_core::stmt::{Assignment, Expr};
50///
51/// let assignment = Assignment::Set(Expr::null());
52/// assert!(assignment.is_set());
53/// ```
54#[derive(Debug, Clone, PartialEq)]
55pub enum Assignment {
56 /// Set a field, replacing the current value.
57 Set(Expr),
58
59 /// Insert one or more values into a set field.
60 Insert(Expr),
61
62 /// Remove one or more values from a set field.
63 Remove(Expr),
64
65 /// Append every element of a list expression to the end of an ordered
66 /// collection field (e.g. `Vec<scalar>`). The expression must evaluate
67 /// to a list whose element type matches the target column.
68 Append(Expr),
69
70 /// Drop the last element of an ordered collection field. Drives
71 /// [`stmt::pop`](crate::stmt::Assignment::Pop) on `Vec<scalar>` fields.
72 /// No-op on an empty collection.
73 Pop,
74
75 /// Drop the element at the given index from an ordered collection field.
76 /// The expression must evaluate to a `usize`-shaped value. Out-of-bounds
77 /// indices are a no-op rather than an error — per-row failure semantics
78 /// on a bulk update are rarely useful.
79 RemoveAt(Expr),
80
81 /// Add the expression to the current numeric column value (`col = col + expr`).
82 /// Atomic against the existing column value on every backend.
83 Add(Expr),
84
85 /// Subtract the expression from the current numeric column value
86 /// (`col = col - expr`). Atomic against the existing column value on
87 /// every backend.
88 Subtract(Expr),
89
90 /// Multiple assignments on the same field.
91 Batch(Vec<Assignment>),
92}
93
94impl Statement {
95 /// Returns this statement's assignments if it is an `Update`.
96 pub fn assignments(&self) -> Option<&Assignments> {
97 match self {
98 Statement::Update(update) => Some(&update.assignments),
99 _ => None,
100 }
101 }
102}
103
104impl Assignments {
105 /// Creates an empty `Assignments`.
106 pub fn new() -> Self {
107 Self {
108 assignments: BTreeMap::new(),
109 unsupported: Vec::new(),
110 }
111 }
112
113 /// Returns `true` if there are no assignments.
114 pub fn is_empty(&self) -> bool {
115 self.assignments.is_empty()
116 }
117
118 /// Returns the number of assigned projections (keys).
119 pub fn len(&self) -> usize {
120 self.assignments.len()
121 }
122
123 /// Returns `true` if an assignment exists for the given projection.
124 ///
125 /// The `key` accepts any type that implements `AsRef<[usize]>`:
126 /// - [`Projection`] — look up by projection directly
127 /// - `&[usize]` — a slice of field indices (e.g., `&[1, 2]`)
128 /// - `[usize; N]` — a fixed-size array (e.g., `[1]`, `[1, 2]`).
129 /// Integer literals infer as `usize` from the `AsRef<[usize]>` bound,
130 /// so `&[1]` works without a suffix.
131 pub fn contains<Q>(&self, key: &Q) -> bool
132 where
133 Q: ?Sized + AsRef<[usize]>,
134 {
135 self.assignments.contains_key(key.as_ref())
136 }
137
138 /// Returns a reference to the assignment for the given projection, if any.
139 ///
140 /// The `key` accepts any type that implements `AsRef<[usize]>`:
141 /// - [`Projection`] — look up by projection directly
142 /// - `&[usize]` — a slice of field indices (e.g., `&[1, 2]`)
143 /// - `[usize; N]` — a fixed-size array (e.g., `[1]`, `[1, 2]`).
144 /// Integer literals infer as `usize` from the `AsRef<[usize]>` bound,
145 /// so `&[1]` works without a suffix.
146 pub fn get<Q>(&self, key: &Q) -> Option<&Assignment>
147 where
148 Q: ?Sized + AsRef<[usize]>,
149 {
150 self.assignments.get(key.as_ref())
151 }
152
153 /// Returns a mutable reference to the assignment for the given projection.
154 ///
155 /// The `key` accepts any type that implements `AsRef<[usize]>`:
156 /// - [`Projection`] — look up by projection directly
157 /// - `&[usize]` — a slice of field indices (e.g., `&[1, 2]`)
158 /// - `[usize; N]` — a fixed-size array (e.g., `[1]`, `[1, 2]`).
159 /// Integer literals infer as `usize` from the `AsRef<[usize]>` bound,
160 /// so `&[1]` works without a suffix.
161 pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut Assignment>
162 where
163 Q: ?Sized + AsRef<[usize]>,
164 {
165 self.assignments.get_mut(key.as_ref())
166 }
167
168 /// Sets a field to the given expression value, replacing any existing
169 /// assignment for that projection.
170 pub fn set<Q>(&mut self, key: Q, expr: impl Into<Expr>)
171 where
172 Q: Into<Projection>,
173 {
174 let key = key.into();
175 self.assignments.insert(key, Assignment::Set(expr.into()));
176 }
177
178 /// Removes the assignment for the given projection, if any.
179 ///
180 /// The `key` accepts any type that implements `AsRef<[usize]>`:
181 /// - [`Projection`] — look up by projection directly
182 /// - `&[usize]` — a slice of field indices (e.g., `&[1, 2]`)
183 /// - `[usize; N]` — a fixed-size array (e.g., `[1]`, `[1, 2]`).
184 /// Integer literals infer as `usize` from the `AsRef<[usize]>` bound,
185 /// so `&[1]` works without a suffix.
186 pub fn unset<Q>(&mut self, key: &Q)
187 where
188 Q: ?Sized + AsRef<[usize]>,
189 {
190 self.assignments.remove(key.as_ref());
191 }
192
193 /// Insert a value into a set. The expression should evaluate to a single
194 /// value that is inserted into the set.
195 pub fn insert<Q>(&mut self, key: Q, expr: impl Into<Expr>)
196 where
197 Q: Into<Projection>,
198 {
199 let key = key.into();
200 let new = Assignment::Insert(expr.into());
201 self.assignments
202 .entry(key)
203 .and_modify(|existing| existing.push(new.clone()))
204 .or_insert(new);
205 }
206
207 /// Adds a `Remove` assignment for the given projection.
208 pub fn remove<Q>(&mut self, key: Q, expr: impl Into<Expr>)
209 where
210 Q: Into<Projection>,
211 {
212 let key = key.into();
213 let new = Assignment::Remove(expr.into());
214 self.assignments
215 .entry(key)
216 .and_modify(|existing| existing.push(new.clone()))
217 .or_insert(new);
218 }
219
220 /// Adds an `Append` assignment for the given projection. The expression
221 /// should evaluate to a list of elements to append to an ordered
222 /// collection field; multiple appends on the same projection batch.
223 pub fn append<Q>(&mut self, key: Q, expr: impl Into<Expr>)
224 where
225 Q: Into<Projection>,
226 {
227 let key = key.into();
228 let new = Assignment::Append(expr.into());
229 self.assignments
230 .entry(key)
231 .and_modify(|existing| existing.push(new.clone()))
232 .or_insert(new);
233 }
234
235 /// Adds a `Pop` assignment for the given projection. Multiple pops on the
236 /// same projection batch.
237 pub fn pop<Q>(&mut self, key: Q)
238 where
239 Q: Into<Projection>,
240 {
241 let key = key.into();
242 let new = Assignment::Pop;
243 self.assignments
244 .entry(key)
245 .and_modify(|existing| existing.push(new.clone()))
246 .or_insert(new);
247 }
248
249 /// Adds a `RemoveAt` assignment for the given projection. The expression
250 /// should evaluate to the element index to drop. Multiple removals on the
251 /// same projection batch.
252 pub fn remove_at<Q>(&mut self, key: Q, expr: impl Into<Expr>)
253 where
254 Q: Into<Projection>,
255 {
256 let key = key.into();
257 let new = Assignment::RemoveAt(expr.into());
258 self.assignments
259 .entry(key)
260 .and_modify(|existing| existing.push(new.clone()))
261 .or_insert(new);
262 }
263
264 /// Adds an `Add` assignment for the given projection. The expression
265 /// should evaluate to a numeric value to add to the current column value.
266 /// Multiple ops on the same projection batch.
267 pub fn add<Q>(&mut self, key: Q, expr: impl Into<Expr>)
268 where
269 Q: Into<Projection>,
270 {
271 let key = key.into();
272 let new = Assignment::Add(expr.into());
273 self.assignments
274 .entry(key)
275 .and_modify(|existing| existing.push(new.clone()))
276 .or_insert(new);
277 }
278
279 /// Adds a `Subtract` assignment for the given projection. The expression
280 /// should evaluate to a numeric value to subtract from the current column
281 /// value. Multiple ops on the same projection batch.
282 pub fn subtract<Q>(&mut self, key: Q, expr: impl Into<Expr>)
283 where
284 Q: Into<Projection>,
285 {
286 let key = key.into();
287 let new = Assignment::Subtract(expr.into());
288 self.assignments
289 .entry(key)
290 .and_modify(|existing| existing.push(new.clone()))
291 .or_insert(new);
292 }
293
294 /// Removes and returns the assignment for the given projection.
295 ///
296 /// The `key` accepts any type that implements `AsRef<[usize]>`:
297 /// - [`Projection`] — look up by projection directly
298 /// - `&[usize]` — a slice of field indices (e.g., `&[1, 2]`)
299 /// - `[usize; N]` — a fixed-size array (e.g., `[1]`, `[1, 2]`).
300 /// Integer literals infer as `usize` from the `AsRef<[usize]>` bound,
301 /// so `&[1]` works without a suffix.
302 pub fn take<Q>(&mut self, key: &Q) -> Option<Assignment>
303 where
304 Q: ?Sized + AsRef<[usize]>,
305 {
306 self.assignments.remove(key.as_ref())
307 }
308
309 /// Records that a requested assignment is unsupported, without adding an
310 /// entry. The statement carrying these assignments fails verification
311 /// with an `unsupported_feature` error built from `reason`.
312 pub fn reject_unsupported(&mut self, reason: impl Into<String>) {
313 self.unsupported.push(reason.into());
314 }
315
316 /// Returns the reasons recorded by [`reject_unsupported`], in the order
317 /// they were recorded. Empty when every requested assignment is
318 /// supported.
319 ///
320 /// [`reject_unsupported`]: Assignments::reject_unsupported
321 pub fn unsupported(&self) -> &[String] {
322 &self.unsupported
323 }
324
325 /// Replaces assignments with entries from `other`, preserving entries
326 /// whose projections do not appear in `other`. Unsupported-assignment
327 /// reasons from both sides are kept.
328 pub fn overlay(&mut self, other: Self) {
329 self.assignments.extend(other.assignments);
330 self.unsupported.extend(other.unsupported);
331 }
332
333 /// Returns an iterator over the assignment projections (keys).
334 pub fn keys(&self) -> impl Iterator<Item = &Projection> + '_ {
335 self.assignments.keys()
336 }
337
338 /// Returns an iterator over `(projection, assignment)` pairs.
339 pub fn iter(&self) -> impl Iterator<Item = (&Projection, &Assignment)> + '_ {
340 self.assignments.iter()
341 }
342
343 /// Returns a mutable iterator over `(projection, assignment)` pairs.
344 pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Projection, &mut Assignment)> + '_ {
345 self.assignments.iter_mut()
346 }
347}
348
349impl IntoIterator for Assignments {
350 type Item = (Projection, Assignment);
351 type IntoIter = std::collections::btree_map::IntoIter<Projection, Assignment>;
352
353 fn into_iter(self) -> Self::IntoIter {
354 self.assignments.into_iter()
355 }
356}
357
358impl<'a> IntoIterator for &'a Assignments {
359 type Item = (&'a Projection, &'a Assignment);
360 type IntoIter = std::collections::btree_map::Iter<'a, Projection, Assignment>;
361
362 fn into_iter(self) -> Self::IntoIter {
363 self.assignments.iter()
364 }
365}
366
367impl<'a> IntoIterator for &'a mut Assignments {
368 type Item = (&'a Projection, &'a mut Assignment);
369 type IntoIter = std::collections::btree_map::IterMut<'a, Projection, Assignment>;
370
371 fn into_iter(self) -> Self::IntoIter {
372 self.assignments.iter_mut()
373 }
374}
375
376/// Indexes into the assignments by projection. Panics if no assignment exists
377/// for the given key.
378///
379/// The index accepts any type that implements `AsRef<[usize]>`:
380/// [`Projection`], `&[usize]`, or `[usize; N]` arrays.
381impl<Q> ops::Index<Q> for Assignments
382where
383 Q: AsRef<[usize]>,
384{
385 type Output = Assignment;
386
387 fn index(&self, index: Q) -> &Self::Output {
388 match self.assignments.get(index.as_ref()) {
389 Some(ret) => ret,
390 None => panic!("no assignment for projection"),
391 }
392 }
393}
394
395/// Mutably indexes into the assignments by projection. Panics if no assignment
396/// exists for the given key.
397///
398/// The index accepts any type that implements `AsRef<[usize]>`:
399/// [`Projection`], `&[usize]`, or `[usize; N]` arrays.
400impl<Q> ops::IndexMut<Q> for Assignments
401where
402 Q: AsRef<[usize]>,
403{
404 fn index_mut(&mut self, index: Q) -> &mut Self::Output {
405 match self.assignments.get_mut(index.as_ref()) {
406 Some(ret) => ret,
407 None => panic!("no assignment for projection"),
408 }
409 }
410}
411
412impl Assignment {
413 /// Returns `true` if this is the `Set` variant.
414 pub fn is_set(&self) -> bool {
415 matches!(self, Self::Set(_))
416 }
417
418 /// Returns `true` if this is the `Remove` variant.
419 pub fn is_remove(&self) -> bool {
420 matches!(self, Self::Remove(_))
421 }
422
423 /// Appends another assignment, converting to `Batch` if needed.
424 pub fn push(&mut self, other: Assignment) {
425 match self {
426 Self::Batch(entries) => entries.push(other),
427 _ => {
428 let prev = std::mem::replace(self, Assignment::Batch(Vec::new()));
429 if let Assignment::Batch(entries) = self {
430 entries.push(prev);
431 entries.push(other);
432 }
433 }
434 }
435 }
436}
437
438impl Node for Assignment {
439 fn visit<V: Visit>(&self, mut visit: V) {
440 visit.visit_assignment(self);
441 }
442
443 fn visit_mut<V: VisitMut>(&mut self, mut visit: V) {
444 visit.visit_assignment_mut(self);
445 }
446}