@oxilite/d1 0.2.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,6 +23,20 @@ npx oxilite-d1 schema > migrations/0001_oxilite.sql # --no-graph-index
23
23
  npx wrangler d1 migrations apply my-graph --remote
24
24
  ```
25
25
 
26
+ > **Upgrading to 0.3.0 from 0.2.x:** 0.3.0 adds the `schema_graphs`, `shapes_index`,
27
+ > `shapes_in` and `datalog_work` tables. A D1 store is opened with `open_existing`, so no
28
+ > DDL runs per request and an existing database will report `no such table: schema_graphs`
29
+ > until you apply a new migration. Regenerate and apply it:
30
+ >
31
+ > ```bash
32
+ > npx wrangler d1 migrations create my-graph oxilite-0-3-0
33
+ > npx oxilite-d1 schema > migrations/0002_oxilite-0-3-0.sql
34
+ > npx wrangler d1 migrations apply my-graph --remote
35
+ > ```
36
+ >
37
+ > Every statement is `CREATE TABLE IF NOT EXISTS`, so re-applying it is safe and your data
38
+ > is untouched.
39
+
26
40
  ## 2. Query it from a Worker
27
41
 
28
42
  ```ts
@@ -84,13 +98,32 @@ The same store runs on a Durable Object's embedded SQLite through a small adapte
84
98
 
85
99
  ## API
86
100
 
87
- `D1Store` mirrors Oxigraph's JavaScript `Store`, asynchronously: `query`, `queryJson`, `update`, `load`, `bulkLoad`, `dump`, `add`, `delete`, `has`, `match`, `size`, `clear`, plus `cypher`, `explain`, `explainUpdate`, `explainCypher`, `optimize`, `materialize` (OWL 2 RL as SQL rules), `clearInferences`, `jsonld(options)` and `credentials(options)`. Query options add `reasoning: "rdfs" | "owl-ql"` and `include_inferred`; create the store with `textIndex: true` for FTS5 search with `oxl:textMatch`.
101
+ `D1Store` mirrors Oxigraph's JavaScript `Store`, asynchronously: `query`, `queryJson`, `update`, `load`, `bulkLoad`, `dump`, `add`, `delete`, `has`, `match`, `size`, `clear`, plus `cypher`, `explain`, `explainUpdate`, `explainCypher`, `optimize`, `materialize` (OWL 2 RL as SQL rules), `clearInferences`, `jsonld(options)` and `credentials(options)`. The [schema registry](https://github.com/Volland/oxilite/blob/main/docs/schema-registry.md) is there too: `registerSchemaGraph(graph, role, { appliesTo })`, `schemaGraphs()`, `setSchemaGraphActive`, `unregisterSchemaGraph`, `dropSchemaGraph`, `shapeIndex()`; registrations are RDF in `<oxilite:schema>`, written with the same portable SPARQL as on Oxigraph. `D1Store.open(db, { systemGraphs: true })` (or `npx oxilite-d1 schema --system-graphs`) starts a blank database with the `oxl:` vocabulary in `<oxilite:vocabulary>`; `installSystemGraphs()` adds it to an existing one. Query options add `reasoning: "rdfs" | "owl-ql"`, `include_inferred` and `include_schema_graphs`; create the store with `textIndex: true` for FTS5 search with `oxl:textMatch`.
88
102
 
89
103
  | Import | Use |
90
104
  |---|---|
91
105
  | `@oxilite/d1` | Workers: pass the wasm module (`@oxilite/d1/oxilite.wasm`) to `D1Store.open` |
92
106
  | `@oxilite/d1/node` | Node.js, tests and Miniflare: the wasm core loads itself |
93
- | `npx oxilite-d1 schema [--jsonld]` | Print the schema as a D1 migration (with the JSON-LD tables) |
107
+ | `npx oxilite-d1 schema [--jsonld] [--versioning log]` | Print the schema as a D1 migration (with the JSON-LD tables, with versioning) |
108
+ | `npx oxilite-d1 versioning-migration --from off --to log` | Print the migration that changes an existing database's versioning level |
109
+
110
+ ### Versioning
111
+
112
+ Open or create the store with `versioning: "stamped"` (a store clock) or `"log"` (an immutable change log). Every D1 batch is then one commit:
113
+
114
+ ```ts
115
+ const store = await D1Store.open(env.DB, { wasm, versioning: "log" });
116
+ await store.withCommit({ author: "ada", message: "close t1" }, (s) => s.update(closeTicket));
117
+ const before = await store.query(sparql, { as_of: "HEAD~1" }); // or "#42", "@2026-09-01T12:00:00Z"
118
+ const diff = await store.diff("HEAD~1"); // [{ tick, added, quad }]
119
+ const log = await store.history(20); // [{ tick, time, kind, author, message, added, removed }]
120
+ ```
121
+
122
+ `versioning()`, `setVersioning(level, { allowLoss })`, `setCommitInfo`, `changes(after)`, `resolveVersion` and `purge(pattern, reason)` complete the API. `SERVICE <oxilite:version/HEAD~1> { … }` compares versions inside one query, `GRAPH <oxilite:history>` reads commits and changes as RDF, and `cypher(q, {}, { asOf: "HEAD~1" })` matches the past. Measured on D1: `stamped` writes 4.82 rows per triple against 4.81 for a plain store, `log` 6.82 and `log` with `asOfIndex` 8.82. A store keeps its level: change an existing database with a migration.
123
+
124
+ ### Upgrading a 0.4 database
125
+
126
+ 0.5 moves the schema registry into the RDF graph `<oxilite:schema>` and adds a scope column to the reasoning cache. `D1Store.open(env.DB, { wasm })` (without `migrated: true`) upgrades a 0.4 database in one batch: registrations become triples, the `schema_graphs` table goes, and the cache is rebuilt. Do it once (for example from a one-off script or a deploy step), then keep opening with `migrated: true`; `openExisting` on a database that was not upgraded fails with a message saying so. New databases: regenerate `migrations/0001_oxilite.sql` with `npx oxilite-d1 schema`.
94
127
 
95
128
  ## Tips
96
129
 
@@ -1,16 +1,55 @@
1
1
  #!/usr/bin/env node
2
- // Usage: npx oxilite-d1 schema [--no-graph-index] [--jsonld] [--no-metadata-indexes] > migrations/0001_oxilite.sql
2
+ // Usage:
3
+ // npx oxilite-d1 schema [--no-graph-index] [--jsonld] [--no-metadata-indexes]
4
+ // [--versioning off|stamped|log] [--as-of-index] [--stamp-index]
5
+ // [--system-graphs] > migrations/0001_oxilite.sql
6
+ // npx oxilite-d1 versioning-migration --from off --to log [--as-of-index] [--stamp-index]
7
+ // [--allow-loss] [--stamp-column] [--frozen-history] > migrations/0002_versioning.sql
3
8
  import { D1Store } from "../dist/node.js";
4
9
 
5
10
  const [command, ...args] = process.argv.slice(2);
11
+ const value = (name) => {
12
+ const i = args.indexOf(name);
13
+ return i >= 0 ? args[i + 1] : undefined;
14
+ };
15
+ const usage = `usage:
16
+ oxilite-d1 schema [--no-graph-index] [--jsonld] [--no-metadata-indexes] [--versioning off|stamped|log] [--as-of-index] [--stamp-index] [--system-graphs]
17
+ oxilite-d1 versioning-migration --from LEVEL --to LEVEL [--as-of-index] [--stamp-index] [--allow-loss] [--stamp-column] [--frozen-history]`;
18
+
6
19
  if (command === "schema") {
7
20
  const jsonld = args.includes("--jsonld")
8
21
  ? args.includes("--no-metadata-indexes")
9
22
  ? { issuer: false, subject: false, validUntil: false }
10
23
  : true
11
24
  : false;
12
- process.stdout.write(D1Store.schemaSql({ graphIndex: !args.includes("--no-graph-index"), jsonld }));
25
+ process.stdout.write(
26
+ D1Store.schemaSql({
27
+ graphIndex: !args.includes("--no-graph-index"),
28
+ jsonld,
29
+ versioning: value("--versioning") ?? "off",
30
+ asOfIndex: args.includes("--as-of-index"),
31
+ stampIndex: args.includes("--stamp-index"),
32
+ systemGraphs: args.includes("--system-graphs"),
33
+ }),
34
+ );
35
+ } else if (command === "versioning-migration") {
36
+ const from = value("--from");
37
+ const to = value("--to");
38
+ if (!from || !to) {
39
+ console.error(usage);
40
+ process.exit(1);
41
+ }
42
+ process.stdout.write(`-- oxilite: versioning ${from} -> ${to} (generated by oxilite-d1 versioning-migration)\n`);
43
+ process.stdout.write(
44
+ D1Store.levelChangeSql(from, to, {
45
+ asOfIndex: args.includes("--as-of-index") || undefined,
46
+ stampIndex: args.includes("--stamp-index") || undefined,
47
+ allowLoss: args.includes("--allow-loss"),
48
+ stampColumn: args.includes("--stamp-column"),
49
+ history: args.includes("--frozen-history") ? "frozen" : "none",
50
+ }),
51
+ );
13
52
  } else {
14
- console.error("usage: oxilite-d1 schema [--no-graph-index] [--jsonld] [--no-metadata-indexes]");
53
+ console.error(usage);
15
54
  process.exit(1);
16
55
  }
package/dist/driver.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type CypherOptions, type CypherResult, type CypherValue, type CredentialOptions, type DocumentFilter, type Drift, type JsonLdOptions, type PresentationKeys, type StoredDocument, type Term, type DumpOptions, type LoadData, type LoadOptions, type Quad, type QueryOptions, type QueryResult, type TermLike } from "@oxilite/common";
1
+ import { type CypherOptions, type DatalogMaterializeResult, type DatalogOptions, type DatalogResult, type CypherResult, type CypherValue, type CredentialOptions, type DocumentFilter, type Drift, type JsonLdOptions, type PresentationKeys, type StoredDocument, type Term, type DumpOptions, type LoadData, type LoadOptions, type Quad, type QueryOptions, type QueryResult, type TermLike, type Change, type CommitInfo, type CommitRecord, type LevelChange, type Versioning, type VersionStatus, type GraphArg, type PropertyShapeEntry, type SchemaGraphEntry, type SchemaRegistration, type SchemaRole } from "@oxilite/common";
2
2
  /** The subset of the Cloudflare D1 binding API used by oxilite. */
3
3
  export interface D1PreparedStatementLike {
4
4
  raw(): Promise<unknown[][]>;
@@ -26,6 +26,9 @@ export interface WasmEngine {
26
26
  queryJson(sparql: string): WasmJob;
27
27
  explain(sparql: string): string;
28
28
  cypher(query: string, params?: string | null, options?: string | null): WasmJob;
29
+ datalog?(program: string, options?: string | null): WasmJob;
30
+ datalog_materialize?(program: string, options?: string | null): WasmJob;
31
+ explain_datalog?(program: string, options?: string | null): string;
29
32
  explainCypher(query: string, params?: string | null, options?: string | null): string;
30
33
  update(sparql: string, baseIri?: string | null): WasmJob;
31
34
  explainUpdate(sparql: string): string;
@@ -40,7 +43,27 @@ export interface WasmEngine {
40
43
  clear(): WasmJob;
41
44
  materialize(): WasmJob;
42
45
  clearInferences(): WasmJob;
46
+ registerSchemaGraphSparql(graph: string, role: string, registration?: string | null): string;
47
+ unregisterSchemaGraphSparql(graph: string): string;
48
+ setSchemaGraphActiveSparql(graph: string, active: boolean): string;
49
+ dropSchemaGraphSparql(graph: string): string;
50
+ schemaGraphRegisteredSparql(graph: string): string;
51
+ graphSizeSparql(graph: string): string;
52
+ schemaGraphsSparql(): string;
53
+ schemaGraphsFromOutput(output: string): string;
54
+ systemGraphsSparql(): string;
55
+ systemGraphsReadySparql(): string;
56
+ shapeIndex(): WasmJob;
43
57
  schemaSql(): string;
58
+ versioning(): WasmJob;
59
+ setVersioning(level: string, change?: string | null): WasmJob;
60
+ levelChangeSql(from: string, to: string, change?: string | null, state?: string | null): string;
61
+ setCommitInfo(info?: string | null): void;
62
+ history(limit: number): WasmJob;
63
+ resolveVersion(version: string): WasmJob;
64
+ changes(after: number, until?: number | null): WasmJob;
65
+ diff(from: string, to: string): WasmJob;
66
+ purge(pattern: string, reason?: string | null): WasmJob;
44
67
  jsonld(op: string, args: string, options?: string | null): WasmJob;
45
68
  jsonldSchemaSql(indexes?: string | null): string;
46
69
  }
@@ -55,6 +78,22 @@ export interface D1StoreOptions {
55
78
  migrated?: boolean;
56
79
  /** Create the FTS5 full-text index over string literals (for `oxl:textMatch`). */
57
80
  textIndex?: boolean;
81
+ /**
82
+ * Versioning of a new store: `"off"` (default), `"stamped"` (a store clock and the tick that
83
+ * added each quad) or `"log"` (an immutable change log: history and time travel). An existing
84
+ * store keeps its level: change it with `setVersioning` or a migration.
85
+ */
86
+ versioning?: Versioning;
87
+ /** With `"log"`: index the change log by predicate and object (faster as-of queries). */
88
+ asOfIndex?: boolean;
89
+ /** With `"stamped"` or `"log"`: index the tick that added each quad. */
90
+ stampIndex?: boolean;
91
+ /**
92
+ * Install the system graphs in a blank store: the oxilite vocabulary in `<oxilite:vocabulary>`
93
+ * and the schema registry's own description in `<oxilite:schema>` (default false, so a new
94
+ * store is empty as in Oxigraph).
95
+ */
96
+ systemGraphs?: boolean;
58
97
  }
59
98
  /** An oxilite RDF store on a Cloudflare D1 database. */
60
99
  export declare class D1Store {
@@ -85,6 +124,28 @@ export declare class D1Store {
85
124
  * writing statement reads, then applies its changes as one D1 batch.
86
125
  */
87
126
  cypher(query: string, params?: Record<string, CypherValue>, options?: CypherOptions): Promise<CypherResult>;
127
+ /** The tick a version reference (`"HEAD~2"`, `"#42"`, `"@2026-09-01T00:00:00Z"`) designates. */
128
+ resolveVersion(version: string): Promise<number>;
129
+ /**
130
+ * A Datalog program over the same quads: recursive rules with stratified negation,
131
+ * constraints and aggregation. A program whose recursion is linear is one SQL statement, so
132
+ * it costs one round trip here as it does anywhere; a component that has to be iterated
133
+ * costs one per round, which `rounds` reports.
134
+ */
135
+ datalog(program: string, options?: DatalogOptions): Promise<DatalogResult>;
136
+ /**
137
+ * Stores what a Datalog program derives as inferences, in the table OWL 2 RL
138
+ * materialization uses, so SPARQL and Cypher see them with `includeInferred`. The writes
139
+ * are one D1 batch.
140
+ */
141
+ datalogMaterialize(program: string, options?: DatalogOptions): Promise<DatalogMaterializeResult>;
142
+ /** How a Datalog program runs: its strata, the strategy per recursive component, the SQL. */
143
+ explainDatalog(program: string, options?: DatalogOptions): string;
144
+ /**
145
+ * The Datalog frontend is an opt-in feature of the WebAssembly core, so say which build is
146
+ * needed rather than failing with "not a function".
147
+ */
148
+ private requireDatalog;
88
149
  /** How a Cypher statement runs: its SPARQL, the SQL, and what runs in Rust. */
89
150
  explainCypher(query: string, params?: Record<string, CypherValue>, options?: CypherOptions): string;
90
151
  /** SPARQL query: `Map[]` for SELECT, `boolean` for ASK, `Quad[]` for CONSTRUCT/DESCRIBE, or a string with `results_format`. */
@@ -93,6 +154,35 @@ export declare class D1Store {
93
154
  queryJson(query: string): Promise<string>;
94
155
  /** The SQL a query compiles to, with join orders and warnings. */
95
156
  explain(query: string): string;
157
+ /** The versioning level of the store and where its clock and history stand. */
158
+ versioning(): Promise<VersionStatus>;
159
+ /**
160
+ * Changes the versioning level. Upgrades keep every quad (the upgrade to `"log"` records the
161
+ * whole store as its genesis commit); a downgrade freezes the history and stops the clock,
162
+ * and deletes them only with `allowLoss`. On a production D1 database prefer a migration
163
+ * (`npx oxilite-d1 versioning-migration`).
164
+ */
165
+ setVersioning(level: Versioning, change?: LevelChange): Promise<VersionStatus>;
166
+ /** Author and message recorded on the commits of the following writes (until changed). */
167
+ setCommitInfo(info?: CommitInfo): void;
168
+ /** Runs `f` with `info` recorded on its writes. */
169
+ withCommit<T>(info: CommitInfo, f: (store: this) => Promise<T>): Promise<T>;
170
+ /** The latest commits and level changes, newest first. */
171
+ history(limit?: number): Promise<CommitRecord[]>;
172
+ /** The changes after tick `after` (up to `until`), in order. */
173
+ changes(after?: number, until?: number): Promise<Change[]>;
174
+ /** The net difference between two versions (`"HEAD~1"`, `"#42"`, `"@2026-09-01T00:00:00Z"`). */
175
+ diff(from: string, to?: string): Promise<Change[]>;
176
+ /**
177
+ * Removes the quads matching the pattern from the store and from its whole history, for
178
+ * erasure requests; the purge is recorded without the removed content.
179
+ */
180
+ purge(pattern: {
181
+ subject?: TermLike;
182
+ predicate?: TermLike;
183
+ object?: TermLike;
184
+ graph?: TermLike;
185
+ }, reason?: string): Promise<void>;
96
186
  /** SPARQL update, applied atomically in one D1 batch. */
97
187
  update(update: string, options?: {
98
188
  base_iri?: string;
@@ -121,6 +211,28 @@ export declare class D1Store {
121
211
  materialize(): Promise<number>;
122
212
  /** Removes every materialized inference. */
123
213
  clearInferences(): Promise<void>;
214
+ /**
215
+ * Declares a graph to hold an ontology, SHACL shapes or a ShEx schema, and (`appliesTo`) the
216
+ * graphs it describes. One atomic batch that also rebuilds the reasoning closure and the
217
+ * shape index; the graph's triples stay where they are.
218
+ */
219
+ registerSchemaGraph(graph: GraphArg, role: SchemaRole, registration?: SchemaRegistration): Promise<void>;
220
+ /** The schema registry, ordered by role and graph. */
221
+ schemaGraphs(): Promise<SchemaGraphEntry[]>;
222
+ private registered;
223
+ /** Activates or deactivates a registration; returns whether one was found. */
224
+ setSchemaGraphActive(graph: GraphArg, active: boolean): Promise<boolean>;
225
+ /** Removes a registration, keeping the graph's triples; returns whether one was found. */
226
+ unregisterSchemaGraph(graph: GraphArg): Promise<boolean>;
227
+ /** Removes a registration and every quad of its graph; returns how many quads it held. */
228
+ dropSchemaGraph(graph: GraphArg): Promise<number>;
229
+ /**
230
+ * Installs or refreshes the system graphs (`<oxilite:vocabulary>`, `<oxilite:schema>`'s own
231
+ * description) in an existing database; returns `false` when they were already current.
232
+ */
233
+ installSystemGraphs(): Promise<boolean>;
234
+ /** The compiled SHACL property shapes of the registered shapes graphs. */
235
+ shapeIndex(): Promise<PropertyShapeEntry[]>;
124
236
  /** Refreshes planner statistics (run after large imports). */
125
237
  optimize(): Promise<void>;
126
238
  clear(): Promise<void>;
package/dist/driver.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // The core (WebAssembly) never touches the database. Each operation is a job: the driver
4
4
  // feeds it the response of every request it asks for, until it is done. Atomic requests
5
5
  // become one `db.batch()` — D1's only transaction.
6
- import { JsonLdError, cypherResult, documentText, filterJson, fromJson, jsonLdError, storedDocument, loadDataToString, outputToResult, toJson, } from "@oxilite/common";
6
+ import { JsonLdError, cypherResult, datalogResult, documentText, filterJson, fromJson, jsonLdError, storedDocument, graphJson, registrationJson, toSchemaGraphs, toShapeIndex, toChanges, loadDataToString, outputToResult, toJson, } from "@oxilite/common";
7
7
  /** A term hash collision (two different terms with the same 59-bit id). */
8
8
  export class OxiliteCollisionError extends Error {
9
9
  }
@@ -11,6 +11,8 @@ function mapError(e) {
11
11
  const message = e instanceof Error ? e.message : String(e);
12
12
  if (message.includes("oxilite: term hash collision"))
13
13
  return new OxiliteCollisionError(message);
14
+ if (message.includes("no such column: scope"))
15
+ return new Error("this D1 database has the oxilite 0.4 schema: open it once with D1Store.open(db) (without `migrated`) to upgrade it, then regenerate your migration with `npx oxilite-d1 schema`");
14
16
  if (message.includes("graph_does_not_exist"))
15
17
  return new Error("the graph does not exist");
16
18
  if (message.includes("graph_already_exists"))
@@ -32,7 +34,14 @@ export class D1Store {
32
34
  this.engine = engine;
33
35
  }
34
36
  static async openWith(Engine, db, options = {}) {
35
- const engine = new Engine(null, JSON.stringify({ graphIndex: options.graphIndex ?? true, textIndex: options.textIndex ?? false }));
37
+ const engine = new Engine(null, JSON.stringify({
38
+ graphIndex: options.graphIndex ?? true,
39
+ textIndex: options.textIndex ?? false,
40
+ versioning: options.versioning ?? "off",
41
+ asOfIndex: options.asOfIndex ?? false,
42
+ stampIndex: options.stampIndex ?? false,
43
+ systemGraphs: options.systemGraphs ?? false,
44
+ }));
36
45
  const store = new D1Store(db, engine);
37
46
  await store.run(options.migrated ? engine.openExisting() : engine.open());
38
47
  return store;
@@ -107,9 +116,52 @@ export class D1Store {
107
116
  * writing statement reads, then applies its changes as one D1 batch.
108
117
  */
109
118
  async cypher(query, params = {}, options = {}) {
110
- const out = (await this.run(this.engine.cypher(query, JSON.stringify(params), JSON.stringify(options))));
119
+ // A version is resolved once, so every read of the statement sees the same tick.
120
+ const opts = { ...options };
121
+ if (options.asOf !== undefined) {
122
+ opts.asOfTick = await this.resolveVersion(options.asOf);
123
+ }
124
+ const out = (await this.run(this.engine.cypher(query, JSON.stringify(params), JSON.stringify(opts))));
111
125
  return cypherResult(out);
112
126
  }
127
+ /** The tick a version reference (`"HEAD~2"`, `"#42"`, `"@2026-09-01T00:00:00Z"`) designates. */
128
+ async resolveVersion(version) {
129
+ return (await this.run(this.engine.resolveVersion(version))).value;
130
+ }
131
+ /**
132
+ * A Datalog program over the same quads: recursive rules with stratified negation,
133
+ * constraints and aggregation. A program whose recursion is linear is one SQL statement, so
134
+ * it costs one round trip here as it does anywhere; a component that has to be iterated
135
+ * costs one per round, which `rounds` reports.
136
+ */
137
+ async datalog(program, options = {}) {
138
+ const datalog = this.requireDatalog(this.engine.datalog);
139
+ const out = (await this.run(datalog(program, JSON.stringify(options))));
140
+ return datalogResult(out);
141
+ }
142
+ /**
143
+ * Stores what a Datalog program derives as inferences, in the table OWL 2 RL
144
+ * materialization uses, so SPARQL and Cypher see them with `includeInferred`. The writes
145
+ * are one D1 batch.
146
+ */
147
+ async datalogMaterialize(program, options = {}) {
148
+ const materialize = this.requireDatalog(this.engine.datalog_materialize);
149
+ return (await this.run(materialize(program, JSON.stringify(options))));
150
+ }
151
+ /** How a Datalog program runs: its strata, the strategy per recursive component, the SQL. */
152
+ explainDatalog(program, options = {}) {
153
+ return this.requireDatalog(this.engine.explain_datalog)(program, JSON.stringify(options));
154
+ }
155
+ /**
156
+ * The Datalog frontend is an opt-in feature of the WebAssembly core, so say which build is
157
+ * needed rather than failing with "not a function".
158
+ */
159
+ requireDatalog(method) {
160
+ if (method === undefined) {
161
+ throw new Error("this build of the oxilite WebAssembly core has no Datalog frontend; rebuild it with the `datalog` feature");
162
+ }
163
+ return method.bind(this.engine);
164
+ }
113
165
  /** How a Cypher statement runs: its SPARQL, the SQL, and what runs in Rust. */
114
166
  explainCypher(query, params = {}, options = {}) {
115
167
  return this.engine.explainCypher(query, JSON.stringify(params), JSON.stringify(options));
@@ -136,6 +188,57 @@ export class D1Store {
136
188
  explain(query) {
137
189
  return this.engine.explain(query);
138
190
  }
191
+ // ---------------------------------------------------------------------------- versioning
192
+ /** The versioning level of the store and where its clock and history stand. */
193
+ async versioning() {
194
+ return (await this.run(this.engine.versioning()));
195
+ }
196
+ /**
197
+ * Changes the versioning level. Upgrades keep every quad (the upgrade to `"log"` records the
198
+ * whole store as its genesis commit); a downgrade freezes the history and stops the clock,
199
+ * and deletes them only with `allowLoss`. On a production D1 database prefer a migration
200
+ * (`npx oxilite-d1 versioning-migration`).
201
+ */
202
+ async setVersioning(level, change = {}) {
203
+ return (await this.run(this.engine.setVersioning(level, JSON.stringify(change))));
204
+ }
205
+ /** Author and message recorded on the commits of the following writes (until changed). */
206
+ setCommitInfo(info = {}) {
207
+ this.engine.setCommitInfo(JSON.stringify(info));
208
+ }
209
+ /** Runs `f` with `info` recorded on its writes. */
210
+ async withCommit(info, f) {
211
+ this.setCommitInfo(info);
212
+ try {
213
+ return await f(this);
214
+ }
215
+ finally {
216
+ this.setCommitInfo({});
217
+ }
218
+ }
219
+ /** The latest commits and level changes, newest first. */
220
+ async history(limit = 20) {
221
+ return (await this.run(this.engine.history(limit)));
222
+ }
223
+ /** The changes after tick `after` (up to `until`), in order. */
224
+ async changes(after = 0, until) {
225
+ return toChanges(await this.run(this.engine.changes(after, until ?? null)));
226
+ }
227
+ /** The net difference between two versions (`"HEAD~1"`, `"#42"`, `"@2026-09-01T00:00:00Z"`). */
228
+ async diff(from, to = "HEAD") {
229
+ return toChanges(await this.run(this.engine.diff(from, to)));
230
+ }
231
+ /**
232
+ * Removes the quads matching the pattern from the store and from its whole history, for
233
+ * erasure requests; the purge is recorded without the removed content.
234
+ */
235
+ async purge(pattern, reason) {
236
+ const json = {};
237
+ for (const [k, v] of Object.entries(pattern))
238
+ if (v)
239
+ json[k] = toJson(v);
240
+ await this.run(this.engine.purge(JSON.stringify(json), reason ?? null));
241
+ }
139
242
  /** SPARQL update, applied atomically in one D1 batch. */
140
243
  async update(update, options = {}) {
141
244
  await this.run(this.engine.update(update, options.base_iri ?? null));
@@ -195,6 +298,58 @@ export class D1Store {
195
298
  async clearInferences() {
196
299
  await this.run(this.engine.clearInferences());
197
300
  }
301
+ // The schema registry is RDF in <oxilite:schema>: the core builds the portable SPARQL, and
302
+ // this driver runs it like any other query or update.
303
+ /**
304
+ * Declares a graph to hold an ontology, SHACL shapes or a ShEx schema, and (`appliesTo`) the
305
+ * graphs it describes. One atomic batch that also rebuilds the reasoning closure and the
306
+ * shape index; the graph's triples stay where they are.
307
+ */
308
+ async registerSchemaGraph(graph, role, registration = {}) {
309
+ await this.update(this.engine.registerSchemaGraphSparql(graphJson(graph), role, registrationJson(registration)));
310
+ }
311
+ /** The schema registry, ordered by role and graph. */
312
+ async schemaGraphs() {
313
+ const out = await this.run(this.engine.query(this.engine.schemaGraphsSparql()));
314
+ return toSchemaGraphs(JSON.parse(this.engine.schemaGraphsFromOutput(JSON.stringify(out))));
315
+ }
316
+ async registered(graph) {
317
+ return (await this.query(this.engine.schemaGraphRegisteredSparql(graphJson(graph)))) === true;
318
+ }
319
+ /** Activates or deactivates a registration; returns whether one was found. */
320
+ async setSchemaGraphActive(graph, active) {
321
+ if (!(await this.registered(graph)))
322
+ return false;
323
+ await this.update(this.engine.setSchemaGraphActiveSparql(graphJson(graph), active));
324
+ return true;
325
+ }
326
+ /** Removes a registration, keeping the graph's triples; returns whether one was found. */
327
+ async unregisterSchemaGraph(graph) {
328
+ if (!(await this.registered(graph)))
329
+ return false;
330
+ await this.update(this.engine.unregisterSchemaGraphSparql(graphJson(graph)));
331
+ return true;
332
+ }
333
+ /** Removes a registration and every quad of its graph; returns how many quads it held. */
334
+ async dropSchemaGraph(graph) {
335
+ const rows = (await this.query(this.engine.graphSizeSparql(graphJson(graph))));
336
+ await this.update(this.engine.dropSchemaGraphSparql(graphJson(graph)));
337
+ return Number(rows[0]?.get("n")?.value ?? 0);
338
+ }
339
+ /**
340
+ * Installs or refreshes the system graphs (`<oxilite:vocabulary>`, `<oxilite:schema>`'s own
341
+ * description) in an existing database; returns `false` when they were already current.
342
+ */
343
+ async installSystemGraphs() {
344
+ if ((await this.query(this.engine.systemGraphsReadySparql())) === true)
345
+ return false;
346
+ await this.update(this.engine.systemGraphsSparql());
347
+ return true;
348
+ }
349
+ /** The compiled SHACL property shapes of the registered shapes graphs. */
350
+ async shapeIndex() {
351
+ return toShapeIndex(await this.run(this.engine.shapeIndex()));
352
+ }
198
353
  /** Refreshes planner statistics (run after large imports). */
199
354
  async optimize() {
200
355
  await this.run(this.engine.optimize());
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { D1Store as Base, type D1DatabaseLike, type D1StoreOptions } from "./driver.js";
2
+ import type { LevelChange, Versioning } from "@oxilite/common";
2
3
  export * from "@oxilite/common";
3
4
  export { OxiliteCollisionError, D1Credentials, D1JsonLdDocuments, type D1DatabaseLike, type D1StoreOptions, } from "./driver.js";
4
5
  /** Initializes the WebAssembly core (a `WebAssembly.Module`, bytes, or a URL/Response). */
@@ -19,6 +20,19 @@ export declare class D1Store {
19
20
  subject?: boolean;
20
21
  validUntil?: boolean;
21
22
  };
23
+ versioning?: Versioning;
24
+ asOfIndex?: boolean;
25
+ stampIndex?: boolean;
26
+ systemGraphs?: boolean;
27
+ wasm?: WebAssembly.Module | BufferSource;
28
+ }): Promise<string>;
29
+ /**
30
+ * The SQL that changes the versioning level of an existing D1 database (for
31
+ * `wrangler d1 migrations`). `state` describes the database when it was lowered before.
32
+ */
33
+ static levelChangeSql(from: Versioning, to: Versioning, options?: LevelChange & {
34
+ stampColumn?: boolean;
35
+ history?: "none" | "frozen";
22
36
  wasm?: WebAssembly.Module | BufferSource;
23
37
  }): Promise<string>;
24
38
  }
package/dist/index.js CHANGED
@@ -4,10 +4,19 @@
4
4
  // import wasm from "@oxilite/d1/oxilite.wasm";
5
5
  // const store = await D1Store.open(env.DB, { wasm });
6
6
  import init, { Engine, initSync } from "../wasm/web/oxilite_wasm.js";
7
- import { D1Store as Base } from "./driver.js";
7
+ import { D1Store as Base, } from "./driver.js";
8
8
  export * from "@oxilite/common";
9
9
  export { OxiliteCollisionError, D1Credentials, D1JsonLdDocuments, } from "./driver.js";
10
10
  let ready = false;
11
+ function engineOptions(o) {
12
+ return JSON.stringify({
13
+ graphIndex: o.graphIndex ?? true,
14
+ versioning: o.versioning ?? "off",
15
+ asOfIndex: o.asOfIndex ?? false,
16
+ stampIndex: o.stampIndex ?? false,
17
+ systemGraphs: o.systemGraphs ?? false,
18
+ });
19
+ }
11
20
  /** Initializes the WebAssembly core (a `WebAssembly.Module`, bytes, or a URL/Response). */
12
21
  export async function initOxilite(wasm) {
13
22
  if (ready)
@@ -32,9 +41,19 @@ export class D1Store {
32
41
  */
33
42
  static async schemaSql(options = {}) {
34
43
  await initOxilite(options.wasm);
35
- const e = new Engine(null, JSON.stringify({ graphIndex: options.graphIndex ?? true }));
44
+ const e = new Engine(null, engineOptions(options));
36
45
  if (options.jsonld)
37
46
  return e.jsonldSchemaSql(JSON.stringify(options.jsonld === true ? {} : options.jsonld));
38
47
  return e.schemaSql();
39
48
  }
49
+ /**
50
+ * The SQL that changes the versioning level of an existing D1 database (for
51
+ * `wrangler d1 migrations`). `state` describes the database when it was lowered before.
52
+ */
53
+ static async levelChangeSql(from, to, options = {}) {
54
+ await initOxilite(options.wasm);
55
+ const { stampColumn, history, wasm: _wasm, ...change } = options;
56
+ const e = new Engine(null, null);
57
+ return e.levelChangeSql(from, to, JSON.stringify(change), JSON.stringify({ stampColumn, history }));
58
+ }
40
59
  }
package/dist/node.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { D1Store as Base, type D1DatabaseLike, type D1StoreOptions } from "./driver.js";
2
+ import type { LevelChange, Versioning } from "@oxilite/common";
2
3
  export * from "@oxilite/common";
3
4
  export { OxiliteCollisionError, D1Credentials, D1JsonLdDocuments, type D1DatabaseLike, type D1StoreOptions, } from "./driver.js";
4
5
  export declare class D1Store {
@@ -11,5 +12,17 @@ export declare class D1Store {
11
12
  subject?: boolean;
12
13
  validUntil?: boolean;
13
14
  };
15
+ versioning?: Versioning;
16
+ asOfIndex?: boolean;
17
+ stampIndex?: boolean;
18
+ systemGraphs?: boolean;
19
+ }): string;
20
+ /**
21
+ * The SQL that changes the versioning level of an existing D1 database (for
22
+ * `wrangler d1 migrations`). `stampColumn` / `history` describe a database lowered before.
23
+ */
24
+ static levelChangeSql(from: Versioning, to: Versioning, options?: LevelChange & {
25
+ stampColumn?: boolean;
26
+ history?: "none" | "frozen";
14
27
  }): string;
15
28
  }
package/dist/node.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @oxilite/d1 for Node.js (tests, scripts, Miniflare): the WebAssembly core loads itself.
2
2
  import { createRequire } from "node:module";
3
- import { D1Store as Base } from "./driver.js";
3
+ import { D1Store as Base, } from "./driver.js";
4
4
  export * from "@oxilite/common";
5
5
  export { OxiliteCollisionError, D1Credentials, D1JsonLdDocuments, } from "./driver.js";
6
6
  const require = createRequire(import.meta.url);
@@ -11,9 +11,24 @@ export class D1Store {
11
11
  }
12
12
  /** The schema as SQL; `jsonld` adds the JSON-LD document tables. */
13
13
  static schemaSql(options = {}) {
14
- const e = new Engine(null, JSON.stringify({ graphIndex: options.graphIndex ?? true }));
14
+ const e = new Engine(null, JSON.stringify({
15
+ graphIndex: options.graphIndex ?? true,
16
+ versioning: options.versioning ?? "off",
17
+ asOfIndex: options.asOfIndex ?? false,
18
+ stampIndex: options.stampIndex ?? false,
19
+ systemGraphs: options.systemGraphs ?? false,
20
+ }));
15
21
  if (options.jsonld)
16
22
  return e.jsonldSchemaSql(JSON.stringify(options.jsonld === true ? {} : options.jsonld));
17
23
  return e.schemaSql();
18
24
  }
25
+ /**
26
+ * The SQL that changes the versioning level of an existing D1 database (for
27
+ * `wrangler d1 migrations`). `stampColumn` / `history` describe a database lowered before.
28
+ */
29
+ static levelChangeSql(from, to, options = {}) {
30
+ const { stampColumn, history, ...change } = options;
31
+ const e = new Engine(null, null);
32
+ return e.levelChangeSql(from, to, JSON.stringify(change), JSON.stringify({ stampColumn, history }));
33
+ }
19
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxilite/d1",
3
- "version": "0.2.2",
3
+ "version": "0.5.0",
4
4
  "description": "oxilite on Cloudflare D1 and Durable Objects: an Oxigraph-compatible SPARQL 1.1 and openCypher store (WebAssembly core)",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -32,7 +32,7 @@
32
32
  "test": "vitest run"
33
33
  },
34
34
  "dependencies": {
35
- "@oxilite/common": "0.2.2"
35
+ "@oxilite/common": "0.5.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "miniflare": "^4"