1use crate::FaucetError;
16use crate::stage::TransformStage;
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19use serde_json::{Map, Value};
20use std::sync::Arc;
21
22pub const DEFAULT_MAX_PRODUCT: usize = 10_000;
24
25fn default_true() -> bool {
26 true
27}
28fn default_max_product() -> usize {
29 DEFAULT_MAX_PRODUCT
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
34#[serde(rename_all = "snake_case")]
35pub enum OnEmpty {
36 #[default]
39 Skip,
40 OneRow,
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
47#[serde(deny_unknown_fields)]
48pub struct CrossJoinSpec {
49 pub arrays: Vec<String>,
52 #[serde(default)]
55 pub prefix: bool,
56 #[serde(default = "default_true")]
59 pub keep_parent: bool,
60 #[serde(default)]
62 pub on_empty: OnEmpty,
63 #[serde(default = "default_true")]
66 pub drop_arrays: bool,
67 #[serde(default = "default_max_product")]
70 pub max_product: usize,
71}
72
73impl CrossJoinSpec {
74 pub fn compile(&self) -> Result<CompiledCrossJoin, FaucetError> {
76 CompiledCrossJoin::compile(self)
77 }
78
79 pub fn into_stage(&self) -> Result<TransformStage, FaucetError> {
82 let compiled = self.compile()?;
83 Ok(TransformStage::PageFn(Arc::new(move |page: Vec<Value>| {
84 let mut out = Vec::with_capacity(page.len());
85 for rec in page {
86 out.extend(compiled.apply(rec)?);
87 }
88 Ok(out)
89 })))
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct CompiledCrossJoin {
96 spec: CrossJoinSpec,
97}
98
99impl CompiledCrossJoin {
100 fn compile(spec: &CrossJoinSpec) -> Result<Self, FaucetError> {
101 if spec.arrays.len() < 2 {
102 return Err(FaucetError::Config(
103 "cross_join: `arrays` needs at least 2 fields (a single array is `explode`)".into(),
104 ));
105 }
106 if spec.arrays.iter().any(|a| a.trim().is_empty()) {
107 return Err(FaucetError::Config(
108 "cross_join: `arrays` entries must be non-empty field names".into(),
109 ));
110 }
111 let mut seen = std::collections::HashSet::new();
112 for a in &spec.arrays {
113 if !seen.insert(a) {
114 return Err(FaucetError::Config(format!(
115 "cross_join: duplicate array field `{a}`"
116 )));
117 }
118 }
119 if spec.max_product == 0 {
120 return Err(FaucetError::Config(
121 "cross_join: `max_product` must be greater than 0".into(),
122 ));
123 }
124 Ok(Self { spec: spec.clone() })
125 }
126
127 pub fn apply(&self, rec: Value) -> Result<Vec<Value>, FaucetError> {
131 let Value::Object(obj) = rec else {
132 return Ok(vec![rec]);
133 };
134
135 let mut sets: Vec<(&str, Vec<Value>)> = Vec::with_capacity(self.spec.arrays.len());
139 for name in &self.spec.arrays {
140 let elems = match obj.get(name) {
141 Some(Value::Array(a)) => a.clone(),
142 _ => Vec::new(),
143 };
144 sets.push((name.as_str(), elems));
145 }
146
147 match self.spec.on_empty {
149 OnEmpty::Skip => {
150 if sets.iter().any(|(_, e)| e.is_empty()) {
151 return Ok(Vec::new());
152 }
153 }
154 OnEmpty::OneRow => {
155 for (_, e) in sets.iter_mut() {
156 if e.is_empty() {
157 e.push(Value::Null);
158 }
159 }
160 }
161 }
162
163 sets.iter()
166 .try_fold(1usize, |acc, (_, e)| acc.checked_mul(e.len()))
167 .filter(|n| *n <= self.spec.max_product)
168 .ok_or_else(|| {
169 FaucetError::Transform(format!(
170 "cross_join: record's cartesian product over {:?} exceeds max_product={} \
171 — narrow the arrays or raise max_product",
172 self.spec.arrays, self.spec.max_product
173 ))
174 })?;
175
176 let mut parent = Map::new();
178 if self.spec.keep_parent {
179 for (k, v) in &obj {
180 if self.spec.drop_arrays && self.spec.arrays.iter().any(|a| a == k) {
181 continue;
182 }
183 parent.insert(k.clone(), v.clone());
184 }
185 } else if !self.spec.drop_arrays {
186 for name in &self.spec.arrays {
188 if let Some(v) = obj.get(name) {
189 parent.insert(name.clone(), v.clone());
190 }
191 }
192 }
193
194 let mut rows: Vec<Map<String, Value>> = vec![parent];
196 for (name, elems) in &sets {
197 let mut next = Vec::with_capacity(rows.len() * elems.len());
198 for base in &rows {
199 for elem in elems {
200 let mut row = base.clone();
201 merge_element(&mut row, name, elem, self.spec.prefix);
202 next.push(row);
203 }
204 }
205 rows = next;
206 }
207
208 Ok(rows.into_iter().map(Value::Object).collect())
209 }
210}
211
212fn merge_element(row: &mut Map<String, Value>, array_name: &str, elem: &Value, prefix: bool) {
216 match elem {
217 Value::Object(fields) => {
218 for (k, v) in fields {
219 let key = if prefix {
220 format!("{array_name}_{k}")
221 } else {
222 k.clone()
223 };
224 row.insert(key, v.clone());
225 }
226 }
227 other => {
228 row.insert(array_name.to_string(), other.clone());
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use serde_json::json;
237
238 fn spec(arrays: &[&str]) -> CrossJoinSpec {
239 CrossJoinSpec {
240 arrays: arrays.iter().map(|s| s.to_string()).collect(),
241 prefix: false,
242 keep_parent: true,
243 on_empty: OnEmpty::Skip,
244 drop_arrays: true,
245 max_product: DEFAULT_MAX_PRODUCT,
246 }
247 }
248
249 #[test]
250 fn crosses_two_sibling_arrays() {
251 let c = spec(&["jobs", "comp"]).compile().unwrap();
252 let rec = json!({
253 "emp_id": 1,
254 "jobs": [{"title": "eng"}, {"title": "mgr"}],
255 "comp": [{"amount": 100}, {"amount": 200}]
256 });
257 let out = c.apply(rec).unwrap();
258 assert_eq!(out.len(), 4); assert_eq!(out[0]["emp_id"], json!(1));
261 assert_eq!(out[0]["title"], json!("eng"));
262 assert_eq!(out[0]["amount"], json!(100));
263 assert!(out[0].get("jobs").is_none());
264 }
265
266 #[test]
267 fn skip_vs_one_row_on_empty() {
268 let rec = json!({"id": 1, "jobs": [{"t": "a"}], "comp": []});
269 assert!(
270 spec(&["jobs", "comp"])
271 .compile()
272 .unwrap()
273 .apply(rec.clone())
274 .unwrap()
275 .is_empty()
276 );
277 let mut s = spec(&["jobs", "comp"]);
278 s.on_empty = OnEmpty::OneRow;
279 let out = s.compile().unwrap().apply(rec).unwrap();
280 assert_eq!(out.len(), 1);
281 assert_eq!(out[0]["t"], json!("a"));
282 assert_eq!(out[0]["comp"], json!(null));
285 }
286
287 #[test]
288 fn prefix_avoids_collisions() {
289 let mut s = spec(&["a", "b"]);
290 s.prefix = true;
291 let rec = json!({"a": [{"x": 1}], "b": [{"x": 2}]});
292 let out = s.compile().unwrap().apply(rec).unwrap();
293 assert_eq!(out.len(), 1);
294 assert_eq!(out[0]["a_x"], json!(1));
295 assert_eq!(out[0]["b_x"], json!(2));
296 }
297
298 #[test]
299 fn scalar_elements_wrap_under_array_name() {
300 let rec = json!({"id": 1, "tags": ["x", "y"], "vals": [10]});
301 let out = spec(&["tags", "vals"])
302 .compile()
303 .unwrap()
304 .apply(rec)
305 .unwrap();
306 assert_eq!(out.len(), 2);
307 assert_eq!(out[0]["tags"], json!("x"));
308 assert_eq!(out[0]["vals"], json!(10));
309 }
310
311 #[test]
312 fn max_product_overflow_errors() {
313 let mut s = spec(&["a", "b"]);
314 s.max_product = 3;
315 let rec = json!({"a": [1, 2], "b": [1, 2]}); assert!(s.compile().unwrap().apply(rec).is_err());
317 }
318
319 #[test]
320 fn non_object_and_missing_array_passthrough() {
321 let c = spec(&["a", "b"]).compile().unwrap();
322 assert_eq!(c.apply(json!(5)).unwrap(), vec![json!(5)]);
324 assert!(c.apply(json!({"a": [{"x": 1}]})).unwrap().is_empty());
326 }
327
328 #[test]
329 fn compile_rejects_bad_specs() {
330 assert!(spec(&["only"]).compile().is_err()); assert!(spec(&["a", ""]).compile().is_err()); assert!(spec(&["a", "a"]).compile().is_err()); let mut s = spec(&["a", "b"]);
334 s.max_product = 0;
335 assert!(s.compile().is_err());
336 }
337
338 #[test]
339 fn into_stage_is_pagefn_and_flat_maps() {
340 let stage = spec(&["a", "b"]).into_stage().unwrap();
341 assert!(matches!(stage, TransformStage::PageFn(_)));
342 }
343
344 #[test]
345 fn into_stage_pagefn_runs_over_a_page() {
346 let TransformStage::PageFn(f) = spec(&["jobs", "comp"]).into_stage().unwrap() else {
348 panic!("expected PageFn");
349 };
350 let page = vec![
351 json!({"id": 1, "jobs": [{"t": "a"}], "comp": [{"c": 1}, {"c": 2}]}),
352 json!({"id": 2, "jobs": [{"t": "b"}, {"t": "c"}], "comp": [{"c": 9}]}),
353 ];
354 let out = f(page).unwrap();
355 assert_eq!(out.len(), 2 + 2); assert_eq!(out[0]["id"], json!(1));
357 }
358
359 #[test]
360 fn into_stage_pagefn_propagates_overflow_error() {
361 let mut s = spec(&["a", "b"]);
362 s.max_product = 1;
363 let TransformStage::PageFn(f) = s.into_stage().unwrap() else {
364 panic!("expected PageFn");
365 };
366 assert!(f(vec![json!({"a": [1, 2], "b": [1, 2]})]).is_err());
367 }
368
369 #[test]
370 fn keep_parent_false_drops_scalars() {
371 let mut s = spec(&["a", "b"]);
372 s.keep_parent = false;
373 let out = s
374 .compile()
375 .unwrap()
376 .apply(json!({"id": 7, "a": [{"x": 1}], "b": [{"y": 2}]}))
377 .unwrap();
378 assert_eq!(out.len(), 1);
379 assert!(out[0].get("id").is_none()); assert_eq!(out[0]["x"], json!(1));
381 }
382
383 #[test]
384 fn keep_parent_false_keep_arrays_when_not_dropping() {
385 let mut s = spec(&["a", "b"]);
386 s.keep_parent = false;
387 s.drop_arrays = false;
388 let out = s
389 .compile()
390 .unwrap()
391 .apply(json!({"id": 7, "a": [{"x": 1}], "b": [{"y": 2}]}))
392 .unwrap();
393 assert_eq!(out.len(), 1);
394 assert_eq!(out[0]["a"], json!([{"x": 1}]));
396 assert!(out[0].get("id").is_none());
397 }
398
399 #[test]
400 fn keep_parent_true_no_drop_keeps_arrays_and_scalars() {
401 let mut s = spec(&["a", "b"]);
402 s.drop_arrays = false;
403 let out = s
404 .compile()
405 .unwrap()
406 .apply(json!({"id": 7, "a": [{"x": 1}], "b": [{"y": 2}]}))
407 .unwrap();
408 assert_eq!(out[0]["id"], json!(7));
409 assert_eq!(out[0]["a"], json!([{"x": 1}])); assert_eq!(out[0]["x"], json!(1)); }
412}