Skip to main content

golden_sql/tests/golden/
dump.rs

1//! The SQL an unversioned store generates for a corpus of queries and updates, one hash per
2//! file and backend. Shared by the golden test and the `golden_sql` example, which writes the
3//! baseline from the code before versioning existed.
4
5use oxilite_core::query::compile_query;
6use oxilite_core::update::{explain_plan, plan_update};
7use oxilite_core::writer::EncodedQuads;
8use oxilite_core::{Capabilities, QueryOptions, Stats};
9use spargebra::SparqlParser;
10use std::path::Path;
11
12fn hash(s: &str) -> String {
13    format!(
14        "{:016x}",
15        xxhash_rust::xxh3::xxh3_64(normalize(s).as_bytes())
16    )
17}
18
19/// Renames the parser's random blank node labels (`_:` and 16 or more hex digits, in the planner's
20/// notes) to `_:b0`, `_:b1`… in order of appearance, so the rendering is deterministic.
21fn normalize(s: &str) -> String {
22    let mut out = String::with_capacity(s.len());
23    let mut seen: Vec<&str> = Vec::new();
24    let mut rest = s;
25    while let Some(i) = rest.find("_:") {
26        out.push_str(&rest[..i]);
27        let tail = &rest[i + 2..];
28        let n = tail.bytes().take_while(u8::is_ascii_hexdigit).count();
29        if n >= 16 {
30            let label = &tail[..n];
31            let k = seen.iter().position(|l| *l == label).unwrap_or_else(|| {
32                seen.push(label);
33                seen.len() - 1
34            });
35            out.push_str(&format!("_:b{k}"));
36            rest = &tail[n..];
37        } else {
38            out.push_str("_:");
39            rest = tail;
40        }
41    }
42    out.push_str(rest);
43    out
44}
45
46fn files(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
47    let Ok(entries) = std::fs::read_dir(dir) else {
48        return;
49    };
50    for e in entries.flatten() {
51        let p = e.path();
52        if p.is_dir() {
53            files(&p, out);
54        } else if matches!(p.extension().and_then(|x| x.to_str()), Some("rq" | "ru")) {
55            out.push(p);
56        }
57    }
58}
59
60/// `path \t backend \t hash` lines, sorted, for every `.rq` / `.ru` file under `root`, plus
61/// the write statements of a fixed set of quads.
62pub fn dump(root: &Path) -> Vec<String> {
63    let backends = [
64        ("native", Capabilities::native()),
65        ("d1", Capabilities::d1()),
66    ];
67    let mut paths = Vec::new();
68    for sub in ["sparql10", "sparql11", "sparql12"] {
69        files(&root.join(sub), &mut paths);
70    }
71    paths.sort();
72    let mut out = Vec::new();
73    let options = QueryOptions::default();
74    let stats = Stats::default();
75    for p in &paths {
76        let Ok(text) = std::fs::read_to_string(p) else {
77            continue;
78        };
79        // How `\u` escapes parse depends on spargebra's `standard-unicode-escaping` feature, which
80        // workspace builds enable (for the W3C runner): leave those files out, so the rendering
81        // depends on the compiler alone.
82        if text.contains("\\u") || text.contains("\\U") {
83            continue;
84        }
85        let rel = p.strip_prefix(root).unwrap().display().to_string();
86        let parser = || {
87            SparqlParser::new()
88                .with_base_iri("http://example.com/base/")
89                .unwrap()
90        };
91        let is_update = p.extension().and_then(|x| x.to_str()) == Some("ru");
92        for (name, caps) in &backends {
93            let render = || -> Option<String> {
94                Some(if is_update {
95                    match plan_update(&parser().parse_update(&text).ok()?, caps) {
96                        Ok(plan) => explain_plan(&plan),
97                        Err(e) => format!("ERR {e}"),
98                    }
99                } else {
100                    match compile_query(&parser().parse_query(&text).ok()?, &stats, caps, &options)
101                    {
102                        Ok(c) => c.explain(),
103                        Err(e) => format!("ERR {e}"),
104                    }
105                })
106            };
107            let Some(first) = render() else { continue };
108            // Updates creating fresh blank nodes embed random ids: record that they vary.
109            let h = if render().map(|r| hash(&r)) == Some(hash(&first)) {
110                hash(&first)
111            } else {
112                "varies".to_owned()
113            };
114            out.push(format!("{rel}\t{name}\t{h}"));
115        }
116    }
117    // The batch writer: inserts and deletes of a fixed set of quads.
118    let ex = |s: String| oxrdf::NamedNode::new_unchecked(format!("http://example.com/{s}"));
119    let quads: Vec<oxrdf::Quad> = (0..700)
120        .map(|i| {
121            oxrdf::Quad::new(
122                ex(format!("s{i}")),
123                ex(format!("p{}", i % 7)),
124                oxrdf::Literal::new_simple_literal(format!("value {i}")),
125                if i % 3 == 0 {
126                    oxrdf::GraphName::DefaultGraph
127                } else {
128                    ex(format!("g{}", i % 5)).into()
129                },
130            )
131        })
132        .collect();
133    let enc = EncodedQuads::new(quads.iter().map(oxrdf::Quad::as_ref));
134    for (name, caps) in &backends {
135        let ins: Vec<String> = enc
136            .insert_statements(caps)
137            .into_iter()
138            .map(|s| s.sql)
139            .collect();
140        let del: Vec<String> = enc
141            .delete_statements(caps)
142            .into_iter()
143            .map(|s| s.sql)
144            .collect();
145        out.push(format!("writer/insert\t{name}\t{}", hash(&ins.join("\n"))));
146        out.push(format!("writer/delete\t{name}\t{}", hash(&del.join("\n"))));
147    }
148    out
149}