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