@factoidal/core 0.1.0 → 0.3.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/CHANGELOG.md +125 -0
- package/README.md +149 -1
- package/bin/engine.mjs +124 -0
- package/bin/factoidal.mjs +658 -0
- package/bin/store.mjs +206 -0
- package/factoidal-npm-entry.js +13657 -30789
- package/factoidal-npm-entry.wasm.assets/{code-7ac046580f1bbdda8dc6.wasm → code-5a7fc68f2ab1323718b8.wasm} +0 -0
- package/factoidal-npm-entry.wasm.js +2 -2
- package/factoidal.js +11448 -25890
- package/fn.js +39 -0
- package/l4-assets/l4factoidal.js +197 -0
- package/l4-assets/l4factoidal.mjs +2 -0
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/package.json +4 -0
- package/l4-assets/version.json +16 -0
- package/l4-core.d.ts +159 -0
- package/l4-core.js +188 -0
- package/l4.d.ts +50 -0
- package/l4.js +104 -0
- package/lib/api.js +162 -2
- package/package.json +30 -3
- package/select.d.ts +116 -0
- package/select.js +492 -0
- package/store-host/deno.mjs +199 -0
- package/store-host/errors.mjs +55 -0
- package/store-host/index.mjs +208 -0
- package/store-host/node.mjs +212 -0
- package/store-host/paths.mjs +77 -0
- package/version.json +24 -23
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `factoidal` command: pack, activate, query, update and compact a
|
|
3
|
+
// persisted Shardborough store from Node or Deno, with no native binary.
|
|
4
|
+
// https://github.com/danbri/factoidal/issues/641
|
|
5
|
+
//
|
|
6
|
+
// STATE TODAY. The argument surface below is complete and settled, so that
|
|
7
|
+
// wiring the remaining subcommands changes no user-visible syntax.
|
|
8
|
+
// `version`, `inspect` and `query` run: the first needs only host file
|
|
9
|
+
// I/O, and the other two drive the three WebAssembly store operations of
|
|
10
|
+
// `formal/lean4/Wasm/Ops/Store.lean`. `pack`, `activate`, `update` and
|
|
11
|
+
// `compact` parse their arguments, report what they would do, and exit 3;
|
|
12
|
+
// they need operations that do not exist yet.
|
|
13
|
+
//
|
|
14
|
+
// This file reads files by name, moves bytes and renders what the engine
|
|
15
|
+
// answered. It parses no manifest, verifies no digest, decodes no block
|
|
16
|
+
// and chooses no artifact -- every one of those is a format decision and
|
|
17
|
+
// it lives in the Lean source (iron rule 7). The table renderer is a
|
|
18
|
+
// human display of the engine's own SPARQL Query Results JSON, not a
|
|
19
|
+
// second serializer: `--format json`, `--format nquads` and
|
|
20
|
+
// `--format turtle` all print documents the engine produced.
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
StoreHostError, listGeneration, readWhole, runtime
|
|
24
|
+
} from '../store-host/index.mjs'
|
|
25
|
+
import { fileUrlToPath, joinPath } from '../store-host/paths.mjs'
|
|
26
|
+
import { loadEngine } from './engine.mjs'
|
|
27
|
+
import {
|
|
28
|
+
StoreOperationError, inspectManifest, openStore, planQuery, queryStore,
|
|
29
|
+
turtleOfNQuads
|
|
30
|
+
} from './store.mjs'
|
|
31
|
+
|
|
32
|
+
const EXIT_OK = 0
|
|
33
|
+
const EXIT_FAILURE = 1
|
|
34
|
+
const EXIT_USAGE = 2
|
|
35
|
+
const EXIT_NOT_WIRED = 3
|
|
36
|
+
|
|
37
|
+
const ISSUE = 'https://github.com/danbri/factoidal/issues/641'
|
|
38
|
+
|
|
39
|
+
const isDeno = typeof globalThis.Deno !== 'undefined'
|
|
40
|
+
const argv = isDeno ? globalThis.Deno.args.slice() : process.argv.slice(2)
|
|
41
|
+
|
|
42
|
+
// Node writes to a PIPE asynchronously, and `process.exit()` drops
|
|
43
|
+
// whatever is still buffered. A SELECT that prints a few megabytes of
|
|
44
|
+
// SPARQL Query Results JSON into `| jq` therefore arrived truncated at
|
|
45
|
+
// the 64 KiB pipe boundary (measured 2026-09-03, 6455 rows). Setting the
|
|
46
|
+
// exit code and letting the process end on its own flushes it.
|
|
47
|
+
function exit (code) {
|
|
48
|
+
if (isDeno) globalThis.Deno.exit(code)
|
|
49
|
+
else process.exitCode = code
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function out (line) { console.log(line) }
|
|
53
|
+
function err (line) { console.error(line) }
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------- usage
|
|
56
|
+
|
|
57
|
+
const USAGE = `factoidal - the Factoidal persisted store, from Node or Deno
|
|
58
|
+
|
|
59
|
+
usage: factoidal <command> [options]
|
|
60
|
+
|
|
61
|
+
commands:
|
|
62
|
+
version print the package and engine versions
|
|
63
|
+
inspect STORE report what the activated manifest commits
|
|
64
|
+
query STORE [QUERY] evaluate a SPARQL query against a store
|
|
65
|
+
pack INPUT OUTPUT build one immutable generation from an RDF file
|
|
66
|
+
activate STORE GENERATION make one generation the activated generation
|
|
67
|
+
update STORE [UPDATE] apply a SPARQL Update through the delta log
|
|
68
|
+
compact STORE GENERATION fold the delta log into a new generation
|
|
69
|
+
|
|
70
|
+
global options:
|
|
71
|
+
-h, --help print this text, or a command's own help
|
|
72
|
+
--json machine-readable output where a command has it
|
|
73
|
+
--quiet print results only, no progress lines
|
|
74
|
+
|
|
75
|
+
exit codes:
|
|
76
|
+
0 success 1 failure 2 usage error 3 not yet wired (${ISSUE})
|
|
77
|
+
|
|
78
|
+
STORE is a collection root: the directory that holds CURRENT.`
|
|
79
|
+
|
|
80
|
+
const COMMAND_USAGE = {
|
|
81
|
+
version: `factoidal version - print the package and engine versions
|
|
82
|
+
|
|
83
|
+
usage: factoidal version [--json]
|
|
84
|
+
|
|
85
|
+
Prints the npm package version, the Lean engine's WebAssembly digest as
|
|
86
|
+
recorded by its build, and which host-I/O implementation is loaded.`,
|
|
87
|
+
|
|
88
|
+
inspect: `factoidal inspect - report what a store's manifest commits
|
|
89
|
+
|
|
90
|
+
usage: factoidal inspect STORE [--json] [--generation NAME]
|
|
91
|
+
|
|
92
|
+
Reads CURRENT, hands the manifest bytes to the engine's
|
|
93
|
+
storeManifestInspect operation, and prints what it decoded: the wire
|
|
94
|
+
version, the layout, the blank-node publication profile, the term-registry
|
|
95
|
+
version, whether the manifest carries a fixed-chunk Merkle commitment, and
|
|
96
|
+
one row per entry with its predicate, row count, byte length, block kind
|
|
97
|
+
and graph set.
|
|
98
|
+
|
|
99
|
+
options:
|
|
100
|
+
--generation NAME inspect this generation instead of the activated one
|
|
101
|
+
--json print the operation's envelope unchanged`,
|
|
102
|
+
|
|
103
|
+
query: `factoidal query - evaluate a SPARQL query against a store
|
|
104
|
+
|
|
105
|
+
usage: factoidal query STORE [QUERY] [options]
|
|
106
|
+
|
|
107
|
+
QUERY is the query text. Give it as the second argument, or with --query,
|
|
108
|
+
or in a file with --file.
|
|
109
|
+
|
|
110
|
+
The command reads CURRENT and the manifest, asks the engine which
|
|
111
|
+
artifacts the query needs, reads exactly those, and hands their bytes to
|
|
112
|
+
the engine's storeQuery operation. The engine verifies every artifact
|
|
113
|
+
against the SHA-256 the manifest commits before it answers.
|
|
114
|
+
|
|
115
|
+
options:
|
|
116
|
+
--query TEXT the query text
|
|
117
|
+
--file PATH read the query text from a file
|
|
118
|
+
--format FORMAT table (default), json, nquads, turtle
|
|
119
|
+
--limit N print at most N table rows; the total is always named
|
|
120
|
+
--explain print the artifact plan instead of the results
|
|
121
|
+
--generation NAME query this generation instead of the activated one
|
|
122
|
+
--json shorthand for --format json
|
|
123
|
+
--quiet print the result only, no plan line on stderr
|
|
124
|
+
|
|
125
|
+
formats:
|
|
126
|
+
table a human display of the engine's SPARQL Query Results JSON;
|
|
127
|
+
ASK prints true or false, CONSTRUCT prints its N-Triples
|
|
128
|
+
json SELECT prints the engine's SPARQL 1.1 Query Results JSON
|
|
129
|
+
document; ASK and CONSTRUCT print the operation's envelope,
|
|
130
|
+
because the operation answers those two with a boolean and a
|
|
131
|
+
serialized graph rather than with a results document
|
|
132
|
+
nquads CONSTRUCT only: the graph the engine serialized
|
|
133
|
+
turtle CONSTRUCT only: that graph through the engine's own Turtle
|
|
134
|
+
writer, which flattens named graphs into the default graph
|
|
135
|
+
|
|
136
|
+
not available, and why:
|
|
137
|
+
--base IRI the store query operation takes no base argument;
|
|
138
|
+
put a BASE clause in the query text instead
|
|
139
|
+
xml, csv, tsv the engine has no operation that writes the SPARQL
|
|
140
|
+
Results XML, CSV or TSV documents, and writing one
|
|
141
|
+
here would be a second serializer (iron rule 7)
|
|
142
|
+
DESCRIBE the engine answers "DESCRIBE is not supported by the
|
|
143
|
+
npm entry yet"
|
|
144
|
+
|
|
145
|
+
A store carrying a non-empty delta log is not served by this path: the
|
|
146
|
+
operation reads the manifest's committed artifacts only. Use the native
|
|
147
|
+
l4block-* tools for a store with uncompacted updates.`,
|
|
148
|
+
|
|
149
|
+
pack: `factoidal pack - build one immutable generation from an RDF file
|
|
150
|
+
|
|
151
|
+
usage: factoidal pack INPUT OUTPUT [options]
|
|
152
|
+
|
|
153
|
+
INPUT is an RDF file. OUTPUT is the generation directory to create; it is
|
|
154
|
+
normally STORE/gen-N. Packing does not activate: run activate next.
|
|
155
|
+
|
|
156
|
+
options:
|
|
157
|
+
--layout LAYOUT ibk3 (triples, default) or ibk4 (quads)
|
|
158
|
+
--syntax SYNTAX turtle, trig or nquads; default from the file extension
|
|
159
|
+
--chunk-bytes N Merkle chunk size; default is the engine's
|
|
160
|
+
--json emit one JSON object`,
|
|
161
|
+
|
|
162
|
+
activate: `factoidal activate - make one generation the activated generation
|
|
163
|
+
|
|
164
|
+
usage: factoidal activate STORE GENERATION [--json]
|
|
165
|
+
|
|
166
|
+
Verifies every artifact of GENERATION and every cross-artifact relation,
|
|
167
|
+
then atomically replaces STORE/CURRENT. A generation that fails
|
|
168
|
+
verification never becomes current.`,
|
|
169
|
+
|
|
170
|
+
update: `factoidal update - apply a SPARQL Update through the delta log
|
|
171
|
+
|
|
172
|
+
usage: factoidal update STORE [UPDATE] [options]
|
|
173
|
+
|
|
174
|
+
UPDATE is the update text. Give it as the second argument, or with
|
|
175
|
+
--update, or in a file with --file. The batch is appended to the
|
|
176
|
+
activated generation's delta log and is visible to the next query.
|
|
177
|
+
|
|
178
|
+
options:
|
|
179
|
+
--update TEXT the update text
|
|
180
|
+
--file PATH read the update text from a file
|
|
181
|
+
--json emit one JSON object`,
|
|
182
|
+
|
|
183
|
+
compact: `factoidal compact - fold the delta log into a new generation
|
|
184
|
+
|
|
185
|
+
usage: factoidal compact STORE GENERATION [options]
|
|
186
|
+
|
|
187
|
+
Reads the activated generation and its committed delta batches, writes
|
|
188
|
+
GENERATION as a new immutable generation under STORE, and records its
|
|
189
|
+
compacted epoch. It does not activate unless --activate is given.
|
|
190
|
+
|
|
191
|
+
options:
|
|
192
|
+
--activate activate the new generation when it verifies
|
|
193
|
+
--json emit one JSON object`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ------------------------------------------------------------- arguments
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Split argv into positional arguments and options. `--name value` and
|
|
200
|
+
* `--name=value` both work; a flag with no value is `true`.
|
|
201
|
+
*/
|
|
202
|
+
function parseArguments (args, valueOptions) {
|
|
203
|
+
const positional = []
|
|
204
|
+
const options = Object.create(null)
|
|
205
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
206
|
+
const argument = args[index]
|
|
207
|
+
if (argument === '--') {
|
|
208
|
+
positional.push(...args.slice(index + 1))
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
if (argument === '-h') {
|
|
212
|
+
options.help = true
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
if (!argument.startsWith('--')) {
|
|
216
|
+
positional.push(argument)
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
const equals = argument.indexOf('=')
|
|
220
|
+
const name = equals < 0 ? argument.slice(2) : argument.slice(2, equals)
|
|
221
|
+
if (equals >= 0) {
|
|
222
|
+
options[name] = argument.slice(equals + 1)
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
if (valueOptions.has(name)) {
|
|
226
|
+
index += 1
|
|
227
|
+
if (index >= args.length) {
|
|
228
|
+
throw new UsageError(`option --${name} needs a value`)
|
|
229
|
+
}
|
|
230
|
+
options[name] = args[index]
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
options[name] = true
|
|
234
|
+
}
|
|
235
|
+
return { positional, options }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
class UsageError extends Error {}
|
|
239
|
+
|
|
240
|
+
const VALUE_OPTIONS = {
|
|
241
|
+
version: new Set([]),
|
|
242
|
+
inspect: new Set(['generation']),
|
|
243
|
+
query: new Set(['query', 'file', 'format', 'limit', 'base', 'generation']),
|
|
244
|
+
pack: new Set(['layout', 'syntax', 'chunk-bytes']),
|
|
245
|
+
activate: new Set([]),
|
|
246
|
+
update: new Set(['update', 'file']),
|
|
247
|
+
compact: new Set([])
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ------------------------------------------------------------- commands
|
|
251
|
+
|
|
252
|
+
function packageDirectory () {
|
|
253
|
+
return fileUrlToPath(new URL('..', import.meta.url).href)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function readJsonFile (path) {
|
|
257
|
+
const bytes = readWhole(path)
|
|
258
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function commandVersion (options) {
|
|
262
|
+
const root = packageDirectory()
|
|
263
|
+
const own = readJsonFile(joinPath(root, 'package.json'))
|
|
264
|
+
let engine = null
|
|
265
|
+
try {
|
|
266
|
+
engine = readJsonFile(joinPath(root, 'l4-assets/version.json'))
|
|
267
|
+
} catch (_error) {
|
|
268
|
+
engine = null
|
|
269
|
+
}
|
|
270
|
+
const report = {
|
|
271
|
+
package: own.name,
|
|
272
|
+
version: own.version,
|
|
273
|
+
host: runtime,
|
|
274
|
+
engine: engine === null
|
|
275
|
+
? null
|
|
276
|
+
: {
|
|
277
|
+
engine: engine.engine,
|
|
278
|
+
version: engine.version,
|
|
279
|
+
gitSha: engine.gitSha,
|
|
280
|
+
leanToolchain: engine.leanToolchain,
|
|
281
|
+
wasmSha256: engine.wasmSha256,
|
|
282
|
+
wasmBytes: engine.wasmBytes
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (options.json) {
|
|
286
|
+
out(JSON.stringify(report, null, 2))
|
|
287
|
+
return EXIT_OK
|
|
288
|
+
}
|
|
289
|
+
out(`${report.package} ${report.version}`)
|
|
290
|
+
out(`host-io ${report.host}`)
|
|
291
|
+
if (report.engine !== null) {
|
|
292
|
+
out(`engine ${report.engine.engine} ${report.engine.version} (${report.engine.leanToolchain})`)
|
|
293
|
+
out(`wasm sha256 ${report.engine.wasmSha256} (${report.engine.wasmBytes} bytes)`)
|
|
294
|
+
} else {
|
|
295
|
+
out('engine (no l4-assets/version.json in this install)')
|
|
296
|
+
}
|
|
297
|
+
return EXIT_OK
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ------------------------------------------------------------ rendering
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* One graph name as the manifest reports it. `{"kind":"default"}` is the
|
|
304
|
+
* default graph; the operation marks it that way so a host never has to
|
|
305
|
+
* recognise a reserved IRI.
|
|
306
|
+
*/
|
|
307
|
+
function graphLabel (graph) {
|
|
308
|
+
if (graph.kind === 'default') return 'default'
|
|
309
|
+
if (graph.kind === 'iri') return `<${graph.value}>`
|
|
310
|
+
if (graph.kind === 'bnode') return `_:${graph.value}`
|
|
311
|
+
return JSON.stringify(graph)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* One SPARQL Query Results JSON term as a table cell. This is a display,
|
|
316
|
+
* not a serialization: `--format json` prints the engine's own document
|
|
317
|
+
* and `--format nquads` prints the engine's own N-Triples.
|
|
318
|
+
*/
|
|
319
|
+
function cellOfTerm (term) {
|
|
320
|
+
if (term === undefined || term === null) return ''
|
|
321
|
+
if (term.type === 'uri') return `<${term.value}>`
|
|
322
|
+
if (term.type === 'bnode') return `_:${term.value}`
|
|
323
|
+
if (term.type === 'literal') {
|
|
324
|
+
const lang = term['xml:lang']
|
|
325
|
+
if (typeof lang === 'string' && lang.length > 0) return `"${term.value}"@${lang}`
|
|
326
|
+
if (typeof term.datatype === 'string') return `"${term.value}"^^<${term.datatype}>`
|
|
327
|
+
return `"${term.value}"`
|
|
328
|
+
}
|
|
329
|
+
return JSON.stringify(term)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function plural (count, noun) {
|
|
333
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Print rows as columns padded to their widest cell. */
|
|
337
|
+
function printTable (headers, rows) {
|
|
338
|
+
const widths = headers.map((header) => header.length)
|
|
339
|
+
for (const row of rows) {
|
|
340
|
+
for (let index = 0; index < row.length; index += 1) {
|
|
341
|
+
if (row[index].length > widths[index]) widths[index] = row[index].length
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const line = (cells) => cells
|
|
345
|
+
.map((cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index]))
|
|
346
|
+
.join(' ')
|
|
347
|
+
out(line(headers))
|
|
348
|
+
for (const row of rows) out(line(row))
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ------------------------------------------------------------- inspect
|
|
352
|
+
|
|
353
|
+
async function commandInspect (positional, options) {
|
|
354
|
+
if (positional.length !== 1) throw new UsageError('inspect needs exactly one STORE')
|
|
355
|
+
const root = positional[0]
|
|
356
|
+
const named = typeof options.generation === 'string' ? options.generation : null
|
|
357
|
+
const store = openStore(root, named)
|
|
358
|
+
const engine = await loadEngine()
|
|
359
|
+
const envelope = inspectManifest(engine, store)
|
|
360
|
+
|
|
361
|
+
if (options.json) {
|
|
362
|
+
// The operation's envelope, unchanged.
|
|
363
|
+
out(JSON.stringify(envelope, null, 2))
|
|
364
|
+
return EXIT_OK
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const files = listGeneration(store.generationDir)
|
|
368
|
+
let directoryBytes = 0
|
|
369
|
+
for (const file of files) directoryBytes += file.size
|
|
370
|
+
|
|
371
|
+
out(`store ${root}`)
|
|
372
|
+
out(`generation ${store.generation}${store.activated ? ' (activated through CURRENT)' : ' (named on the command line)'}`)
|
|
373
|
+
out(`manifest ${store.manifestName}, ${store.manifest.length} bytes, wire version ${envelope.wireVersion}`)
|
|
374
|
+
out(`layout ${envelope.layout}`)
|
|
375
|
+
out(`blank-node profile ${envelope.blankNodeProfile === '' ? '(none recorded)' : envelope.blankNodeProfile}`)
|
|
376
|
+
out(`term registry ${envelope.termRegistryVersion === '' ? '(none recorded)' : envelope.termRegistryVersion}`)
|
|
377
|
+
out(`fixed-chunk Merkle commitment ${envelope.rangeCommitted ? 'yes' : 'no'}`)
|
|
378
|
+
out(`${envelope.entries.length} ${envelope.entries.length === 1 ? 'entry' : 'entries'}, ${envelope.totalBytes} bytes, ${plural(envelope.totalRows, 'row')}`)
|
|
379
|
+
out(`generation directory holds ${files.length} files, ${directoryBytes} bytes`)
|
|
380
|
+
out('')
|
|
381
|
+
printTable(
|
|
382
|
+
['#', 'rows', 'bytes', 'kind', 'graphs', 'predicate'],
|
|
383
|
+
envelope.entries.map((entry) => [
|
|
384
|
+
String(entry.ordinal),
|
|
385
|
+
String(entry.rows),
|
|
386
|
+
String(entry.bytes),
|
|
387
|
+
entry.blockKind,
|
|
388
|
+
entry.graphs.length === 0 ? '-' : entry.graphs.map(graphLabel).join(' '),
|
|
389
|
+
entry.predicate
|
|
390
|
+
]))
|
|
391
|
+
return EXIT_OK
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function notWired (command, detail) {
|
|
395
|
+
err(`factoidal ${command}: not yet wired. ${detail}`)
|
|
396
|
+
err(`This needs a WebAssembly operation from the Lean engine; see ${ISSUE}.`)
|
|
397
|
+
return EXIT_NOT_WIRED
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const QUERY_FORMATS = ['table', 'json', 'nquads', 'turtle']
|
|
401
|
+
const UNAVAILABLE_FORMATS = {
|
|
402
|
+
xml: 'the SPARQL Results XML document',
|
|
403
|
+
csv: 'the SPARQL Results CSV document',
|
|
404
|
+
tsv: 'the SPARQL Results TSV document'
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function queryText (positional, options) {
|
|
408
|
+
if (typeof options.file === 'string') {
|
|
409
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(readWhole(options.file))
|
|
410
|
+
}
|
|
411
|
+
if (positional.length > 1) return positional.slice(1).join(' ')
|
|
412
|
+
if (typeof options.query === 'string') return options.query
|
|
413
|
+
throw new UsageError('query needs QUERY, --query TEXT or --file PATH')
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function queryFormat (options) {
|
|
417
|
+
if (options.json === true && typeof options.format !== 'string') return 'json'
|
|
418
|
+
if (typeof options.format !== 'string') return 'table'
|
|
419
|
+
const format = options.format.toLowerCase()
|
|
420
|
+
if (QUERY_FORMATS.indexOf(format) >= 0) return format
|
|
421
|
+
if (Object.prototype.hasOwnProperty.call(UNAVAILABLE_FORMATS, format)) {
|
|
422
|
+
throw new UsageError(
|
|
423
|
+
`--format ${format} needs an engine operation that writes ` +
|
|
424
|
+
`${UNAVAILABLE_FORMATS[format]}; there is none, and writing one here ` +
|
|
425
|
+
'would be a second serializer. Use --format json.')
|
|
426
|
+
}
|
|
427
|
+
throw new UsageError(`--format ${options.format} is not one of ${QUERY_FORMATS.join(', ')}`)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function queryLimit (options, format) {
|
|
431
|
+
if (typeof options.limit !== 'string') return null
|
|
432
|
+
const limit = Number(options.limit)
|
|
433
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
434
|
+
throw new UsageError('--limit needs a non-negative whole number')
|
|
435
|
+
}
|
|
436
|
+
if (format !== 'table') {
|
|
437
|
+
throw new UsageError(
|
|
438
|
+
'--limit truncates the printed table only; it is not carried into the ' +
|
|
439
|
+
"engine's own documents. Put a LIMIT clause in the query instead.")
|
|
440
|
+
}
|
|
441
|
+
return limit
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Report a refusal the store operations made, and what to do about it. */
|
|
445
|
+
function reportStoreFailure (error) {
|
|
446
|
+
err(`factoidal query: ${error.message}`)
|
|
447
|
+
if (error.capLimit !== null) {
|
|
448
|
+
err(`This query needs more of the store than one WebAssembly call may read: ${error.capValue} against a cap of ${error.capLimit}.`)
|
|
449
|
+
err('Narrow the query - bind a predicate, or restrict the graph - or use the native l4block-* tools.')
|
|
450
|
+
} else if (error.stackLimit) {
|
|
451
|
+
err('The runtime ran out of call stack inside the engine, not the store.')
|
|
452
|
+
err('Some evaluator paths recurse once per row, and a few thousand rows can')
|
|
453
|
+
err("exceed Node's default WebAssembly frame budget. Raise it with")
|
|
454
|
+
err('node --stack-size=4000, add a LIMIT, or run the query under Deno.')
|
|
455
|
+
} else if (error.digestKey !== null) {
|
|
456
|
+
err(`The bytes of '${error.digestKey}' in the generation directory are not the bytes the manifest commits.`)
|
|
457
|
+
err('The generation is damaged or was edited after it was packed; repack or restore it.')
|
|
458
|
+
}
|
|
459
|
+
return EXIT_FAILURE
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function commandQuery (positional, options) {
|
|
463
|
+
if (positional.length < 1) throw new UsageError('query needs a STORE')
|
|
464
|
+
if (typeof options.base === 'string') {
|
|
465
|
+
throw new UsageError(
|
|
466
|
+
'the store query operation takes no base argument; put a BASE clause ' +
|
|
467
|
+
'in the query text instead')
|
|
468
|
+
}
|
|
469
|
+
const root = positional[0]
|
|
470
|
+
const sparql = queryText(positional, options)
|
|
471
|
+
const format = queryFormat(options)
|
|
472
|
+
const limit = queryLimit(options, format)
|
|
473
|
+
const quiet = options.quiet === true
|
|
474
|
+
|
|
475
|
+
const named = typeof options.generation === 'string' ? options.generation : null
|
|
476
|
+
const store = openStore(root, named)
|
|
477
|
+
const engine = await loadEngine()
|
|
478
|
+
|
|
479
|
+
if (options.explain === true) {
|
|
480
|
+
let plan
|
|
481
|
+
try {
|
|
482
|
+
plan = planQuery(engine, store, sparql)
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (error instanceof StoreOperationError) return reportStoreFailure(error)
|
|
485
|
+
throw error
|
|
486
|
+
}
|
|
487
|
+
if (format === 'json' || options.json === true) {
|
|
488
|
+
out(JSON.stringify(plan, null, 2))
|
|
489
|
+
return EXIT_OK
|
|
490
|
+
}
|
|
491
|
+
out(`layout ${plan.layout} (wire version ${plan.wireVersion})`)
|
|
492
|
+
out(`mode ${plan.mode}`)
|
|
493
|
+
out(`${plural(plan.shards, 'artifact')}, ${plan.bytes} bytes, ${plural(plan.rows, 'row')}`)
|
|
494
|
+
for (const key of plan.keys) out(` ${key}`)
|
|
495
|
+
return EXIT_OK
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let answer
|
|
499
|
+
try {
|
|
500
|
+
answer = queryStore(engine, store, sparql)
|
|
501
|
+
} catch (error) {
|
|
502
|
+
if (error instanceof StoreOperationError) return reportStoreFailure(error)
|
|
503
|
+
throw error
|
|
504
|
+
}
|
|
505
|
+
const { plan, result, blobBytes } = answer
|
|
506
|
+
if (!quiet) {
|
|
507
|
+
err(`mode ${result.mode}, ${plural(result.shards, 'artifact')}, ${blobBytes} bytes read, plan declares ${plural(plan.rows, 'block row')}`)
|
|
508
|
+
}
|
|
509
|
+
return renderQueryResult(engine, result, format, limit, quiet)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function renderQueryResult (engine, result, format, limit, quiet) {
|
|
513
|
+
if (result.kind === 'select') {
|
|
514
|
+
const vars = result.srj.head.vars
|
|
515
|
+
const bindings = result.srj.results.bindings
|
|
516
|
+
if (format === 'json') {
|
|
517
|
+
out(JSON.stringify(result.srj, null, 2))
|
|
518
|
+
return EXIT_OK
|
|
519
|
+
}
|
|
520
|
+
if (format !== 'table') {
|
|
521
|
+
err(`factoidal query: --format ${format} needs a CONSTRUCT query; this one is a SELECT`)
|
|
522
|
+
return EXIT_FAILURE
|
|
523
|
+
}
|
|
524
|
+
const shown = limit === null ? bindings : bindings.slice(0, limit)
|
|
525
|
+
printTable(vars, shown.map((row) => vars.map((name) => cellOfTerm(row[name]))))
|
|
526
|
+
if (shown.length < bindings.length) {
|
|
527
|
+
err(`showing ${shown.length} of ${bindings.length} rows (--limit ${limit})`)
|
|
528
|
+
} else if (!quiet) {
|
|
529
|
+
err(plural(bindings.length, 'row'))
|
|
530
|
+
}
|
|
531
|
+
return EXIT_OK
|
|
532
|
+
}
|
|
533
|
+
if (result.kind === 'ask') {
|
|
534
|
+
if (format === 'json') {
|
|
535
|
+
// The operation answers ASK with a boolean, not with a results
|
|
536
|
+
// document, so the envelope is what there is to print.
|
|
537
|
+
out(JSON.stringify(result, null, 2))
|
|
538
|
+
return EXIT_OK
|
|
539
|
+
}
|
|
540
|
+
if (format !== 'table') {
|
|
541
|
+
err(`factoidal query: --format ${format} needs a CONSTRUCT query; this one is an ASK`)
|
|
542
|
+
return EXIT_FAILURE
|
|
543
|
+
}
|
|
544
|
+
out(result.boolean ? 'true' : 'false')
|
|
545
|
+
return EXIT_OK
|
|
546
|
+
}
|
|
547
|
+
if (result.kind === 'construct') {
|
|
548
|
+
if (format === 'json') {
|
|
549
|
+
out(JSON.stringify(result, null, 2))
|
|
550
|
+
return EXIT_OK
|
|
551
|
+
}
|
|
552
|
+
if (format === 'turtle') {
|
|
553
|
+
out(turtleOfNQuads(engine, result.nquads))
|
|
554
|
+
return EXIT_OK
|
|
555
|
+
}
|
|
556
|
+
// table and nquads both print what the engine serialized.
|
|
557
|
+
if (result.nquads.length > 0) out(result.nquads.replace(/\n$/, ''))
|
|
558
|
+
return EXIT_OK
|
|
559
|
+
}
|
|
560
|
+
err(`factoidal query: the engine answered an unknown result kind "${result.kind}"`)
|
|
561
|
+
return EXIT_FAILURE
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function commandPack (positional, _options) {
|
|
565
|
+
if (positional.length !== 2) throw new UsageError('pack needs INPUT and OUTPUT')
|
|
566
|
+
return notWired('pack', 'The streaming pack operations are stage 3 of the milestone.')
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function commandActivate (positional, _options) {
|
|
570
|
+
if (positional.length !== 2) throw new UsageError('activate needs STORE and GENERATION')
|
|
571
|
+
return notWired('activate', 'Activation must verify every artifact before it replaces CURRENT.')
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function commandUpdate (positional, options) {
|
|
575
|
+
if (positional.length < 1) throw new UsageError('update needs a STORE')
|
|
576
|
+
const text = positional.length > 1
|
|
577
|
+
? positional.slice(1).join(' ')
|
|
578
|
+
: (typeof options.update === 'string' ? options.update : null)
|
|
579
|
+
if (text === null && typeof options.file !== 'string') {
|
|
580
|
+
throw new UsageError('update needs UPDATE, --update TEXT or --file PATH')
|
|
581
|
+
}
|
|
582
|
+
return notWired('update', 'The delta-log operations are stage 4 of the milestone.')
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function commandCompact (positional, _options) {
|
|
586
|
+
if (positional.length !== 2) throw new UsageError('compact needs STORE and GENERATION')
|
|
587
|
+
return notWired('compact', 'Compaction is stage 4 of the milestone.')
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const COMMANDS = {
|
|
591
|
+
version: (positional, options) => commandVersion(options),
|
|
592
|
+
inspect: commandInspect,
|
|
593
|
+
query: commandQuery,
|
|
594
|
+
pack: commandPack,
|
|
595
|
+
activate: commandActivate,
|
|
596
|
+
update: commandUpdate,
|
|
597
|
+
compact: commandCompact
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ----------------------------------------------------------------- main
|
|
601
|
+
|
|
602
|
+
async function main () {
|
|
603
|
+
if (argv.length === 0) {
|
|
604
|
+
out(USAGE)
|
|
605
|
+
return EXIT_USAGE
|
|
606
|
+
}
|
|
607
|
+
const command = argv[0]
|
|
608
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
609
|
+
const topic = argv[1]
|
|
610
|
+
if (typeof topic === 'string' && Object.prototype.hasOwnProperty.call(COMMAND_USAGE, topic)) {
|
|
611
|
+
out(COMMAND_USAGE[topic])
|
|
612
|
+
} else {
|
|
613
|
+
out(USAGE)
|
|
614
|
+
}
|
|
615
|
+
return EXIT_OK
|
|
616
|
+
}
|
|
617
|
+
if (command === '--version') return commandVersion({})
|
|
618
|
+
if (!Object.prototype.hasOwnProperty.call(COMMANDS, command)) {
|
|
619
|
+
err(`factoidal: unknown command "${command}"`)
|
|
620
|
+
err(USAGE)
|
|
621
|
+
return EXIT_USAGE
|
|
622
|
+
}
|
|
623
|
+
let parsed
|
|
624
|
+
try {
|
|
625
|
+
parsed = parseArguments(argv.slice(1), VALUE_OPTIONS[command])
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (error instanceof UsageError) {
|
|
628
|
+
err(`factoidal ${command}: ${error.message}`)
|
|
629
|
+
return EXIT_USAGE
|
|
630
|
+
}
|
|
631
|
+
throw error
|
|
632
|
+
}
|
|
633
|
+
if (parsed.options.help) {
|
|
634
|
+
out(COMMAND_USAGE[command])
|
|
635
|
+
return EXIT_OK
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
return await COMMANDS[command](parsed.positional, parsed.options)
|
|
639
|
+
} catch (error) {
|
|
640
|
+
if (error instanceof UsageError) {
|
|
641
|
+
err(`factoidal ${command}: ${error.message}`)
|
|
642
|
+
err('')
|
|
643
|
+
err(COMMAND_USAGE[command])
|
|
644
|
+
return EXIT_USAGE
|
|
645
|
+
}
|
|
646
|
+
if (error instanceof StoreHostError) {
|
|
647
|
+
err(`factoidal ${command}: ${error.code}: ${error.message}`)
|
|
648
|
+
return EXIT_FAILURE
|
|
649
|
+
}
|
|
650
|
+
if (error instanceof StoreOperationError) {
|
|
651
|
+
err(`factoidal ${command}: ${error.message}`)
|
|
652
|
+
return EXIT_FAILURE
|
|
653
|
+
}
|
|
654
|
+
throw error
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
exit(await main())
|