Skip to main content

rete_core/
results.rs

1//! Serialize a query result into the playground JSON envelope, written
2//! **directly into a `String`** — no intermediate `serde_json::Value` tree.
3//!
4//! On a large `SELECT` the tree path (`serde_json::Map` per row + a key clone and
5//! a term clone per cell, then a second pass to stringify) allocates ~25× the
6//! payload and costs more than the query itself. Writing the JSON straight into a
7//! buffer cuts the serialization peak heap ~13× and the time ~10× (measured with
8//! `rete-bench --query-mem`). This is the form the WASM `query()` returns across
9//! the worker boundary, so the saved allocation matters most there.
10
11use crate::sparql::QueryOutput;
12
13/// Serialize `out` as `{ "kind": …, … }`. `extra` is a raw JSON fragment of
14/// additional object members appended before the closing brace (e.g.
15/// `,"remote":{…}`); pass `""` for none. `CONSTRUCT` is rendered as a `triples`
16/// array — the text formats (Turtle / JSON-LD) are handled by the caller, which
17/// owns those serializers.
18///
19/// Row object keys are emitted in variable order (the `vars` array), not sorted;
20/// consumers read rows by variable name, so the order is presentational only.
21pub fn results_envelope_json(out: &QueryOutput, extra: &str) -> String {
22    let mut s = String::from("{");
23    match out {
24        QueryOutput::Ask(b) => {
25            s.push_str(r#""kind":"ask","boolean":"#);
26            s.push_str(if *b { "true" } else { "false" });
27        }
28        QueryOutput::Select(project, solutions) => {
29            // Variable order: the projection, else the union of solution keys.
30            let mut vars: Vec<&str> = project.iter().map(String::as_str).collect();
31            if vars.is_empty() {
32                let mut seen = std::collections::BTreeSet::new();
33                for sol in solutions {
34                    for k in sol.keys() {
35                        if seen.insert(k.as_str()) {
36                            vars.push(k.as_str());
37                        }
38                    }
39                }
40            }
41            s.push_str(r#""kind":"select","vars":["#);
42            for (i, v) in vars.iter().enumerate() {
43                if i > 0 {
44                    s.push(',');
45                }
46                push_json_string(&mut s, v);
47            }
48            s.push_str(r#"],"rows":["#);
49            for (i, sol) in solutions.iter().enumerate() {
50                if i > 0 {
51                    s.push(',');
52                }
53                s.push('{');
54                let mut first = true;
55                for var in &vars {
56                    if let Some(term) = sol.get(*var) {
57                        if !first {
58                            s.push(',');
59                        }
60                        first = false;
61                        push_json_string(&mut s, var);
62                        s.push(':');
63                        push_json_string(&mut s, term);
64                    }
65                }
66                s.push('}');
67            }
68            s.push(']');
69        }
70        QueryOutput::Construct(triples) => {
71            s.push_str(r#""kind":"construct","triples":["#);
72            for (i, (a, b, c)) in triples.iter().enumerate() {
73                if i > 0 {
74                    s.push(',');
75                }
76                s.push('[');
77                push_json_string(&mut s, a);
78                s.push(',');
79                push_json_string(&mut s, b);
80                s.push(',');
81                push_json_string(&mut s, c);
82                s.push(']');
83            }
84            s.push(']');
85        }
86    }
87    s.push_str(extra);
88    s.push('}');
89    s
90}
91
92/// Append `v` to `out` as a JSON string literal (RFC 8259 escaping): the
93/// mandatory escapes (`"`, `\`, and C0 controls, the common ones in short form),
94/// every other char — including all UTF-8 — passed through. Matches what
95/// `serde_json` emits for a string by default.
96pub fn push_json_string(out: &mut String, v: &str) {
97    out.push('"');
98    for c in v.chars() {
99        match c {
100            '"' => out.push_str("\\\""),
101            '\\' => out.push_str("\\\\"),
102            '\n' => out.push_str("\\n"),
103            '\r' => out.push_str("\\r"),
104            '\t' => out.push_str("\\t"),
105            '\u{08}' => out.push_str("\\b"),
106            '\u{0C}' => out.push_str("\\f"),
107            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
108            c => out.push(c),
109        }
110    }
111    out.push('"');
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::Binding;
118    use serde_json::{json, Value};
119
120    fn parse(s: &str) -> Value {
121        serde_json::from_str(s).unwrap_or_else(|e| panic!("invalid JSON ({e}): {s}"))
122    }
123
124    fn row(pairs: &[(&str, &str)]) -> Binding {
125        pairs
126            .iter()
127            .map(|(k, v)| (k.to_string(), v.to_string()))
128            .collect()
129    }
130
131    #[test]
132    fn ask_envelope() {
133        assert_eq!(
134            parse(&results_envelope_json(&QueryOutput::Ask(true), "")),
135            json!({"kind":"ask","boolean":true})
136        );
137        assert_eq!(
138            parse(&results_envelope_json(&QueryOutput::Ask(false), "")),
139            json!({"kind":"ask","boolean":false})
140        );
141    }
142
143    #[test]
144    fn select_envelope_matches_reference() {
145        let out = QueryOutput::Select(
146            vec!["s".into(), "o".into()],
147            vec![
148                row(&[("s", "<a>"), ("o", "<b>")]),
149                // a row missing a projected var → that key is simply absent.
150                row(&[("s", "<c>")]),
151            ],
152        );
153        let got = parse(&results_envelope_json(&out, ""));
154        let want = json!({
155            "kind": "select",
156            "vars": ["s", "o"],
157            "rows": [ {"s": "<a>", "o": "<b>"}, {"s": "<c>"} ],
158        });
159        assert_eq!(got, want);
160    }
161
162    #[test]
163    fn select_unprojected_uses_union_of_keys() {
164        let out = QueryOutput::Select(vec![], vec![row(&[("y", "1"), ("x", "2")])]);
165        let got = parse(&results_envelope_json(&out, ""));
166        // vars is the union of solution keys (any order, as a set).
167        let vars: std::collections::BTreeSet<String> = got["vars"]
168            .as_array()
169            .unwrap()
170            .iter()
171            .map(|v| v.as_str().unwrap().to_string())
172            .collect();
173        assert_eq!(
174            vars,
175            ["x".to_string(), "y".to_string()].into_iter().collect()
176        );
177        assert_eq!(got["rows"][0]["x"], json!("2"));
178    }
179
180    #[test]
181    fn escaping_matches_serde_json() {
182        // Terms with quotes, backslashes, controls, and unicode must escape exactly
183        // as serde_json would (the reference).
184        let tricky = "a\"b\\c\nd\te\r\u{08}\u{0c}\u{1}f—🜨";
185        let out = QueryOutput::Select(vec!["v".into()], vec![row(&[("v", tricky)])]);
186        let got = parse(&results_envelope_json(&out, ""));
187        assert_eq!(got["rows"][0]["v"], json!(tricky));
188        // And the raw bytes match serde_json's own string encoding.
189        let mut buf = String::new();
190        push_json_string(&mut buf, tricky);
191        assert_eq!(buf, serde_json::to_string(tricky).unwrap());
192    }
193
194    #[test]
195    fn construct_triples_and_extra_member() {
196        let out = QueryOutput::Construct(vec![("<s>".into(), "<p>".into(), "\"lit\"".into())]);
197        let got = parse(&results_envelope_json(&out, r#","remote":{"bytes":42}"#));
198        assert_eq!(got["kind"], json!("construct"));
199        assert_eq!(got["triples"], json!([["<s>", "<p>", "\"lit\""]]));
200        assert_eq!(got["remote"]["bytes"], json!(42));
201    }
202
203    #[test]
204    fn empty_select_is_valid() {
205        let out = QueryOutput::Select(vec!["x".into()], vec![]);
206        let got = parse(&results_envelope_json(&out, ""));
207        assert_eq!(got, json!({"kind":"select","vars":["x"],"rows":[]}));
208    }
209}