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}