Skip to main content

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}