Skip to main content

razeri_plugin_contract/
flatten.rs

1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5//! **One row holding an array becomes one row per element** — the vocabulary
6//! an engine answers a fan-out in.
7//!
8//! Razeri composes the load: it knows the relation being read, the alias that
9//! relation was given, the array column and the element fields the statement
10//! needs. What it cannot know is how this engine turns an array into rows,
11//! because no two families spell it the same way — a lateral `unnest`, a JSON
12//! table function, an array join, a lateral view. So Razeri asks, once, and
13//! the engine answers with the whole shape: the `FROM` item that produces the
14//! rows, the expression that reads an element's position, and one expression
15//! per field.
16//!
17//! ## Why the three parts are one answer
18//!
19//! They cannot be asked for separately. At least one engine needs two table
20//! functions applied in series — the first produces the elements, the second
21//! types their fields — so its field expressions name an alias its own `FROM`
22//! item introduced. An interface that asked three questions would have no
23//! place to carry that dependency, and the caller would be composing a
24//! statement out of parts that do not know about each other.
25//!
26//! ## The ordinal counts from one, everywhere
27//!
28//! Engines disagree about where an array starts: about half hand back a
29//! position counting from one and about half from zero, and at least one hands
30//! it back as text. The element's key is derived from its position, and the
31//! key is hashed into the surrogate key Razeri promises is the same on every
32//! engine — so an element that is position 1 in one warehouse and position 0
33//! in another would carry two different keys for one row. Normalizing in the
34//! answer is what keeps that promise true, and it costs the engine that counts
35//! from zero one addition.
36
37use crate::data_type::DataType;
38
39/// What Razeri is asking to have flattened.
40///
41/// Every name here is spelled the way it already appears in the statement
42/// being composed — qualified where the statement qualifies it, quoted where
43/// the statement quotes it. An implementation interpolates them as they are
44/// rather than re-spelling them, because the answer has to name the same
45/// relation the rest of the statement names.
46#[derive(Debug, Clone)]
47#[non_exhaustive]
48pub struct FlattenRequest<'a> {
49    /// The relation being read, qualified, as the statement's `FROM` spells it.
50    pub source_relation: &'a str,
51    /// The alias the recompute gave that relation.
52    pub source_alias: &'a str,
53    /// The array column, qualified by the source alias.
54    pub array_column: &'a str,
55    /// What the column was declared to hold. An array of records and an array
56    /// of scalars are both flattened; on engines that fold either to a
57    /// document type, this is how an implementation tells a native container
58    /// from a document holding a list.
59    pub array_type: &'a DataType,
60    /// The alias to give one element.
61    pub element_alias: &'a str,
62    /// The alias to give the element's position.
63    pub ordinal_alias: &'a str,
64    /// The element fields the statement reads, in declared order. Empty for an
65    /// array of scalars, where the element is itself the value.
66    pub fields: &'a [FlattenField<'a>],
67}
68
69impl<'a> FlattenRequest<'a> {
70    /// Build a request. Every part is required, because every engine that can
71    /// answer at all needs all of them.
72    #[must_use]
73    pub fn new(
74        source_relation: &'a str,
75        source_alias: &'a str,
76        array_column: &'a str,
77        array_type: &'a DataType,
78        element_alias: &'a str,
79        ordinal_alias: &'a str,
80        fields: &'a [FlattenField<'a>],
81    ) -> Self {
82        Self {
83            source_relation,
84            source_alias,
85            array_column,
86            array_type,
87            element_alias,
88            ordinal_alias,
89            fields,
90        }
91    }
92
93    /// **What one element holds**, where the column was declared as an array.
94    ///
95    /// An engine whose elements are untyped reads them through this: an array
96    /// of plain values is answered with one expression, and the expression has
97    /// to say what kind of value it is producing. `None` is a column that was
98    /// not declared as an array at all, which razeri refuses before it gets
99    /// here.
100    #[must_use]
101    pub fn element_type(&self) -> Option<&'a DataType> {
102        match self.array_type {
103            DataType::Array(array) => Some(array.element_type.as_ref()),
104            _ => None,
105        }
106    }
107
108    /// **True when the elements are records with named fields.**
109    ///
110    /// An array of scalars asks for no fields, because the element is the
111    /// value: the answer's `fields` is then empty and the element expression
112    /// reads the whole of it.
113    #[must_use]
114    pub fn elements_have_fields(&self) -> bool {
115        !self.fields.is_empty()
116    }
117}
118
119/// One field of an element, by the name the declared type gives it.
120#[derive(Debug, Clone)]
121#[non_exhaustive]
122pub struct FlattenField<'a> {
123    /// The field's name inside the element, unquoted, exactly as the declared
124    /// type spells it. An implementation quotes it the way its engine wants,
125    /// which for most is a JSON path rather than an identifier.
126    pub name: &'a str,
127    /// The type the target column declares for it, which is the type the read
128    /// is cast to on engines whose elements are untyped.
129    pub data_type: &'a DataType,
130}
131
132impl<'a> FlattenField<'a> {
133    /// Name one field of an element and the type it is read as.
134    #[must_use]
135    pub fn new(name: &'a str, data_type: &'a DataType) -> Self {
136        Self { name, data_type }
137    }
138}
139
140/// How this engine spells the fan-out.
141#[derive(Debug, Clone, PartialEq, Eq)]
142#[non_exhaustive]
143pub struct Flatten {
144    /// The `FROM` item producing one row per element, written to follow the
145    /// source relation and its alias. An engine that needs two table functions
146    /// applied in series returns both here, as one piece of text.
147    ///
148    /// Razeri writes `FROM <source_relation> AS <source_alias>` and then this,
149    /// so an implementation begins with whatever its engine puts between the
150    /// two — a comma for a lateral join in the `FROM` list, nothing at all for
151    /// an engine whose fan-out is a clause of its own.
152    pub from_item: String,
153    /// Reads the element's position. **One for the first element**, whatever
154    /// the engine counts from.
155    pub ordinal: String,
156    /// One expression per requested field, in the order asked. An array of
157    /// scalars is answered with exactly one expression, reading the element
158    /// whole.
159    pub fields: Vec<String>,
160}
161
162impl Flatten {
163    /// State a fan-out: the `FROM` item, the one-based ordinal, and one
164    /// expression per field asked for.
165    #[must_use]
166    pub fn new(
167        from_item: impl Into<String>,
168        ordinal: impl Into<String>,
169        fields: Vec<String>,
170    ) -> Self {
171        Self { from_item: from_item.into(), ordinal: ordinal.into(), fields }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::{Flatten, FlattenField, FlattenRequest};
178    use crate::agnostic::AnsiDialect;
179    use crate::data_type::DataType;
180    use crate::renderer::DmlRenderer;
181
182    /// The ANSI renderer stands in for "an engine that has answered nothing":
183    /// every method of the DML trait carries a default, so an empty
184    /// implementation is a whole dialect's DML, and the flatten it states is
185    /// the one a plugin author gets for free.
186    impl DmlRenderer for AnsiDialect {}
187
188    /// A dialect that says nothing says `None`, and Razeri has a refusal to
189    /// write rather than a statement to guess at.
190    #[test]
191    fn an_engine_that_states_nothing_answers_none() {
192        let array = DataType::json();
193        let request = FlattenRequest::new(
194            "\"raw\".\"orders\"",
195            "\"src\"",
196            "\"src\".\"items\"",
197            &array,
198            "\"elem\"",
199            "\"_ordinal\"",
200            &[],
201        );
202        assert!(AnsiDialect.render_flatten(&request).is_none());
203    }
204
205    /// An array of scalars asks for no fields: the element is the value, and
206    /// the answer carries exactly one expression for it.
207    #[test]
208    fn an_array_of_scalars_asks_for_no_fields() {
209        let array = DataType::json();
210        let request = FlattenRequest::new(
211            "\"raw\".\"pings\"",
212            "\"src\"",
213            "\"src\".\"branches\"",
214            &array,
215            "\"elem\"",
216            "\"_ordinal\"",
217            &[],
218        );
219        assert!(!request.elements_have_fields());
220        assert!(request.element_type().is_none(), "a document column declares no element type");
221
222        let answered = Flatten::new(", unnest(\"src\".\"branches\")", "1", vec!["\"elem\"".into()]);
223        assert_eq!(answered.fields.len(), 1);
224    }
225
226    /// An array of records asks for one expression per field, in the order the
227    /// table declared them.
228    #[test]
229    fn an_array_of_records_asks_for_one_expression_per_field() {
230        let product = DataType::varchar(10);
231        let quantity = DataType::int();
232        let fields =
233            [FlattenField::new("product_id", &product), FlattenField::new("quantity", &quantity)];
234        let array = DataType::json();
235        let request = FlattenRequest::new(
236            "\"raw\".\"orders\"",
237            "\"src\"",
238            "\"src\".\"items\"",
239            &array,
240            "\"elem\"",
241            "\"_ordinal\"",
242            &fields,
243        );
244        assert!(request.elements_have_fields());
245        assert_eq!(
246            request.fields.iter().map(|field| field.name).collect::<Vec<_>>(),
247            ["product_id", "quantity"]
248        );
249    }
250}