@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 +35 -2
- package/bin/oxilite-d1.mjs +42 -3
- package/dist/driver.d.ts +113 -1
- package/dist/driver.js +158 -3
- package/dist/index.d.ts +14 -0
- package/dist/index.js +21 -2
- package/dist/node.d.ts +13 -0
- package/dist/node.js +17 -2
- package/package.json +2 -2
- package/wasm/node/oxilite_wasm.d.ts +90 -3
- package/wasm/node/oxilite_wasm.js +392 -1
- package/wasm/node/oxilite_wasm_bg.wasm +0 -0
- package/wasm/node/oxilite_wasm_bg.wasm.d.ts +20 -0
- package/wasm/web/oxilite_wasm.d.ts +110 -3
- package/wasm/web/oxilite_wasm.js +392 -1
- package/wasm/web/oxilite_wasm_bg.wasm +0 -0
- package/wasm/web/oxilite_wasm_bg.wasm.d.ts +20 -0
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 `
|
|
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
|
|
package/bin/oxilite-d1.mjs
CHANGED
|
@@ -1,16 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Usage:
|
|
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(
|
|
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(
|
|
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({
|
|
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
|
-
|
|
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,
|
|
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({
|
|
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.
|
|
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.
|
|
35
|
+
"@oxilite/common": "0.5.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"miniflare": "^4"
|