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> {
135 let Value::Object(obj) = rec else {
136 return Ok(vec![rec]);
137 };
138
139 let mut sets: Vec<(&str, Vec<Value>)> = Vec::with_capacity(self.spec.arrays.len());
143 for name in &self.spec.arrays {
144 let elems = match obj.get(name) {
145 Some(Value::Array(a)) => a.clone(),
146 None | Some(Value::Null) => Vec::new(),
147 Some(other) => {
148 return Err(FaucetError::Transform(format!(
149 "cross_join: field `{name}` is {} rather than an array",
150 match other {
151 Value::Object(_) => "an object",
152 Value::String(_) => "a string",
153 Value::Number(_) => "a number",
154 _ => "a boolean",
155 }
156 )));
157 }
158 };
159 sets.push((name.as_str(), elems));
160 }
161
162 match self.spec.on_empty {
164 OnEmpty::Skip => {
165 if sets.iter().any(|(_, e)| e.is_empty()) {
166 return Ok(Vec::new());
167 }
168 }
169 OnEmpty::OneRow => {
170 for (_, e) in sets.iter_mut() {
171 if e.is_empty() {
172 e.push(Value::Null);
173 }
174 }
175 }
176 }
177
178 sets.iter()
181 .try_fold(1usize, |acc, (_, e)| acc.checked_mul(e.len()))
182 .filter(|n| *n <= self.spec.max_product)
183 .ok_or_else(|| {
184 FaucetError::Transform(format!(
185 "cross_join: record's cartesian product over {:?} exceeds max_product={} \
186 — narrow the arrays or raise max_product",
187 self.spec.arrays, self.spec.max_product
188 ))
189 })?;
190
191 let mut parent = Map::new();
193 if self.spec.keep_parent {
194 for (k, v) in &obj {
195 if self.spec.drop_arrays && self.spec.arrays.iter().any(|a| a == k) {
196 continue;
197 }
198 parent.insert(k.clone(), v.clone());
199 }
200 } else if !self.spec.drop_arrays {
201 for name in &self.spec.arrays {
203 if let Some(v) = obj.get(name) {
204 parent.insert(name.clone(), v.clone());
205 }
206 }
207 }
208
209 let mut rows: Vec<Map<String, Value>> = vec![parent];
211 for (name, elems) in &sets {
212 let mut next = Vec::with_capacity(rows.len() * elems.len());
213 for base in &rows {
214 for elem in elems {
215 let mut row = base.clone();
216 merge_element(&mut row, name, elem, self.spec.prefix)?;
217 next.push(row);
218 }
219 }
220 rows = next;
221 }
222
223 Ok(rows.into_iter().map(Value::Object).collect())
224 }
225}
226
227fn merge_element(
232 row: &mut Map<String, Value>,
233 array_name: &str,
234 elem: &Value,
235 prefix: bool,
236) -> Result<(), FaucetError> {
237 match elem {
238 Value::Object(fields) => {
239 for (k, v) in fields {
240 let key = if prefix {
241 format!("{array_name}_{k}")
242 } else {
243 k.clone()
244 };
245 if row.contains_key(&key) {
246 return Err(FaucetError::Transform(format!(
247 "cross_join: field `{key}` of `{array_name}` would overwrite an \
248 existing column — set `prefix: true` or rename it first"
249 )));
250 }
251 row.insert(key, v.clone());
252 }
253 }
254 other => {
255 row.insert(array_name.to_string(), other.clone());
256 }
257 }
258 Ok(())
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use serde_json::json;
265
266 fn spec(arrays: &[&str]) -> CrossJoinSpec {
267 CrossJoinSpec {
268 arrays: arrays.iter().map(|s| s.to_string()).collect(),
269 prefix: false,
270 keep_parent: true,
271 on_empty: OnEmpty::Skip,
272 drop_arrays: true,
273 max_product: DEFAULT_MAX_PRODUCT,
274 }
275 }
276
277 #[test]
278 fn crosses_two_sibling_arrays() {
279 let c = spec(&["jobs", "comp"]).compile().unwrap();
280 let rec = json!({
281 "emp_id": 1,
282 "jobs": [{"title": "eng"}, {"title": "mgr"}],
283 "comp": [{"amount": 100}, {"amount": 200}]
284 });
285 let out = c.apply(rec).unwrap();
286 assert_eq!(out.len(), 4); assert_eq!(out[0]["emp_id"], json!(1));
289 assert_eq!(out[0]["title"], json!("eng"));
290 assert_eq!(out[0]["amount"], json!(100));
291 assert!(out[0].get("jobs").is_none());
292 }
293
294 #[test]
295 fn skip_vs_one_row_on_empty() {
296 let rec = json!({"id": 1, "jobs": [{"t": "a"}], "comp": []});
297 assert!(
298 spec(&["jobs", "comp"])
299 .compile()
300 .unwrap()
301 .apply(rec.clone())
302 .unwrap()
303 .is_empty()
304 );
305 let mut s = spec(&["jobs", "comp"]);
306 s.on_empty = OnEmpty::OneRow;
307 let out = s.compile().unwrap().apply(rec).unwrap();
308 assert_eq!(out.len(), 1);
309 assert_eq!(out[0]["t"], json!("a"));
310 assert_eq!(out[0]["comp"], json!(null));
313 }
314
315 #[test]
316 fn prefix_avoids_collisions() {
317 let mut s = spec(&["a", "b"]);
318 s.prefix = true;
319 let rec = json!({"a": [{"x": 1}], "b": [{"x": 2}]});
320 let out = s.compile().unwrap().apply(rec).unwrap();
321 assert_eq!(out.len(), 1);
322 assert_eq!(out[0]["a_x"], json!(1));
323 assert_eq!(out[0]["b_x"], json!(2));
324 }
325
326 #[test]
327 fn scalar_elements_wrap_under_array_name() {
328 let rec = json!({"id": 1, "tags": ["x", "y"], "vals": [10]});
329 let out = spec(&["tags", "vals"])
330 .compile()
331 .unwrap()
332 .apply(rec)
333 .unwrap();
334 assert_eq!(out.len(), 2);
335 assert_eq!(out[0]["tags"], json!("x"));
336 assert_eq!(out[0]["vals"], json!(10));
337 }
338
339 #[test]
340 fn max_product_overflow_errors() {
341 let mut s = spec(&["a", "b"]);
342 s.max_product = 3;
343 let rec = json!({"a": [1, 2], "b": [1, 2]}); assert!(s.compile().unwrap().apply(rec).is_err());
345 }
346
347 #[test]
348 fn non_object_and_missing_array_passthrough() {
349 let c = spec(&["a", "b"]).compile().unwrap();
350 assert_eq!(c.apply(json!(5)).unwrap(), vec![json!(5)]);
352 assert!(c.apply(json!({"a": [{"x": 1}]})).unwrap().is_empty());
354 }
355
356 #[test]
357 fn overwriting_a_column_is_refused_without_prefix() {
358 let c = spec(&["a", "b"]).compile().unwrap();
359 let err = c
360 .apply(json!({"id": 1, "a": [{"id": 9}], "b": [{"y": 2}]}))
361 .unwrap_err();
362 assert!(err.to_string().contains("field `id` of `a`"), "{err}");
363 let err = c
364 .apply(json!({"a": [{"x": 1}], "b": [{"x": 2}]}))
365 .unwrap_err();
366 assert!(err.to_string().contains("field `x` of `b`"), "{err}");
367 let mut p = spec(&["a", "b"]);
368 p.prefix = true;
369 let out = p
370 .compile()
371 .unwrap()
372 .apply(json!({"id": 1, "a": [{"id": 9}], "b": [{"id": 2}]}))
373 .unwrap();
374 assert_eq!(out[0]["id"], json!(1));
375 assert_eq!(out[0]["a_id"], json!(9));
376 }
377
378 #[test]
379 fn a_present_non_array_is_a_type_error() {
380 let c = spec(&["a", "b"]).compile().unwrap();
381 for bad in [json!({"x": 1}), json!("s"), json!(3), json!(true)] {
382 let err = c.apply(json!({"a": [{"x": 1}], "b": bad})).unwrap_err();
383 assert!(err.to_string().contains("rather than an array"), "{err}");
384 }
385 assert!(
386 c.apply(json!({"a": [{"x": 1}], "b": null}))
387 .unwrap()
388 .is_empty()
389 );
390 }
391
392 #[test]
393 fn compile_rejects_bad_specs() {
394 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"]);
398 s.max_product = 0;
399 assert!(s.compile().is_err());
400 }
401
402 #[test]
403 fn into_stage_is_pagefn_and_flat_maps() {
404 let stage = spec(&["a", "b"]).into_stage().unwrap();
405 assert!(matches!(stage, TransformStage::PageFn(_)));
406 }
407
408 #[test]
409 fn into_stage_pagefn_runs_over_a_page() {
410 let TransformStage::PageFn(f) = spec(&["jobs", "comp"]).into_stage().unwrap() else {
412 panic!("expected PageFn");
413 };
414 let page = vec![
415 json!({"id": 1, "jobs": [{"t": "a"}], "comp": [{"c": 1}, {"c": 2}]}),
416 json!({"id": 2, "jobs": [{"t": "b"}, {"t": "c"}], "comp": [{"c": 9}]}),
417 ];
418 let out = f(page).unwrap();
419 assert_eq!(out.len(), 2 + 2); assert_eq!(out[0]["id"], json!(1));
421 }
422
423 #[test]
424 fn into_stage_pagefn_propagates_overflow_error() {
425 let mut s = spec(&["a", "b"]);
426 s.max_product = 1;
427 let TransformStage::PageFn(f) = s.into_stage().unwrap() else {
428 panic!("expected PageFn");
429 };
430 assert!(f(vec![json!({"a": [1, 2], "b": [1, 2]})]).is_err());
431 }
432
433 #[test]
434 fn keep_parent_false_drops_scalars() {
435 let mut s = spec(&["a", "b"]);
436 s.keep_parent = false;
437 let out = s
438 .compile()
439 .unwrap()
440 .apply(json!({"id": 7, "a": [{"x": 1}], "b": [{"y": 2}]}))
441 .unwrap();
442 assert_eq!(out.len(), 1);
443 assert!(out[0].get("id").is_none()); assert_eq!(out[0]["x"], json!(1));
445 }
446
447 #[test]
448 fn keep_parent_false_keep_arrays_when_not_dropping() {
449 let mut s = spec(&["a", "b"]);
450 s.keep_parent = false;
451 s.drop_arrays = false;
452 let out = s
453 .compile()
454 .unwrap()
455 .apply(json!({"id": 7, "a": [{"x": 1}], "b": [{"y": 2}]}))
456 .unwrap();
457 assert_eq!(out.len(), 1);
458 assert_eq!(out[0]["a"], json!([{"x": 1}]));
460 assert!(out[0].get("id").is_none());
461 }
462
463 #[test]
464 fn keep_parent_true_no_drop_keeps_arrays_and_scalars() {
465 let mut s = spec(&["a", "b"]);
466 s.drop_arrays = false;
467 let out = s
468 .compile()
469 .unwrap()
470 .apply(json!({"id": 7, "a": [{"x": 1}], "b": [{"y": 2}]}))
471 .unwrap();
472 assert_eq!(out[0]["id"], json!(7));
473 assert_eq!(out[0]["a"], json!([{"x": 1}])); assert_eq!(out[0]["x"], json!(1)); }
476}