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