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`.
9/// Version 2 keeps the schema registry as RDF in `<oxilite:schema>` (no `schema_graphs`
10/// table) and scopes `tbox_closure`; opening a version 1 store migrates it (`ops::open_job`).
11pub const SCHEMA_VERSION: &str = "2";
12
13/// The TBox closure cache, one closure per scope (see `reason::closure_statements`).
14pub const TBOX_TABLE: &str = "CREATE TABLE IF NOT EXISTS tbox_closure (\
15    kind INTEGER NOT NULL, scope INTEGER NOT NULL, sub INTEGER NOT NULL, sup INTEGER NOT NULL, \
16    PRIMARY KEY (kind, scope, sup, sub)) WITHOUT ROWID, STRICT";
17pub const TBOX_INDEX: &str =
18    "CREATE INDEX IF NOT EXISTS tbox_closure_sub ON tbox_closure(kind, scope, sub, sup)";
19
20/// Options chosen when a store is created.
21#[derive(Debug, Clone, PartialEq, Eq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23#[cfg_attr(feature = "serde", serde(default, rename_all = "camelCase"))]
24pub struct StoreOptions {
25    /// Create the optional `quads_gspo` index (fast `GRAPH <g> { ?s ?p ?o }`, `CLEAR GRAPH`).
26    pub graph_index: bool,
27    /// Create the full-text index over string literals (FTS5, see [`crate::text`]).
28    pub text_index: bool,
29    /// How much history the store keeps (see [`crate::version`]); `Off` by default. Applied when
30    /// the store is created; an existing store changes level only through an explicit level
31    /// change.
32    pub versioning: crate::version::Versioning,
33    /// With `stamped` or `log`: index `quads.t` (fast "added since" queries, one more row
34    /// written per quad).
35    pub stamp_index: bool,
36    /// With `log`: index the change log by predicate and object too (fast as-of queries on
37    /// any pattern, two more rows written per change).
38    pub as_of_index: bool,
39    /// Install the system graphs in a blank store: the oxilite vocabulary in
40    /// `<oxilite:vocabulary>` and the registry's own description in `<oxilite:schema>` (see
41    /// `registry::system_quads`). Off by default, so a new store is empty as in Oxigraph.
42    pub system_graphs: bool,
43}
44
45impl Default for StoreOptions {
46    fn default() -> Self {
47        Self {
48            graph_index: true,
49            text_index: false,
50            versioning: crate::version::Versioning::Off,
51            stamp_index: false,
52            as_of_index: false,
53            system_graphs: false,
54        }
55    }
56}
57
58impl StoreOptions {
59    /// The level change that creates this store's versioning.
60    pub fn level_change(&self) -> crate::version::LevelChange {
61        crate::version::LevelChange {
62            as_of_index: self.as_of_index.then_some(true),
63            stamp_index: self.stamp_index.then_some(true),
64            ..Default::default()
65        }
66    }
67}
68
69/// The schema as one SQL script (for `wrangler d1 migrations`).
70pub fn schema_sql(options: &StoreOptions) -> String {
71    schema_sql_with(options, &[])
72}
73
74/// The schema plus extra DDL of optional modules (e.g. `oxilite-jsonld`'s tables).
75pub fn schema_sql_with(options: &StoreOptions, extra: &[Statement]) -> String {
76    let mut out =
77        String::from("-- oxilite schema (generated by oxilite_core::schema::schema_sql)\n");
78    for s in create_schema(options).statements.iter().chain(extra) {
79        out.push_str(&s.sql);
80        out.push_str(";\n");
81    }
82    out
83}
84
85/// DDL creating the oxilite schema, versioning included: a script for a new database (a D1
86/// migration). Not idempotent when versioning is on (`ALTER TABLE`); stores opened in place use
87/// [`base_schema`] and apply their level through `version::change_statements`.
88pub fn create_schema(options: &StoreOptions) -> Request {
89    let mut r = base_schema(options);
90    if options.versioning > crate::version::Versioning::Off {
91        let change = crate::version::change_statements(
92            &crate::version::VersionState::default(),
93            options.versioning,
94            &options.level_change(),
95        )
96        .expect("versioning from off is always possible");
97        r.statements.extend(change);
98    }
99    if options.system_graphs {
100        r.statements
101            .extend(system_graph_statements(&crate::sql::Capabilities::d1()));
102    }
103    r
104}
105
106/// Statements writing the system graphs (see `StoreOptions::system_graphs`) and rebuilding the
107/// schema caches they scope.
108pub fn system_graph_statements(caps: &crate::sql::Capabilities) -> Vec<Statement> {
109    let quads = crate::registry::system_quads();
110    let mut s = crate::writer::EncodedQuads::new(quads.iter().map(oxrdf::Quad::as_ref))
111        .insert_statements(caps);
112    s.extend(crate::reason::closure_statements());
113    s.extend(crate::shapes::refresh_statements());
114    s
115}
116
117/// DDL statements creating (idempotently) the oxilite schema without versioning.
118pub fn base_schema(options: &StoreOptions) -> Request {
119    let mut s = vec![
120        "CREATE TABLE IF NOT EXISTS oxilite_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT",
121        // Hashed terms. `id` is the rowid alias: the fastest possible key.
122        "CREATE TABLE IF NOT EXISTS terms (\
123            id INTEGER PRIMARY KEY, \
124            lex TEXT NOT NULL, \
125            dt TEXT, \
126            lang TEXT, \
127            dir INTEGER, \
128            num REAL, \
129            nt INTEGER, \
130            ts REAL) STRICT",
131        "CREATE INDEX IF NOT EXISTS terms_num ON terms(num) WHERE num IS NOT NULL",
132        "CREATE INDEX IF NOT EXISTS terms_ts ON terms(ts) WHERE ts IS NOT NULL",
133        // Detects xxh3 collisions atomically: aborts the whole batch.
134        "CREATE TRIGGER IF NOT EXISTS terms_collision BEFORE INSERT ON terms \
135         WHEN EXISTS (SELECT 1 FROM terms t WHERE t.id = NEW.id AND \
136            (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)) \
137         BEGIN SELECT RAISE(ABORT, 'oxilite: term hash collision'); END",
138        "CREATE TABLE IF NOT EXISTS triple_terms (\
139            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",
140        // The quad table is its own clustered SPOG index; secondary indexes contain every
141        // column, so every triple-pattern scan is index-only.
142        "CREATE TABLE IF NOT EXISTS quads (\
143            s INTEGER NOT NULL, p INTEGER NOT NULL, o INTEGER NOT NULL, g INTEGER NOT NULL DEFAULT 0, \
144            PRIMARY KEY (s, p, o, g)) WITHOUT ROWID, STRICT",
145        "CREATE INDEX IF NOT EXISTS quads_posg ON quads(p, o, s, g)",
146        "CREATE INDEX IF NOT EXISTS quads_ospg ON quads(o, s, p, g)",
147        "CREATE TABLE IF NOT EXISTS graphs (id INTEGER PRIMARY KEY) STRICT",
148        "CREATE TABLE IF NOT EXISTS stats_pred (\
149            p INTEGER PRIMARY KEY, triples INTEGER NOT NULL, distinct_s INTEGER NOT NULL, distinct_o INTEGER NOT NULL) STRICT",
150        "CREATE TABLE IF NOT EXISTS stats_class (o INTEGER PRIMARY KEY, instances INTEGER NOT NULL) STRICT",
151        // Frequent (predicate, object) pairs of low-cardinality predicates (planner skew).
152        "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",
153        // Reasoning: the schema closure (see `reason::closure_statements`) and materialized
154        // OWL 2 RL inferences, kept apart from asserted quads.
155        // `scope`: the graph a closure applies to, or the id of `oxl:AllGraphs` (see `registry`).
156        TBOX_TABLE,
157        TBOX_INDEX,
158        "CREATE TABLE IF NOT EXISTS quads_inf (\
159            s INTEGER NOT NULL, p INTEGER NOT NULL, o INTEGER NOT NULL, g INTEGER NOT NULL DEFAULT 0, \
160            PRIMARY KEY (s, p, o, g)) WITHOUT ROWID, STRICT",
161        "CREATE INDEX IF NOT EXISTS quads_inf_posg ON quads_inf(p, o, s, g)",
162        "CREATE INDEX IF NOT EXISTS quads_inf_ospg ON quads_inf(o, s, p, g)",
163        // Which producer (OWL 2 RL, a named rule set) derived each inference, so one producer
164        // can be recomputed without discarding the others' conclusions (see `reason`).
165        "CREATE TABLE IF NOT EXISTS quads_inf_src (\
166            src INTEGER NOT NULL, s INTEGER NOT NULL, p INTEGER NOT NULL, o INTEGER NOT NULL, g INTEGER NOT NULL DEFAULT 0, \
167            PRIMARY KEY (s, p, o, g, src)) WITHOUT ROWID, STRICT",
168        "CREATE TABLE IF NOT EXISTS inf_producers (id INTEGER PRIMARY KEY, name TEXT NOT NULL) STRICT",
169        // The compiled SHACL property shapes of the registered shapes graphs, and the values of
170        // their `sh:in` lists (see `shapes`). Both are caches, rebuilt from `quads`.
171        "CREATE TABLE IF NOT EXISTS shapes_index (\
172            target TEXT NOT NULL, path TEXT NOT NULL, datatype TEXT, min_count INTEGER, \
173            max_count INTEGER, pattern TEXT, relationship INTEGER NOT NULL DEFAULT 0, \
174            PRIMARY KEY (target, path)) WITHOUT ROWID, STRICT",
175        "CREATE TABLE IF NOT EXISTS shapes_in (\
176            target TEXT NOT NULL, path TEXT NOT NULL, id INTEGER NOT NULL, \
177            lex TEXT, dt TEXT, lang TEXT, dir INTEGER, \
178            PRIMARY KEY (target, path, id)) WITHOUT ROWID, STRICT",
179        // Work table for Datalog components that need iteration (non-linear recursion).
180        // Rows are term ids, padded to a fixed width so one table serves every arity; `run`
181        // scopes an evaluation, so concurrent programs do not see each other and cleanup is
182        // exact. Unused columns default to 0 because a WITHOUT ROWID primary key is NOT NULL.
183        "CREATE TABLE IF NOT EXISTS datalog_work (\
184            run INTEGER NOT NULL, rel INTEGER NOT NULL, \
185            c0 INTEGER NOT NULL DEFAULT 0, c1 INTEGER NOT NULL DEFAULT 0, \
186            c2 INTEGER NOT NULL DEFAULT 0, c3 INTEGER NOT NULL DEFAULT 0, \
187            c4 INTEGER NOT NULL DEFAULT 0, c5 INTEGER NOT NULL DEFAULT 0, \
188            PRIMARY KEY (run, rel, c0, c1, c2, c3, c4, c5)) WITHOUT ROWID, STRICT",
189        // Staging table for SPARQL UPDATE (DELETE/INSERT … WHERE) inside one atomic batch.
190        "CREATE TABLE IF NOT EXISTS update_buffer (\
191            op INTEGER NOT NULL, s INTEGER NOT NULL, p INTEGER NOT NULL, o INTEGER NOT NULL, g INTEGER NOT NULL) STRICT",
192        // Assertions inside atomic batches: inserting a non-NULL value aborts the batch with a
193        // "CHECK constraint failed: <name>" error naming the violated SPARQL condition.
194        "CREATE TABLE IF NOT EXISTS oxilite_guard (\
195            graph_does_not_exist INTEGER CHECK (graph_does_not_exist IS NULL), \
196            graph_already_exists INTEGER CHECK (graph_already_exists IS NULL), \
197            computed_value_not_storable INTEGER CHECK (computed_value_not_storable IS NULL)) STRICT",
198    ]
199    .into_iter()
200    .map(Statement::from)
201    .collect::<Vec<_>>();
202    if options.graph_index {
203        s.push("CREATE INDEX IF NOT EXISTS quads_gspo ON quads(g, s, p, o)".into());
204    }
205    if options.text_index {
206        s.extend(crate::text::schema_statements());
207    }
208    s.push(
209        format!(
210            "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', '{}')",
211            u8::from(options.graph_index),
212            Tag::Integer as u8
213        )
214        .into(),
215    );
216    Request::atomic(s)
217}