Skip to main content

polydat_core/library/
json.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! JSON construction, serialization, and manipulation nodes.
5//!
6//! JSON is a first-class `Value` type in the GK. Nodes can produce
7//! and consume `Value::Json(std::sync::Arc::new(serde_json::Value))` directly, avoiding
8//! serialization/deserialization round-trips when passing structured
9//! data between nodes or to adapters that consume JSON natively.
10//!
11//! Objects are built compositionally: `json_with(key, value)`
12//! produces a single-pair partial Json, and the variadic
13//! `json_object(parts...)` merges them. Workload syntax:
14//!
15//! ```text
16//! record := json_object(
17//!     json_with("name", name_wire),
18//!     json_with("age", age_wire),
19//! )
20//! ```
21
22use crate::ast::{Value, ValueRef};
23use serde_json::json;
24
25// =================================================================
26// Construction: build JSON values from inputs
27// =================================================================
28
29/// Build a single-pair partial JSON object: `{ key: value }`.
30///
31/// Signature: `(value: any) -> (json)` with const `key: &str`.
32///
33/// The compositional building block for `json_object`. Each
34/// `json_with` produces a one-entry Json Object that the variadic
35/// `json_object` merger flattens. The value is converted via
36/// `value_to_json`, so any `Value` variant works as the input:
37///   - U64 / F64 → JSON number
38///   - Bool → JSON bool
39///   - Str → JSON string
40///   - Json → nested as-is
41#[crate::polydat_node(category = Json)]
42fn json_with(
43    key: crate::derive_support::Const<&str>,
44    value: Value,
45) -> std::sync::Arc<serde_json::Value> {
46    let mut m = serde_json::Map::new();
47    m.insert(key.0.to_string(), value_to_json(&value));
48    std::sync::Arc::new(serde_json::Value::Object(m))
49}
50
51/// Merge N partial Json Objects into a single Json Object.
52///
53/// Signature: `(parts_0: json, parts_1: json, ...) -> (json)`
54///
55/// Variadic merger over Json inputs. Each input is expected to be a
56/// Json Object (typically produced by `json_with`); non-Object
57/// variants are silently skipped — keeping the operator
58/// composable with conditional `if(...)` branches that may yield
59/// `null`. Later parts shadow earlier on key collision.
60#[crate::polydat_node(category = Json, variadic_min = 0)]
61fn json_object(parts: &[Value]) -> std::sync::Arc<serde_json::Value> {
62    std::sync::Arc::new(json_object_of(parts))
63}
64
65/// The merge `json_object` performs, over borrowed views (SRD 115
66/// §6.1), so both tiers run one body: P1 views its `Value` inputs, the
67/// compiled helper views its slots.
68pub(crate) fn json_object_of_refs<'a>(
69    parts: impl IntoIterator<Item = ValueRef<'a>>,
70) -> serde_json::Value {
71    let mut merged = serde_json::Map::new();
72    for v in parts {
73        if let ValueRef::Json(serde_json::Value::Object(map)) = v {
74            for (k, val) in map {
75                merged.insert(k.clone(), val.clone());
76            }
77        }
78    }
79    serde_json::Value::Object(merged)
80}
81
82/// The merge `json_object` performs, over owned values.
83pub(crate) fn json_object_of(parts: &[Value]) -> serde_json::Value {
84    json_object_of_refs(parts.iter().map(ValueRef::from))
85}
86
87/// The array `json_array` builds, over borrowed views.
88pub(crate) fn json_array_of_refs<'a>(
89    elems: impl IntoIterator<Item = ValueRef<'a>>,
90) -> serde_json::Value {
91    serde_json::Value::Array(elems.into_iter().map(|v| json_of_ref(v)).collect())
92}
93
94/// The array `json_array` builds, over owned values.
95pub(crate) fn json_array_of(elems: &[Value]) -> serde_json::Value {
96    json_array_of_refs(elems.iter().map(ValueRef::from))
97}
98
99/// Build a JSON array from N inputs.
100///
101/// Signature: `(elem_0: any, elem_1: any, ...) -> (json)`
102///
103/// Each element is converted through `value_to_json`, which
104/// dispatches on the `Value` variant.
105#[crate::polydat_node(category = Json, variadic_min = 0)]
106fn json_array(elems: &[Value]) -> std::sync::Arc<serde_json::Value> {
107    std::sync::Arc::new(json_array_of(elems))
108}
109
110/// Wrap a single value as a JSON value.
111///
112/// Signature: `(input: any) -> (json)`
113///
114/// Useful for promoting a scalar to JSON for further composition.
115/// Each variant is coerced through `value_to_json`.
116#[crate::polydat_node(category = Json)]
117fn to_json(input: Value) -> std::sync::Arc<serde_json::Value> {
118    std::sync::Arc::new(value_to_json(&input))
119}
120
121/// Merge two JSON objects into one (shallow merge, right wins).
122///
123/// Signature: `(left: json, right: json) -> (json)`
124#[crate::polydat_node(category = Json)]
125fn json_merge(
126    left: &serde_json::Value,
127    right: &serde_json::Value,
128) -> std::sync::Arc<serde_json::Value> {
129    let mut result = left.clone();
130    if let (serde_json::Value::Object(base), serde_json::Value::Object(overlay)) =
131        (&mut result, right)
132    {
133        for (k, v) in overlay {
134            base.insert(k.clone(), v.clone());
135        }
136    }
137    std::sync::Arc::new(result)
138}
139
140// =================================================================
141// Serialization: JSON ↔ String
142// =================================================================
143
144/// Serialize a JSON value to a compact string.
145///
146/// Signature: `json_to_str(input: json) -> (String)`
147///
148/// Workload-callable AND the auto-adapter the assembly phase inserts
149/// on Json → Str boundaries; `compile::assembly` constructs the
150/// `JsonToStr` struct for the auto-adapter slot.
151#[crate::polydat_node(category = Conversions)]
152fn json_to_str(input: &serde_json::Value) -> String {
153    input.to_string()
154}
155
156/// Serialize a JSON value to a pretty-printed string.
157#[crate::polydat_node(category = Json)]
158fn json_to_str_pretty(input: &serde_json::Value) -> String {
159    serde_json::to_string_pretty(input).unwrap_or_default()
160}
161
162/// Parse a JSON string into a JSON value.
163#[crate::polydat_node(category = Json)]
164fn str_to_json(input: &str) -> std::sync::Arc<serde_json::Value> {
165    let parsed = serde_json::from_str(input).unwrap_or(serde_json::Value::Null);
166    std::sync::Arc::new(parsed)
167}
168
169/// Escape a string for safe embedding in a JSON string value.
170///
171/// Signature: `(input: String) -> (String)`
172///
173/// Escape a string for embedding inside JSON. Escapes `"`,
174/// `\`, control characters, etc. Does NOT add surrounding
175/// quotes — the result is the interior of a JSON string.
176#[crate::polydat_node(category = Json)]
177fn escape_json(input: String) -> String {
178    // serde_json::to_string adds quotes; strip them for interior-only.
179    let json_str = serde_json::to_string(&input).unwrap_or_default();
180    json_str[1..json_str.len() - 1].to_string()
181}
182
183// =================================================================
184// Field access
185// =================================================================
186
187/// Extract a field from a JSON object by key.
188///
189/// Extract a single field by key from a JSON object input.
190/// Returns the field's value (or `null` if missing).
191#[crate::polydat_node(category = Json)]
192fn json_field(
193    input: &serde_json::Value,
194    key: crate::derive_support::Const<&str>,
195) -> std::sync::Arc<serde_json::Value> {
196    std::sync::Arc::new(input.get(*key).cloned().unwrap_or(serde_json::Value::Null))
197}
198
199// =================================================================
200// Helpers
201// =================================================================
202
203pub(crate) fn value_to_json(v: &Value) -> serde_json::Value {
204    json_of_ref(ValueRef::from(v))
205}
206
207/// The JSON coercion over a borrowed view (SRD 115 §6.1): the compiled
208/// helpers call this on their slots without owning a `Value`.
209pub(crate) fn json_of_ref(v: ValueRef<'_>) -> serde_json::Value {
210    match v {
211        // Bytes uses base64 here (JSON-payload convention) instead of
212        // the hex form `Value::to_json_value` returns, so the Bytes
213        // arm stays local. Everything else delegates to the
214        // canonical typed-to-JSON projection — adding a new Value
215        // variant doesn't require a parallel arm here.
216        ValueRef::Bytes(b) => {
217            use base64::Engine;
218            json!(base64::engine::general_purpose::STANDARD.encode(b))
219        }
220        other => other.to_json_value(),
221    }
222}
223
224/// Flatten a JSON tree into a single newline-separated text by
225/// concatenating every leaf value's textual form.
226///
227/// Walks the tree depth-first; for each leaf:
228///   - Strings emit their text verbatim (newlines inside the
229///     string survive — important when the JSON carries
230///     multi-line content like CQL `create_statement`).
231///   - Numbers / booleans emit their natural string form.
232///   - Nulls are skipped.
233/// Successive leaves are joined with `\n`.
234///
235/// Use case: probe-phase regex matches over a multi-row body.
236/// `regex_match(json_text(body), "(?im)^TABLE …")` lets the
237/// regex see the actual newlines inside `create_statement`-shape
238/// columns; the previous `regex_match(exactly_one_value(body), …)`
239/// shape silently degrades when the body isn't unary AND when
240/// the upstream wire is forced through a `JsonToStr` adapter
241/// that escapes newlines as `\n` literals.
242///
243/// The input is a PolyWire so non-Json values fall through their
244/// display form (a Str input is already textual; numeric/bool
245/// scalars render naturally), useful when the upstream wire is
246/// heterogeneous (e.g. a body extern that sometimes carries a
247/// string, sometimes a JSON value).
248#[crate::polydat_node(category = Json)]
249fn json_text(input: Value) -> String {
250    json_text_of(&input)
251}
252
253/// The text `json_text` produces, shared with its compiled helper.
254pub(crate) fn json_text_of(input: &Value) -> String {
255    let mut buf = String::new();
256    json_text_into(input, &mut buf);
257    buf
258}
259
260/// The text `json_text` produces, written into any text sink (the
261/// cycle arena writer in the compiled helper, SRD 115 §6).
262pub(crate) fn json_text_into<W: std::fmt::Write>(input: &Value, out: &mut W) {
263    json_text_ref_into(ValueRef::from(input), out);
264}
265
266/// `json_text` over a borrowed view, into any sink.
267pub(crate) fn json_text_ref_into<W: std::fmt::Write>(input: ValueRef<'_>, out: &mut W) {
268    match input {
269        ValueRef::Json(j) => {
270            let mut first = true;
271            walk_json_leaves(j, out, &mut first);
272        }
273        other => {
274            let _ = out.write_str(&other.display());
275        }
276    }
277}
278
279fn walk_json_leaves<W: std::fmt::Write>(j: &serde_json::Value, out: &mut W, first: &mut bool) {
280    use serde_json::Value as J;
281    let mut leaf = |out: &mut W, text: &str| {
282        if !*first {
283            let _ = out.write_char('\n');
284        }
285        *first = false;
286        let _ = out.write_str(text);
287    };
288    match j {
289        J::String(s) => leaf(out, s),
290        J::Number(n) => leaf(out, &n.to_string()),
291        J::Bool(b) => leaf(out, if *b { "true" } else { "false" }),
292        J::Null => {}
293        J::Array(arr) => {
294            for item in arr {
295                walk_json_leaves(item, out, first);
296            }
297        }
298        J::Object(obj) => {
299            for value in obj.values() {
300                walk_json_leaves(value, out, first);
301            }
302        }
303    }
304}
305
306/// `body_column_i32(body, "name")` — extract the named column from
307/// every row of a JSON result body, parse each as i32, and return
308/// the values as a `VecI32` wire.
309///
310/// This is the canonical capture path for tabular result data into
311/// typed-vector wires. Adapter result bodies that already serialize
312/// to `[{ "key": 1, ... }, { "key": 2, ... }, ...]`-shaped JSON can
313/// expose per-column wires for downstream readers (the recall
314/// evaluator, custom metrics, etc.) without forcing string
315/// round-trips through the metric reader.
316///
317/// Robust extraction rules:
318/// - Body shape `[{...}, {...}, ...]`: walks each row, looks up the
319///   column by name, parses as i32 via `json_value_as_i32`.
320/// - Body shape `{ "rows": [...] }`: walks `rows`; same per-row
321///   extraction as above. Matches common envelope formats (Jolokia,
322///   HTTP wrappers).
323/// - Body shape `{ "key": value, ... }` (single row at top level):
324///   produces a single-element vector.
325/// - Non-JSON input: empty vector. This preserves the "no values
326///   extracted" diagnostic at the evaluator instead of panicking
327///   here.
328///
329/// Rows whose column is absent / null / unparseable contribute
330/// nothing (no zero-fill, no error). Mirrors the legacy host-side
331/// `extract_indices_from_json` JSON-walk so workloads can swap to
332/// this typed-wire path without recall-value drift.
333///
334/// The body is a PolyWire so the non-Json fallback ("empty vector,
335/// no panic") applies; `column` is a const string with no default
336/// (required workload arg).
337#[crate::polydat_node(category = Json)]
338fn body_column_i32(body: Value, column: crate::derive_support::Const<&str>) -> Vec<i32> {
339    let json = match &body {
340        Value::Json(j) => j,
341        _ => return Vec::new(),
342    };
343    extract_column_i32(json, column.0)
344}
345
346/// Walk a JSON value extracting `column` from every row. Handles
347/// top-level array, `{ rows: [...] }` envelope, and bare object
348/// forms — the same shapes adapter `ResultBody::to_json()` produces
349/// across CQL / HTTP / stdout drivers.
350fn extract_column_i32(json: &serde_json::Value, column: &str) -> Vec<i32> {
351    match json {
352        serde_json::Value::Array(rows) => rows
353            .iter()
354            .filter_map(|row| json_value_as_i32(row.get(column)?))
355            .collect(),
356        serde_json::Value::Object(obj) => {
357            // Two shapes can land here: an envelope object with a
358            // `rows` array (preferred), or a single row object
359            // whose column we extract as a one-element vector.
360            // Try envelope first to match the common
361            // `{rows: [...]}` shape adapters use.
362            if let Some(serde_json::Value::Array(rows)) = obj.get("rows") {
363                return rows
364                    .iter()
365                    .filter_map(|row| json_value_as_i32(row.get(column)?))
366                    .collect();
367            }
368            obj.get(column)
369                .and_then(json_value_as_i32)
370                .map(|n| vec![n])
371                .unwrap_or_default()
372        }
373        _ => Vec::new(),
374    }
375}
376
377/// Parse a JSON value as i32 with the same tolerance the legacy
378/// `json_field_as_i64` extractor used: number→cast, string→parse,
379/// bool/null/object/array→skip. Out-of-range numerics saturate to
380/// the closest i32 boundary; preserves the legacy "best-effort"
381/// behaviour rather than silently dropping rows.
382fn json_value_as_i32(v: &serde_json::Value) -> Option<i32> {
383    match v {
384        serde_json::Value::Number(n) => n
385            .as_i64()
386            .map(|i| i.clamp(i32::MIN as i64, i32::MAX as i64) as i32)
387            .or_else(|| n.as_u64().map(|u| u.min(i32::MAX as u64) as i32))
388            .or_else(|| {
389                n.as_f64()
390                    .and_then(|f| if f.is_finite() { Some(f as i32) } else { None })
391            }),
392        serde_json::Value::String(s) => s.trim().parse::<i32>().ok(),
393        _ => None,
394    }
395}
396
397// =================================================================
398// Vector operations: normalize and random generation
399// =================================================================
400
401/// L2-normalize a bracket-encoded float vector string `[1.0,2.0,3.0]`.
402///
403/// Parses the bracket-format vector, computes the L2 norm, and returns
404/// a normalized vector in the same bracket format. Passes through
405/// unchanged if the input is not bracket-encoded or the norm is
406/// effectively zero.
407///
408/// Signature: `normalize_vector(vector: Str) -> (output: Str)`
409#[crate::polydat_node(category = Json)]
410fn normalize_vector(vector: &str) -> String {
411    let trimmed = vector.trim();
412    if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
413        return vector.to_string();
414    }
415    let inner = &trimmed[1..trimmed.len() - 1];
416    let values: Vec<f64> = inner
417        .split(',')
418        .filter_map(|v| v.trim().parse::<f64>().ok())
419        .collect();
420    let norm = values.iter().map(|v| v * v).sum::<f64>().sqrt();
421    if norm < 1e-15 {
422        return vector.to_string();
423    }
424    let normalized: Vec<String> = values.iter().map(|v| format!("{}", v / norm)).collect();
425    format!("[{}]", normalized.join(","))
426}
427
428/// Generate a deterministic f64 vector as a bracket-encoded JSON array string.
429///
430/// Uses xxHash3 to derive pseudo-random values in `[min, max)` for each
431/// dimension. The seed and dimension are provided at cycle time; `min`
432/// and `max` are constants set at construction.
433///
434/// Signature: `random_vector(seed: u64, dim: u64) -> (output: Str)`
435/// Consts: `min: f64 = 0.0`, `max: f64 = 1.0`
436#[crate::polydat_node(category = Json)]
437fn random_vector(
438    seed: u64,
439    dim: u64,
440    #[poly_default(0.0f64)] min: crate::derive_support::Const<f64>,
441    #[poly_default(1.0f64)] max: crate::derive_support::Const<f64>,
442) -> String {
443    let min_v = *min;
444    let max_v = *max;
445    let range = max_v - min_v;
446    let dim = dim as usize;
447    let mut h = seed;
448    let mut values = Vec::with_capacity(dim);
449    for _ in 0..dim {
450        h = xxhash_rust::xxh3::xxh3_64(&h.to_le_bytes());
451        let unit = (h as f64) / (u64::MAX as f64); // [0, 1)
452        values.push(format!("{}", min_v + range * unit));
453    }
454    format!("[{}]", values.join(","))
455}
456
457// =================================================================
458// Array inspection: operate on bracket-encoded arrays like [1,2,3]
459// =================================================================
460
461/// Return the number of elements in a bracket-encoded array string.
462///
463/// Parses `[a,b,c,...]` and counts elements. Returns 0 for empty
464/// arrays or non-array input.
465#[crate::polydat_node(category = Json)]
466fn array_len(input: &str) -> u64 {
467    let trimmed = input.trim();
468    if trimmed == "[]" || trimmed.is_empty() {
469        0
470    } else if trimmed.starts_with('[') && trimmed.ends_with(']') {
471        let inner = &trimmed[1..trimmed.len() - 1];
472        inner.split(',').count() as u64
473    } else {
474        0
475    }
476}
477
478/// Return the element at a given index from a bracket-encoded array.
479///
480/// `array_at(array_str, index)` → string element at position.
481/// Index wraps modulo array length. Returns "" for empty arrays.
482#[crate::polydat_node(category = Json)]
483fn array_at(array: &str, index: u64) -> String {
484    let trimmed = array.trim();
485    if trimmed.starts_with('[') && trimmed.ends_with(']') {
486        let inner = &trimmed[1..trimmed.len() - 1];
487        let elements: Vec<&str> = inner.split(',').map(|e| e.trim()).collect();
488        if elements.is_empty() || (elements.len() == 1 && elements[0].is_empty()) {
489            String::new()
490        } else {
491            elements[(index as usize) % elements.len()].to_string()
492        }
493    } else {
494        String::new()
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501    use crate::ast::{PolydatNode, PortType, Value};
502
503    #[test]
504    fn body_column_i32_extracts_array_of_rows() {
505        // Standard CQL SELECT shape: top-level array of row objects.
506        let body = Value::Json(std::sync::Arc::new(serde_json::json!([
507            { "key": 4, "value": 0.5 },
508            { "key": 17, "value": 0.4 },
509            { "key": 42, "value": 0.3 },
510        ])));
511        let node = BodyColumnI32::new(PortType::Json, "key".to_string());
512        let mut out = [Value::None];
513        node.eval(&[body], &mut out);
514        let Value::VecI32(slice) = &out[0] else {
515            panic!("expected VecI32, got {:?}", out[0])
516        };
517        assert_eq!(slice.as_slice(), &[4, 17, 42]);
518    }
519
520    #[test]
521    fn body_column_i32_extracts_envelope_rows() {
522        // Envelope shape: { "rows": [...] }.
523        let body = Value::Json(std::sync::Arc::new(serde_json::json!({
524            "rows": [
525                { "id": 1 },
526                { "id": 7 },
527                { "id": 13 },
528            ],
529            "metadata": "ignored",
530        })));
531        let node = BodyColumnI32::new(PortType::Json, "id".to_string());
532        let mut out = [Value::None];
533        node.eval(&[body], &mut out);
534        let Value::VecI32(slice) = &out[0] else {
535            panic!("expected VecI32")
536        };
537        assert_eq!(slice.as_slice(), &[1, 7, 13]);
538    }
539
540    #[test]
541    fn body_column_i32_skips_rows_missing_column() {
542        // Robustness: rows without the column don't zero-fill.
543        let body = Value::Json(std::sync::Arc::new(serde_json::json!([
544            { "key": 1 },
545            { "other": 2 },     // skipped
546            { "key": "not_an_int" },  // skipped
547            { "key": 3 },
548        ])));
549        let node = BodyColumnI32::new(PortType::Json, "key".to_string());
550        let mut out = [Value::None];
551        node.eval(&[body], &mut out);
552        let Value::VecI32(slice) = &out[0] else {
553            panic!("expected VecI32")
554        };
555        assert_eq!(slice.as_slice(), &[1, 3]);
556    }
557
558    #[test]
559    fn body_column_i32_string_numeric_parses() {
560        // Stringified numbers parse — common for some adapters
561        // that don't preserve native numeric typing.
562        let body = Value::Json(std::sync::Arc::new(serde_json::json!([
563            { "key": "42" },
564            { "key": "-7" },
565        ])));
566        let node = BodyColumnI32::new(PortType::Json, "key".to_string());
567        let mut out = [Value::None];
568        node.eval(&[body], &mut out);
569        let Value::VecI32(slice) = &out[0] else {
570            panic!("expected VecI32")
571        };
572        assert_eq!(slice.as_slice(), &[42, -7]);
573    }
574
575    #[test]
576    fn body_column_i32_empty_body_produces_empty_vec() {
577        let body = Value::Json(std::sync::Arc::new(serde_json::json!([])));
578        let node = BodyColumnI32::new(PortType::Json, "key".to_string());
579        let mut out = [Value::None];
580        node.eval(&[body], &mut out);
581        let Value::VecI32(slice) = &out[0] else {
582            panic!("expected VecI32")
583        };
584        assert!(slice.as_slice().is_empty());
585    }
586
587    #[test]
588    fn body_column_i32_non_json_input_produces_empty_vec() {
589        // Defensive: non-JSON input shouldn't panic; the node's
590        // type contract still holds. The evaluator surfaces a
591        // "no values extracted" diagnostic downstream.
592        let node = BodyColumnI32::new(PortType::Json, "key".to_string());
593        let mut out = [Value::None];
594        node.eval(&[Value::Str("not json".into())], &mut out);
595        let Value::VecI32(slice) = &out[0] else {
596            panic!("expected VecI32")
597        };
598        assert!(slice.as_slice().is_empty());
599    }
600
601    /// `json_text` flattens a multi-row describe-keyspace body
602    /// to newline-joined leaves. The actual newlines INSIDE
603    /// `create_statement` strings survive verbatim, so a
604    /// line-anchored regex can match the table-declaration
605    /// line. This is the workload-of-record probe shape:
606    /// `regex_match(json_text(body), "(?im)^…TABLE foo\(…")`.
607    #[test]
608    fn json_text_flattens_multirow_describe_for_regex() {
609        let body = Value::Json(std::sync::Arc::new(serde_json::json!([
610            {
611                "keyspace_name": "system_views",
612                "type": "table",
613                "name": "sai_column_indexes",
614                "create_statement": "CREATE TABLE system_views.sai_column_indexes (\n    keyspace_name text,\n    table_name text\n);"
615            },
616            {
617                "keyspace_name": "system_views",
618                "type": "table",
619                "name": "indexes",
620                "create_statement": "CREATE VIRTUAL TABLE system_views.indexes (\n    keyspace_name text\n);"
621            },
622        ])));
623
624        let node = JsonText::new(PortType::Json);
625        let mut out = [Value::None];
626        node.eval(&[body], &mut out);
627
628        let text = match &out[0] {
629            Value::Str(s) => s.clone(),
630            other => panic!("expected Str, got {other:?}"),
631        };
632
633        // The actual schema-text newlines are intact (not the
634        // `\n` literal escape sequences a JSON-stringification
635        // would produce).
636        assert!(text.contains("CREATE TABLE system_views.sai_column_indexes (\n"));
637        assert!(text.contains("CREATE VIRTUAL TABLE system_views.indexes (\n"));
638
639        // The workload's intended regex (with the CREATE-prefix
640        // fix) matches the flattened text.
641        let pat = regex::Regex::new(
642            r"(?im)^\s*(?:CREATE\s+)?(?:VIRTUAL\s+)?TABLE\s+system_views\.sai_column_indexes\s*\(",
643        )
644        .unwrap();
645        assert!(
646            pat.is_match(&text),
647            "regex should match the flattened schema text"
648        );
649    }
650
651    /// Helper: drive `json_with(key, value)` through one eval and
652    /// return the resulting `Value::Json` partial-object. The
653    /// `value_pt` is the wire-type the test wants attached to the
654    /// PolyWire input port (`json_with` accepts any Value variant).
655    fn jw(key: &str, value_pt: PortType, value: Value) -> Value {
656        let node = JsonWith::new(key.to_string(), value_pt);
657        let mut out = [Value::None];
658        node.eval(&[value], &mut out);
659        std::mem::replace(&mut out[0], Value::None)
660    }
661
662    #[test]
663    fn json_object_basic() {
664        // Compositional form: three json_with parts merged.
665        let name = jw("name", PortType::Str, Value::Str("Alice".into()));
666        let age = jw("age", PortType::U64, Value::U64(30));
667        let active = jw("active", PortType::Bool, Value::Bool(true));
668
669        let node = JsonObject::new(3);
670        let mut out = [Value::None];
671        node.eval(&[name, age, active], &mut out);
672        let j = out[0].as_json();
673        assert_eq!(j["name"], "Alice");
674        assert_eq!(j["age"], 30);
675        assert_eq!(j["active"], true);
676    }
677
678    #[test]
679    fn json_object_nested() {
680        // Inner object: { x: 10, y: 20 }.
681        let inner_x = jw("x", PortType::U64, Value::U64(10));
682        let inner_y = jw("y", PortType::U64, Value::U64(20));
683        let inner_node = JsonObject::new(2);
684        let mut inner_out = [Value::None];
685        inner_node.eval(&[inner_x, inner_y], &mut inner_out);
686
687        // Outer: { point: <inner> }.
688        let point = jw(
689            "point",
690            PortType::Json,
691            std::mem::replace(&mut inner_out[0], Value::None),
692        );
693        let outer = JsonObject::new(1);
694        let mut out = [Value::None];
695        outer.eval(&[point], &mut out);
696        let j = out[0].as_json();
697        assert_eq!(j["point"]["x"], 10);
698        assert_eq!(j["point"]["y"], 20);
699    }
700
701    #[test]
702    fn json_object_later_part_wins_on_collision() {
703        // Documents the merge-order semantic: later json_with shadows
704        // earlier, mirroring how json_merge treats right-wins.
705        let first = jw("k", PortType::U64, Value::U64(1));
706        let second = jw("k", PortType::U64, Value::U64(2));
707        let node = JsonObject::new(2);
708        let mut out = [Value::None];
709        node.eval(&[first, second], &mut out);
710        assert_eq!(out[0].as_json()["k"], 2);
711    }
712
713    #[test]
714    fn json_object_skips_non_json_parts() {
715        // Composability with conditional branches that may yield
716        // non-Json (e.g. `if(cond, json_with(...), null)`).
717        let valid = jw("k", PortType::U64, Value::U64(1));
718        let node = JsonObject::new(2);
719        let mut out = [Value::None];
720        node.eval(&[valid, Value::None], &mut out);
721        let j = out[0].as_json();
722        assert_eq!(j["k"], 1);
723        assert_eq!(j.as_object().unwrap().len(), 1);
724    }
725
726    #[test]
727    fn json_array_basic() {
728        let node = JsonArray::new(3);
729        let mut out = [Value::None];
730        node.eval(
731            &[Value::U64(1), Value::Str("two".into()), Value::F64(3.0)],
732            &mut out,
733        );
734        let j = out[0].as_json();
735        let arr = j.as_array().unwrap();
736        assert_eq!(arr.len(), 3);
737        assert_eq!(arr[0], 1);
738        assert_eq!(arr[1], "two");
739        assert_eq!(arr[2], 3.0);
740    }
741
742    #[test]
743    fn json_to_str_compact() {
744        let node = JsonToStr::new();
745        let mut out = [Value::None];
746        let input = Value::Json(std::sync::Arc::new(json!({"a": 1, "b": "hello"})));
747        node.eval(&[input], &mut out);
748        let s = out[0].as_str();
749        assert!(s.contains("\"a\":1") || s.contains("\"a\": 1"));
750        assert!(s.contains("\"b\":\"hello\"") || s.contains("\"b\": \"hello\""));
751    }
752
753    #[test]
754    fn str_to_json_roundtrip() {
755        let to_str = JsonToStr::new();
756        let from_str = StrToJson::default();
757        let original = Value::Json(std::sync::Arc::new(json!({"key": [1, 2, 3]})));
758        let mut mid = [Value::None];
759        let mut out = [Value::None];
760        to_str.eval(std::slice::from_ref(&original), &mut mid);
761        from_str.eval(&[mid[0].clone()], &mut out);
762        assert_eq!(out[0].as_json(), original.as_json());
763    }
764
765    #[test]
766    fn escape_json_basic() {
767        let node = EscapeJson::new();
768        let mut out = [Value::None];
769        node.eval(&[Value::Str("hello \"world\"\nline2".into())], &mut out);
770        let s = out[0].as_str();
771        assert!(s.contains("\\\""));
772        assert!(s.contains("\\n"));
773        assert!(!s.starts_with('"'));
774    }
775
776    #[test]
777    fn json_merge_basic() {
778        let node = JsonMerge::new();
779        let mut out = [Value::None];
780        let left = Value::Json(std::sync::Arc::new(json!({"a": 1, "b": 2})));
781        let right = Value::Json(std::sync::Arc::new(json!({"b": 99, "c": 3})));
782        node.eval(&[left, right], &mut out);
783        let j = out[0].as_json();
784        assert_eq!(j["a"], 1);
785        assert_eq!(j["b"], 99); // right wins
786        assert_eq!(j["c"], 3);
787    }
788
789    #[test]
790    fn json_field_basic() {
791        let node = JsonField::new("name".to_string());
792        let mut out = [Value::None];
793        node.eval(
794            &[Value::Json(std::sync::Arc::new(
795                json!({"name": "Alice", "age": 30}),
796            ))],
797            &mut out,
798        );
799        assert_eq!(out[0].as_json(), &json!("Alice"));
800    }
801
802    #[test]
803    fn json_field_missing() {
804        let node = JsonField::new("missing".to_string());
805        let mut out = [Value::None];
806        node.eval(
807            &[Value::Json(std::sync::Arc::new(json!({"name": "Alice"})))],
808            &mut out,
809        );
810        assert!(out[0].as_json().is_null());
811    }
812
813    #[test]
814    fn to_json_from_u64() {
815        let node = ToJson::new(PortType::U64);
816        let mut out = [Value::None];
817        node.eval(&[Value::U64(42)], &mut out);
818        assert_eq!(out[0].as_json(), &json!(42));
819    }
820
821    #[test]
822    fn json_pretty_print() {
823        let node = JsonToStrPretty::default();
824        let mut out = [Value::None];
825        node.eval(
826            &[Value::Json(std::sync::Arc::new(json!({"a": 1})))],
827            &mut out,
828        );
829        let s = out[0].as_str();
830        assert!(s.contains('\n'), "pretty print should have newlines");
831    }
832}