toasty_core/stmt/
projection.rs

1use crate::{
2    schema::{
3        app::{Field, FieldId},
4        db::ColumnId,
5    },
6    stmt::{Expr, Value},
7};
8
9use indexmap::Equivalent;
10use std::{
11    borrow::Borrow,
12    cmp::Ordering,
13    fmt,
14    hash::{Hash, Hasher},
15    ops,
16};
17
18/// A sequence of field indices describing how to navigate into a nested
19/// value.
20///
21/// A `Projection` can be:
22/// - **Identity** (empty) -- refers to the base value itself
23/// - **Single-step** -- indexes one level deep (e.g., field 2 of a record)
24/// - **Multi-step** -- indexes through multiple levels (e.g., field 1, then
25///   field 0 of the nested record)
26///
27/// Dereferences to `[usize]`, so slice operations work directly.
28///
29/// # Examples
30///
31/// ```
32/// use toasty_core::stmt::Projection;
33///
34/// let p = Projection::single(3);
35/// assert_eq!(p.as_slice(), &[3_usize]);
36///
37/// let p = Projection::identity();
38/// assert!(p.is_identity());
39/// assert!(p.as_slice().is_empty());
40/// ```
41#[derive(Clone, PartialEq, Eq)]
42pub struct Projection {
43    steps: Steps,
44}
45
46impl PartialOrd for Projection {
47    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
48        Some(self.cmp(other))
49    }
50}
51
52impl Ord for Projection {
53    fn cmp(&self, other: &Self) -> Ordering {
54        self.as_slice().cmp(other.as_slice())
55    }
56}
57
58/// Trait for types that can be projected through a [`Projection`].
59///
60/// Returns the projected expression, or `None` if the projection is not
61/// applicable.
62pub trait Project {
63    /// Applies the projection and returns the resulting expression.
64    fn project(self, projection: &Projection) -> Option<Expr>;
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68enum Steps {
69    /// References the projection base
70    Identity,
71
72    /// One field step
73    Single([usize; 1]),
74
75    /// Multi field steps
76    Multi(Vec<usize>),
77}
78
79// Custom Hash implementation to ensure compatibility with Equivalent trait:
80// - Single-step projections hash like their contained usize: hash(Projection([1])) == hash(1)
81// - Multi-step projections hash like their slice: hash(Projection([1,2])) == hash([1,2])
82// This allows IndexMap lookups with both usize and [usize] to work correctly.
83impl Hash for Projection {
84    fn hash<H: Hasher>(&self, state: &mut H) {
85        self.steps.hash(state);
86    }
87}
88
89impl Hash for Steps {
90    fn hash<H: Hasher>(&self, state: &mut H) {
91        match self {
92            Steps::Identity => {
93                // Hash a discriminant for identity
94                0u8.hash(state);
95            }
96            Steps::Single([index]) => {
97                // Hash as a single usize (no length prefix, no array wrapping)
98                // This makes hash(Projection([i])) == hash(i)
99                index.hash(state);
100            }
101            Steps::Multi(indices) => {
102                // Hash as a slice: this includes length and elements
103                // This makes hash(Projection([i,j])) == hash([i,j])
104                indices.as_slice().hash(state);
105            }
106        }
107    }
108}
109
110/// An iterator over the steps of a [`Projection`].
111pub struct Iter<'a>(std::slice::Iter<'a, usize>);
112
113impl Projection {
114    /// Creates an identity projection (zero steps, refers to the base itself).
115    pub const fn identity() -> Self {
116        Self {
117            steps: Steps::Identity,
118        }
119    }
120
121    /// Returns `true` if this is an identity projection (no steps).
122    pub const fn is_identity(&self) -> bool {
123        matches!(self.steps, Steps::Identity)
124    }
125
126    /// Creates a single-step projection indexing one field.
127    pub fn single(step: usize) -> Self {
128        Self {
129            steps: Steps::Single([step]),
130        }
131    }
132
133    /// Mostly here for `const`
134    pub const fn from_index(index: usize) -> Self {
135        Self {
136            steps: Steps::Single([index]),
137        }
138    }
139
140    /// Returns the projection steps as a slice.
141    pub fn as_slice(&self) -> &[usize] {
142        self.steps.as_slice()
143    }
144
145    /// Appends a step to this projection.
146    pub fn push(&mut self, step: usize) {
147        match &mut self.steps {
148            Steps::Identity => {
149                self.steps = Steps::Single([step]);
150            }
151            Steps::Single([first]) => {
152                self.steps = Steps::Multi(vec![*first, step]);
153            }
154            Steps::Multi(steps) => {
155                steps.push(step);
156            }
157        }
158    }
159
160    /// Returns `true` if this projection is equal to `other`.
161    pub fn resolves_to(&self, other: impl Into<Self>) -> bool {
162        let other = other.into();
163        *self == other
164    }
165}
166
167impl AsRef<[usize]> for Projection {
168    fn as_ref(&self) -> &[usize] {
169        self.as_slice()
170    }
171}
172
173impl ops::Deref for Projection {
174    type Target = [usize];
175
176    fn deref(&self) -> &Self::Target {
177        self.steps.as_slice()
178    }
179}
180
181impl ops::DerefMut for Projection {
182    fn deref_mut(&mut self) -> &mut Self::Target {
183        match &mut self.steps {
184            Steps::Identity => &mut [],
185            Steps::Single(step) => &mut step[..],
186            Steps::Multi(steps) => &mut steps[..],
187        }
188    }
189}
190
191impl<'a> IntoIterator for &'a Projection {
192    type Item = usize;
193    type IntoIter = Iter<'a>;
194
195    fn into_iter(self) -> Self::IntoIter {
196        Iter(self[..].iter())
197    }
198}
199
200impl From<&Field> for Projection {
201    fn from(value: &Field) -> Self {
202        Self::single(value.id.index)
203    }
204}
205
206impl From<FieldId> for Projection {
207    fn from(value: FieldId) -> Self {
208        Self::single(value.index)
209    }
210}
211
212impl From<ColumnId> for Projection {
213    fn from(value: ColumnId) -> Self {
214        Self::single(value.index)
215    }
216}
217
218impl From<usize> for Projection {
219    fn from(value: usize) -> Self {
220        Self::single(value)
221    }
222}
223
224impl From<&[usize]> for Projection {
225    fn from(value: &[usize]) -> Self {
226        match value {
227            [] => Self::identity(),
228            [value] => Self::single(*value),
229            value => Self {
230                steps: Steps::Multi(value.into()),
231            },
232        }
233    }
234}
235
236impl<const N: usize> From<[usize; N]> for Projection {
237    fn from(value: [usize; N]) -> Self {
238        Self::from(&value[..])
239    }
240}
241
242impl fmt::Debug for Projection {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        let mut f = f.debug_tuple("Projection");
245
246        if self.is_identity() {
247            f.field(&"identity");
248        } else {
249            for field in &self[..] {
250                f.field(&field);
251            }
252        }
253
254        f.finish()
255    }
256}
257
258impl Steps {
259    fn as_slice(&self) -> &[usize] {
260        match self {
261            Self::Identity => &[],
262            Self::Single(step) => &step[..],
263            Self::Multi(steps) => &steps[..],
264        }
265    }
266}
267
268impl Iterator for Iter<'_> {
269    type Item = usize;
270
271    fn next(&mut self) -> Option<usize> {
272        self.0.next().copied()
273    }
274}
275
276impl Project for Expr {
277    fn project(self, projection: &Projection) -> Option<Expr> {
278        Some(self.entry(projection)?.to_expr())
279    }
280}
281
282impl Project for &Expr {
283    fn project(self, projection: &Projection) -> Option<Expr> {
284        Some(self.entry(projection)?.to_expr())
285    }
286}
287
288impl Project for &&Expr {
289    fn project(self, projection: &Projection) -> Option<Expr> {
290        Some(self.entry(projection)?.to_expr())
291    }
292}
293
294impl Project for Value {
295    fn project(self, projection: &Projection) -> Option<Expr> {
296        Some(self.entry(projection).to_expr())
297    }
298}
299
300impl Project for &Value {
301    fn project(self, projection: &Projection) -> Option<Expr> {
302        Some(self.entry(projection).to_expr())
303    }
304}
305
306impl Project for &&Value {
307    fn project(self, projection: &Projection) -> Option<Expr> {
308        Some(self.entry(projection).to_expr())
309    }
310}
311
312// Borrow implementations for BTreeMap lookups
313impl Borrow<[usize]> for Projection {
314    fn borrow(&self) -> &[usize] {
315        self.as_slice()
316    }
317}
318
319// Allow using usize directly where Projection is expected (for single-step projections)
320impl Equivalent<Projection> for usize {
321    fn equivalent(&self, key: &Projection) -> bool {
322        matches!(key.as_slice(), [index] if *index == *self)
323    }
324}
325
326// Allow using &Projection where Projection is expected
327impl Equivalent<Projection> for &Projection {
328    fn equivalent(&self, key: &Projection) -> bool {
329        *self == key
330    }
331}
332
333// Note: `Equivalent<Projection> for [usize]` is provided automatically by
334// the blanket impl in `equivalent` crate because `Projection: Borrow<[usize]>`.
335
336// PartialEq implementations for ergonomic comparisons
337impl PartialEq<usize> for Projection {
338    fn eq(&self, other: &usize) -> bool {
339        matches!(self.as_slice(), [index] if *index == *other)
340    }
341}
342
343impl PartialEq<Projection> for usize {
344    fn eq(&self, other: &Projection) -> bool {
345        other == self
346    }
347}
348
349impl PartialEq<[usize]> for Projection {
350    fn eq(&self, other: &[usize]) -> bool {
351        self.as_slice() == other
352    }
353}
354
355impl PartialEq<Projection> for [usize] {
356    fn eq(&self, other: &Projection) -> bool {
357        other == self
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn test_projection_eq_usize() {
367        let proj_single = Projection::from(5);
368        let proj_multi = Projection::from([1, 2]);
369
370        // Single-step projection == usize (both directions)
371        assert_eq!(proj_single, 5);
372        assert_eq!(5, proj_single);
373        assert_ne!(proj_single, 3);
374        assert_ne!(3, proj_single);
375
376        // Multi-step projection != usize
377        assert_ne!(proj_multi, 1);
378        assert_ne!(1, proj_multi);
379    }
380
381    #[test]
382    fn test_projection_eq_slice() {
383        let proj_single = Projection::from(5);
384        let proj_multi = Projection::from([1, 2, 3]);
385
386        // Projection == [usize] slice (both directions)
387        assert_eq!(proj_single, [5][..]);
388        assert_eq!([5][..], proj_single);
389        assert_eq!(proj_multi, [1, 2, 3][..]);
390        assert_eq!([1, 2, 3][..], proj_multi);
391
392        // Mismatches
393        assert_ne!(proj_single, [1, 2][..]);
394        assert_ne!([1, 2][..], proj_single);
395    }
396
397    #[test]
398    fn test_projection_hash_compatibility() {
399        use std::collections::hash_map::DefaultHasher;
400        use std::hash::{Hash, Hasher};
401
402        fn hash<T: Hash + ?Sized>(value: &T) -> u64 {
403            let mut hasher = DefaultHasher::new();
404            value.hash(&mut hasher);
405            hasher.finish()
406        }
407
408        // Single-step projection should hash like the contained usize
409        let proj_single = Projection::from(42);
410        assert_eq!(hash(&proj_single), hash(&42_usize));
411
412        // Multi-step projection should hash like the slice
413        let proj_multi = Projection::from([1, 2, 3]);
414        let slice: &[usize] = &[1, 2, 3];
415        assert_eq!(hash(&proj_multi), hash(slice));
416
417        // Verify this works with IndexMap
418        use indexmap::IndexMap;
419        let mut map = IndexMap::new();
420        map.insert(Projection::from(5), "value");
421
422        // Can look up with usize
423        assert_eq!(map.get(&5_usize), Some(&"value"));
424
425        // Can look up with single-element slice
426        let slice_single: &[usize] = &[5];
427        assert_eq!(map.get(slice_single), Some(&"value"));
428
429        // Multi-step example
430        map.insert(Projection::from([1, 2]), "multi");
431
432        // Can look up with multi-element slice
433        let slice_multi: &[usize] = &[1, 2];
434        assert_eq!(map.get(slice_multi), Some(&"multi"));
435    }
436}