Skip to main content

oxilite_core/
schema.rs

1//! Database schema.
2//!
3// @lat: [[architecture#Storage schema]]
4
5use crate::encoding::{Tag, INT_OFFSET, PAYLOAD_BITS};
6use crate::sql::{Request, Statement};
7
8/// Current schema version stored in `oxilite_meta`.
9pub const SCHEMA_VERSION: &str = "1";
10
11/// Options chosen when a store is created.
12#[derive(Debug, Clone, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "serde", serde(default, rename_all = "camelCase"))]
15pub struct StoreOptions {
16    /// Create the optional `quads_gspo` index (fast `GRAPH <g> { ?s ?p ?o }`, `CLEAR GRAPH`).
17    pub graph_index: bool,
18    /// Create the full-text index over string literals (FTS5, see [`crate::text`]).
19    pub text_index: bool,
20}
21
22impl Default for StoreOptions {
23    fn default() -> Self {
24        Self {
25            graph_index: true,
26            text_index: false,
27        }
28    }
29}
30
31/// The schema as one SQL script (for `wrangler d1 migrations`).
32pub fn schema_sql(options: &StoreOptions) -> String {
33    schema_sql_with(options, &[])
34}
35
36/// The schema plus extra DDL of optional modules (e.g. `oxilite-jsonld`'s tables).
37pub fn schema_sql_with(options: &StoreOptions, extra: &[Statement]) -> String {
38    let mut out =
39        String::from("-- oxilite schema (generated by oxilite_core::schema::schema_sql)\n");
40    for s in create_schema(options).statements.iter().chain(extra) {
41        out.push_str(&s.sql);
42        out.push_str(";\n");
43    }
44    out
45}
46
47/// DDL statements creating (idempotently) the oxilite schema.
48pub fn create_schema(options: &StoreOptions) -> Request {
49    let mut s = vec![
50        "CREATE TABLE IF NOT EXISTS oxilite_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT",
51        // Hashed terms. `id` is the rowid alias: the fastest possible key.
52        "CREATE TABLE IF NOT EXISTS terms (\
53            id INTEGER PRIMARY KEY, \
54            lex TEXT NOT NULL, \
55            dt TEXT, \
56            lang TEXT, \
57            dir INTEGER, \
58            num REAL, \
59            nt INTEGER, \
60            ts REAL) STRICT",
61        "CREATE INDEX IF NOT EXISTS terms_num ON terms(num) WHERE num IS NOT NULL",
62        "CREATE INDEX IF NOT EXISTS terms_ts ON terms(ts) WHERE ts IS NOT NULL",
63        // Detects xxh3 collisions atomically: aborts the whole batch.
64        "CREATE TRIGGER IF NOT EXISTS terms_collision BEFORE INSERT ON terms \
65         WHEN EXISTS (SELECT 1 FROM terms t WHERE t.id = NEW.id AND \
66            (t.lex IS NOT NEW.lex OR t.dt IS NOT NEW.dt OR t.lang IS NOT NEW.lang OR t.dir IS NOT NEW.dir)) \
67         BEGIN SELECT RAISE(ABORT, 'oxilite: term hash collision'); END",
68        "CREATE TABLE IF NOT EXISTS triple_terms (\
69            id INTEGER PRIMARY KEY, s INTEGER NOT NULL, p INTEGER NOT NULL, o INTEGER NOT NULL, vk TEXT NOT NULL, sk TEXT NOT NULL) STRICT",
70        // The quad table is its own clustered SPOG index; secondary indexes contain every
71        // column, so every triple-pattern scan is index-only.
72        "CREATE TABLE IF NOT EXISTS quads (\
73            s INTEGER NOT NULL, p INTEGER NOT NULL, o INTEGER NOT NULL, g INTEGER NOT NULL DEFAULT 0, \
74            PRIMARY KEY (s, p, o, g)) WITHOUT ROWID, STRICT",
75        "CREATE INDEX IF NOT EXISTS quads_posg ON quads(p, o, s, g)",
76        "CREATE INDEX IF NOT EXISTS quads_ospg ON quads(o, s, p, g)",
77        "CREATE TABLE IF NOT EXISTS graphs (id INTEGER PRIMARY KEY) STRICT",
78        "CREATE TABLE IF NOT EXISTS stats_pred (\
79            p INTEGER PRIMARY KEY, triples INTEGER NOT NULL, distinct_s INTEGER NOT NULL, distinct_o INTEGER NOT NULL) STRICT",
80        "CREATE TABLE IF NOT EXISTS stats_class (o INTEGER PRIMARY KEY, instances INTEGER NOT NULL) STRICT",
81        // Frequent (predicate, object) pairs of low-cardinality predicates (planner skew).
82        "CREATE TABLE IF NOT EXISTS stats_po (p INTEGER NOT NULL, o INTEGER NOT NULL, n INTEGER NOT NULL, PRIMARY KEY (p, o)) WITHOUT ROWID, STRICT",
83        // Reasoning: the schema closure (see `reason::closure_statements`) and materialized
84        // OWL 2 RL inferences, kept apart from asserted quads.
85        "CREATE TABLE IF NOT EXISTS tbox_closure (\
86            kind INTEGER NOT NULL, sub INTEGER NOT NULL, sup INTEGER NOT NULL, PRIMARY KEY (kind, sup, sub)) WITHOUT ROWID, STRICT",
87        "CREATE INDEX IF NOT EXISTS tbox_closure_sub ON tbox_closure(kind, sub, sup)",
88        "CREATE TABLE IF NOT EXISTS quads_inf (\
89            s INTEGER NOT NULL, p INTEGER NOT NULL, o INTEGER NOT NULL, g INTEGER NOT NULL DEFAULT 0, \
90            PRIMARY KEY (s, p, o, g)) WITHOUT ROWID, STRICT",
91        "CREATE INDEX IF NOT EXISTS quads_inf_posg ON quads_inf(p, o, s, g)",
92        "CREATE INDEX IF NOT EXISTS quads_inf_ospg ON quads_inf(o, s, p, g)",
93        // Which producer (OWL 2 RL, a named rule set) derived each inference, so one producer
94        // can be recomputed without discarding the others' conclusions (see `reason`).
95        "CREATE TABLE IF NOT EXISTS quads_inf_src (\
96            src INTEGER NOT NULL, s INTEGER NOT NULL, p INTEGER NOT NULL, o INTEGER NOT NULL, g INTEGER NOT NULL DEFAULT 0, \
97            PRIMARY KEY (s, p, o, g, src)) WITHOUT ROWID, STRICT",
98        "CREATE TABLE IF NOT EXISTS inf_producers (id INTEGER PRIMARY KEY, name TEXT NOT NULL) STRICT",
99        // Schema registry: which named graphs hold an ontology, SHACL shapes or a ShEx schema
100        // (see `registry`). The RDF itself stays in `quads`; this only labels a graph.
101        "CREATE TABLE IF NOT EXISTS schema_graphs (\
102            g INTEGER PRIMARY KEY, role INTEGER NOT NULL, iri TEXT, version TEXT, sha256 TEXT, \
103            imports TEXT, active INTEGER NOT NULL DEFAULT 1, loaded_at REAL NOT NULL) STRICT",
104        "CREATE INDEX IF NOT EXISTS schema_graphs_role ON schema_graphs(role, active)",
105        // The compiled SHACL property shapes of the registered shapes graphs, and the values of
106        // their `sh:in` lists (see `shapes`). Both are caches, rebuilt from `quads`.
107        "CREATE TABLE IF NOT EXISTS shapes_index (\
108            target TEXT NOT NULL, path TEXT NOT NULL, datatype TEXT, min_count INTEGER, \
109            max_count INTEGER, pattern TEXT, relationship INTEGER NOT NULL DEFAULT 0, \
110            PRIMARY KEY (target, path)) WITHOUT ROWID, STRICT",
111        "CREATE TABLE IF NOT EXISTS shapes_in (\
112            target TEXT NOT NULL, path TEXT NOT NULL, id INTEGER NOT NULL, \
113            lex TEXT, dt TEXT, lang TEXT, dir INTEGER, \
114            PRIMARY KEY (target, path, id)) WITHOUT ROWID, STRICT",
115        // Work table for Datalog components that need iteration (non-linear recursion).
116        // Rows are term ids, padded to a fixed width so one table serves every arity; `run`
117        // scopes an evaluation, so concurrent programs do not see each other and cleanup is
118        // exact. Unused columns default to 0 because a WITHOUT ROWID primary key is NOT NULL.
119        "CREATE TABLE IF NOT EXISTS datalog_work (\
120            run INTEGER NOT NULL, rel INTEGER NOT NULL, \
121            c0 INTEGER NOT NULL DEFAULT 0, c1 INTEGER NOT NULL DEFAULT 0, \
122            c2 INTEGER NOT NULL DEFAULT 0, c3 INTEGER NOT NULL DEFAULT 0, \
123            c4 INTEGER NOT NULL DEFAULT 0, c5 INTEGER NOT NULL DEFAULT 0, \
124            PRIMARY KEY (run, rel, c0, c1, c2, c3, c4, c5)) WITHOUT ROWID, STRICT",
125        // Staging table for SPARQL UPDATE (DELETE/INSERT … WHERE) inside one atomic batch.
126        "CREATE TABLE IF NOT EXISTS update_buffer (\
127            op INTEGER NOT NULL, s INTEGER NOT NULL, p INTEGER NOT NULL, o INTEGER NOT NULL, g INTEGER NOT NULL) STRICT",
128        // Assertions inside atomic batches: inserting a non-NULL value aborts the batch with a
129        // "CHECK constraint failed: <name>" error naming the violated SPARQL condition.
130        "CREATE TABLE IF NOT EXISTS oxilite_guard (\
131            graph_does_not_exist INTEGER CHECK (graph_does_not_exist IS NULL), \
132            graph_already_exists INTEGER CHECK (graph_already_exists IS NULL), \
133            computed_value_not_storable INTEGER CHECK (computed_value_not_storable IS NULL)) STRICT",
134    ]
135    .into_iter()
136    .map(Statement::from)
137    .collect::<Vec<_>>();
138    if options.graph_index {
139        s.push("CREATE INDEX IF NOT EXISTS quads_gspo ON quads(g, s, p, o)".into());
140    }
141    if options.text_index {
142        s.extend(crate::text::schema_statements());
143    }
144    s.push(
145        format!(
146            "INSERT OR IGNORE INTO oxilite_meta(key, value) VALUES ('schema_version', '{SCHEMA_VERSION}'), ('graph_index', '{}'), ('int_offset', '{INT_OFFSET}'), ('payload_bits', '{PAYLOAD_BITS}'), ('integer_tag', '{}')",
147            u8::from(options.graph_index),
148            Tag::Integer as u8
149        )
150        .into(),
151    );
152    Request::atomic(s)
153}