Skip to main content

uqa_sql/ast/
assignment_target.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Ordered mutation targets shared by parsed and executable expressions.
8
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11/// Target syntax is separate from the value expression. Every bound belongs to the original input row, even when repeated targets compose writes to one column.
12#[derive(Debug, Clone, PartialEq)]
13pub struct AssignmentTarget<E = super::Expr> {
14    pub column: String,
15    pub indirection: Vec<AssignmentStep<E>>,
16}
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub enum AssignmentStep<E> {
20    Field(String),
21    Index(Box<E>),
22    Slice {
23        lower: Option<Box<E>>,
24        upper: Option<Box<E>>,
25    },
26}
27
28impl<E> From<String> for AssignmentTarget<E> {
29    fn from(column: String) -> Self {
30        Self {
31            column,
32            indirection: Vec::new(),
33        }
34    }
35}
36
37impl<E> From<&str> for AssignmentTarget<E> {
38    fn from(column: &str) -> Self {
39        column.to_owned().into()
40    }
41}
42
43impl<E> AssignmentTarget<E> {
44    pub fn is_whole_column(&self) -> bool {
45        self.indirection.is_empty()
46    }
47
48    pub fn expressions(&self) -> impl Iterator<Item = &E> {
49        self.indirection.iter().flat_map(|step| {
50            match step {
51                AssignmentStep::Field(_) => [None, None],
52                AssignmentStep::Index(index) => [Some(index.as_ref()), None],
53                AssignmentStep::Slice { lower, upper } => [lower.as_deref(), upper.as_deref()],
54            }
55            .into_iter()
56            .flatten()
57        })
58    }
59
60    pub fn expressions_mut(&mut self) -> impl Iterator<Item = &mut E> {
61        self.indirection.iter_mut().flat_map(|step| {
62            match step {
63                AssignmentStep::Field(_) => [None, None],
64                AssignmentStep::Index(index) => [Some(index.as_mut()), None],
65                AssignmentStep::Slice { lower, upper } => {
66                    [lower.as_deref_mut(), upper.as_deref_mut()]
67                }
68            }
69            .into_iter()
70            .flatten()
71        })
72    }
73
74    pub fn map<T>(self, mut map: impl FnMut(E) -> T) -> AssignmentTarget<T> {
75        AssignmentTarget {
76            column: self.column,
77            indirection: self
78                .indirection
79                .into_iter()
80                .map(|step| match step {
81                    AssignmentStep::Field(field) => AssignmentStep::Field(field),
82                    AssignmentStep::Index(index) => AssignmentStep::Index(Box::new(map(*index))),
83                    AssignmentStep::Slice { lower, upper } => AssignmentStep::Slice {
84                        lower: lower.map(|value| Box::new(map(*value))),
85                        upper: upper.map(|value| Box::new(map(*value))),
86                    },
87                })
88                .collect(),
89        }
90    }
91}
92
93impl<E: Serialize> Serialize for AssignmentTarget<E> {
94    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
95        #[derive(Serialize)]
96        #[serde(untagged)]
97        enum Target<'a, E> {
98            Column(&'a str),
99            Partial {
100                column: &'a str,
101                indirection: &'a [AssignmentStep<E>],
102            },
103        }
104        if self.is_whole_column() {
105            Target::<E>::Column(&self.column).serialize(serializer)
106        } else {
107            Target::Partial {
108                column: &self.column,
109                indirection: &self.indirection,
110            }
111            .serialize(serializer)
112        }
113    }
114}
115
116impl<'de, E: Deserialize<'de>> Deserialize<'de> for AssignmentTarget<E> {
117    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
118        #[derive(Deserialize)]
119        #[serde(untagged)]
120        enum Target<E> {
121            Column(String),
122            Partial {
123                column: String,
124                indirection: Vec<AssignmentStep<E>>,
125            },
126        }
127        Ok(match Target::deserialize(deserializer)? {
128            Target::Column(column) => column.into(),
129            Target::Partial {
130                column,
131                indirection,
132            } => Self {
133                column,
134                indirection,
135            },
136        })
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn ordinary_target_encoding_preserves_predecessor_definitions() {
146        let target: AssignmentTarget<i32> = serde_json::from_str("\"value\"").unwrap();
147        assert!(target.is_whole_column());
148        assert_eq!(target.column, "value");
149        assert_eq!(serde_json::to_string(&target).unwrap(), "\"value\"");
150    }
151
152    #[test]
153    fn partial_targets_keep_bound_omission_order_and_expression_rewrites() {
154        let mut target = AssignmentTarget {
155            column: "value".into(),
156            indirection: vec![
157                AssignmentStep::Index(Box::new(1)),
158                AssignmentStep::Field("items".into()),
159                AssignmentStep::Slice {
160                    lower: None,
161                    upper: Some(Box::new(2)),
162                },
163                AssignmentStep::Slice {
164                    lower: Some(Box::new(3)),
165                    upper: None,
166                },
167            ],
168        };
169        for value in target.expressions_mut() {
170            *value += 10;
171        }
172        assert_eq!(
173            target.expressions().copied().collect::<Vec<_>>(),
174            [11, 12, 13]
175        );
176        let rendered = serde_json::to_string(&target).unwrap();
177        assert_eq!(
178            serde_json::from_str::<AssignmentTarget<i32>>(&rendered).unwrap(),
179            target
180        );
181        let lowered = target.map(|value| value.to_string());
182        assert_eq!(
183            lowered
184                .expressions()
185                .map(String::as_str)
186                .collect::<Vec<_>>(),
187            ["11", "12", "13"]
188        );
189        assert!(matches!(
190            lowered.indirection[2],
191            AssignmentStep::Slice { lower: None, .. }
192        ));
193        assert!(matches!(
194            lowered.indirection[3],
195            AssignmentStep::Slice { upper: None, .. }
196        ));
197    }
198}