@factoidal/core 0.10.0 → 0.11.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/lib/api.mjs CHANGED
@@ -22,66 +22,36 @@
22
22
  // JSDoc note "entry_jsoo.ml's X" below names that lineage. The Lean op of
23
23
  // the same name serves X now (formal/lean4/Wasm/Dispatch.lean lists them).
24
24
 
25
- import { dataFactory, termFromSrj, quadToNQuads } from '../rdfjs.mjs';
25
+ import { dataFactory, termFromSrj, termToJson, quadToJson, _setTextEngine } from '../rdfjs.mjs';
26
26
  import { Dataset, INTERNAL, checkOptions, makeRegistry, WRITER_TAGS } from './dataset.mjs';
27
27
  import { version as packageVersion } from './version.mjs';
28
28
  import { openGeneration, readRegion, manifestKeys, hexOfBytes } from './store.mjs';
29
29
  import { fullEngineMessage } from './lean-entry.mjs';
30
30
  import { memoryDeltaLogStore } from './delta-log-hosts.mjs';
31
31
 
32
- const DATA_FORMAT_EXT = {
33
- turtle: 'ttl',
34
- ttl: 'ttl',
35
- ntriples: 'nt',
36
- nt: 'nt',
37
- nquads: 'nq',
38
- nq: 'nq',
39
- trig: 'trig',
40
- rdfxml: 'rdf',
41
- 'rdf-xml': 'rdf',
42
- rdf: 'rdf',
43
- jsonld: 'jsonld',
44
- 'json-ld': 'jsonld',
45
- // RDF 1.2 opt-in: these select the engine's RDF 1.2 parsers (triple
46
- // terms <<( s p o )>>, ~ reifiers, {| |} annotations, VERSION,
47
- // directional literals "x"@lang--dir; Wasm/Ops/Parse.lean's
48
- // `Mode.rdf12`); the plain names above stay RDF 1.1.
49
- turtle12: 'ttl12',
50
- ttl12: 'ttl12',
51
- ntriples12: 'nt12',
52
- nt12: 'nt12',
53
- nquads12: 'nq12',
54
- nq12: 'nq12',
55
- trig12: 'trig12',
56
- };
57
-
58
- // Canonical format tag for the engine's parse ops (parseToDatasetJson,
59
- // datasetOpen); the *12 tags select the RDF 1.2 mode.
60
- const DATA_FORMAT_TAG = {
61
- ttl: 'turtle', nt: 'ntriples', nq: 'nquads', trig: 'trig', rdf: 'rdfxml',
62
- jsonld: 'jsonld',
63
- ttl12: 'turtle12', nt12: 'ntriples12', nq12: 'nquads12', trig12: 'trig12',
64
- };
65
-
66
- const ENTAIL_VALUES = new Set(['none', 'RDFS', 'OWL-RL', 'x-rdfscore', 'x-rdfsplus']);
67
-
68
- // queryRaw's options (op queryRaw, Wasm/Ops/QueryExtras.lean).
69
- const RAW_OUTPUT_FORMATS = new Set(['json', 'csv', 'tsv', 'xml', 'table', 'ntriples']);
70
- const RAW_DATA_FORMATS = {
71
- turtle: 'turtle', ttl: 'turtle', ntriples: 'ntriples', nt: 'ntriples',
72
- nquads: 'nquads', nq: 'nquads', trig: 'trig', rdfxml: 'rdfxml',
73
- 'rdf-xml': 'rdfxml', rdf: 'rdfxml',
74
- };
32
+ // The format names, regimes and query outputs are the engine's tables (op
33
+ // engineTables, formal/lean4/Wasm/Ops/Handles.lean), read once per engine:
34
+ // no host copy (https://github.com/danbri/factoidal/issues/894 item 5).
75
35
 
76
- function extForFormat(fmt) {
36
+ // The parse tag a format name selects, from the engine's table.
37
+ function parseTagOf(tables, fmt) {
77
38
  const key = String(fmt || 'turtle').toLowerCase();
78
- if (!(key in DATA_FORMAT_EXT)) {
39
+ const tag = tables.parseFormats[key];
40
+ if (tag === undefined) {
79
41
  throw new TypeError(
80
- `Unknown format '${fmt}'. Expected one of: ` +
81
- Object.keys(DATA_FORMAT_EXT).join(', ')
82
- );
42
+ `Unknown format '${fmt}'. Expected one of: ` + Object.keys(tables.parseFormats).join(', '));
83
43
  }
84
- return DATA_FORMAT_EXT[key];
44
+ return tag;
45
+ }
46
+
47
+ // The engine's refusal of a regime name (table refusedRegimePrefixes), or null.
48
+ function iklRefusalOf(tables, who, regime) {
49
+ if (regime == null) return null;
50
+ const r = String(regime);
51
+ for (const [prefix, message] of Object.entries(tables.refusedRegimePrefixes)) {
52
+ if (r.toLowerCase().startsWith(prefix)) return message.replace('WHO', who).replace('REGIME', r);
53
+ }
54
+ return null;
85
55
  }
86
56
 
87
57
  // Where a function takes an options object, a bare string names the
@@ -114,16 +84,13 @@ function missingOpError(what, e, op) {
114
84
  'current build.');
115
85
  }
116
86
 
117
- // Detect the query form for result-shape dispatch. IRIs and comments
118
- // are blanked first so '#' inside a PREFIX IRI cannot hide the verb.
119
- // The engine's SPARQL parser remains the authority — this only picks
120
- // the output shape.
121
- function sniffQueryForm(sparql) {
122
- const cleaned = String(sparql)
123
- .replace(/<[^>]*>/g, ' ')
124
- .replace(/#[^\n]*/g, ' ');
125
- const m = cleaned.match(/\b(select|ask|construct|describe)\b/i);
126
- return m ? m[1].toLowerCase() : 'select';
87
+ // The query form, from the engine's SPARQL parser (op queryForm). A query
88
+ // the parser rejects answers 'select'; the query op then reports the parse
89
+ // error itself.
90
+ function queryFormOf(e, sparql) {
91
+ if (typeof e.queryForm !== 'function') return 'select';
92
+ const r = JSON.parse(e.queryForm(String(sparql)));
93
+ return r.ok ? r.kind : 'select';
127
94
  }
128
95
 
129
96
  // Bytes for an op that reads the byte region (queryHdt, storeOpen): a
@@ -585,15 +552,16 @@ function buildApi(driver) {
585
552
  return out;
586
553
  }
587
554
 
588
- // A ShEx focus/shape-label argument is either a raw string (an IRI,
589
- // or "_:label" for a blank node -- the entry ABI's own convention)
590
- // or an RDF/JS term (NamedNode/BlankNode); accepting both lets a
591
- // caller pass a term straight out of a query() binding.
555
+ // A ShEx focus/shape-label argument is a string in N-Triples term syntax
556
+ // ("<http://...>" or "_:label"), which the engine reads with its one
557
+ // term-string reader, or an RDF/JS term (NamedNode/BlankNode); accepting
558
+ // both lets a caller pass a term straight out of a query() binding.
592
559
  function shexTermToString(t, who) {
593
- if (typeof t === 'string') return t;
560
+ if (typeof t === 'string') return JSON.stringify(termToJson(t));
594
561
  if (t && typeof t.termType === 'string') {
595
- if (t.termType === 'BlankNode') return `_:${t.value}`;
596
- if (t.termType === 'NamedNode') return t.value;
562
+ if (t.termType === 'BlankNode' || t.termType === 'NamedNode') {
563
+ return JSON.stringify(termToJson(t)); // the engine writes the node
564
+ }
597
565
  throw new TypeError(`${who}: expected an IRI or blank node term`);
598
566
  }
599
567
  throw new TypeError(`${who}: expected a string or an RDF/JS term`);
@@ -613,9 +581,19 @@ function buildApi(driver) {
613
581
  async function ctx() {
614
582
  const e = await entry();
615
583
  if (ctxCache && ctxCache.e === e) return ctxCache;
584
+ let tablesCache = null;
616
585
  const c = {
617
586
  e,
618
587
  registry,
588
+ tables() {
589
+ if (!tablesCache) {
590
+ requireEntryFn(e, 'engineTables', 'format and regime tables');
591
+ tablesCache = entryResult(e.engineTables(), 'engineTables').tables;
592
+ for (const k of Object.keys(WRITER_TAGS)) delete WRITER_TAGS[k];
593
+ Object.assign(WRITER_TAGS, tablesCache.writerFormats);
594
+ }
595
+ return tablesCache;
596
+ },
619
597
  missing: (what, op) => missingOpError(what, e, op),
620
598
  wrap(r, prefixes, extra) {
621
599
  const { ok: _ok, handle, count, prefixes: own, ...info } = r;
@@ -638,16 +616,34 @@ function buildApi(driver) {
638
616
  async closure(ds, regime) { return closureOnHandle(c, ds, regime); },
639
617
  };
640
618
  ctxCache = c;
619
+ installTextEngine(e);
641
620
  return c;
642
621
  }
643
622
 
623
+ // rdfjs.mjs's N-Quads text functions (nquadsToQuads, quadsToNQuads,
624
+ // termToNQuads, DatasetCore.fromNQuads/toNQuads) call these engine ops.
625
+ function installTextEngine(e) {
626
+ if (typeof e.parseQuadsJson !== 'function') return;
627
+ _setTextEngine({
628
+ parseQuads(text) {
629
+ return parseResult(e.parseQuadsJson(text, 'nquads12', ''), 'nquadsToQuads', text).quads;
630
+ },
631
+ writeQuads(quadsJson) {
632
+ return entryResult(e.quadsToNQuads(JSON.stringify(quadsJson)), 'quadsToNQuads').nquads;
633
+ },
634
+ termTokens(termsJson) {
635
+ return entryResult(e.termsToNTriples(JSON.stringify(termsJson)), 'termToNQuads').tokens;
636
+ },
637
+ });
638
+ }
639
+
644
640
  // The parse tag of `format`, or the engine's inference when it is omitted.
645
641
  function formatTagOf(c, text, format, who) {
646
642
  if (format === undefined || format === null || format === '') {
647
643
  requireEntryFn(c.e, 'detectFormat', `${who} (format inference)`);
648
644
  return { tag: entryResult(c.e.detectFormat(text), who).format, inferred: true };
649
645
  }
650
- return { tag: DATA_FORMAT_TAG[extForFormat(format)], inferred: false };
646
+ return { tag: parseTagOf(c.tables(), format), inferred: false };
651
647
  }
652
648
 
653
649
  // Open `text` as a Dataset. A parse error names the format and says
@@ -714,17 +710,11 @@ function buildApi(driver) {
714
710
  async function queryDatasetHandle(c, ds, sparql, opts) {
715
711
  if (typeof sparql !== 'string') throw new TypeError('query: sparql must be a string');
716
712
  const entail = opts.entail || 'none';
717
- if (/^x-ikl/i.test(entail)) {
718
- throw new TypeError(
719
- `query: entail '${entail}' is not implemented. The x-ikl-* ` +
720
- "entailment regimes are the owner's design " +
721
- "(danbri/factoidal#581); the Lean engine's dispatch for them " +
722
- 'was deleted on 2026-08-26 as collateral of the IKL-to-RDF ' +
723
- 'projection purge (danbri/factoidal#626). This is not a ' +
724
- 'policy exclusion.');
725
- }
726
- if (!ENTAIL_VALUES.has(entail)) {
727
- throw new TypeError(`query: entail must be one of ${[...ENTAIL_VALUES].join(', ')}`);
713
+ const tables = c.tables();
714
+ const ikl = iklRefusalOf(tables, 'query', entail);
715
+ if (ikl) throw new TypeError(ikl);
716
+ if (!tables.entailRegimes.includes(entail)) {
717
+ throw new TypeError(`query: entail must be one of ${tables.entailRegimes.join(', ')}`);
728
718
  }
729
719
  if (opts.version !== undefined && !['1.1', '1.2'].includes(String(opts.version))) {
730
720
  throw new TypeError("query: version must be '1.1' or '1.2'");
@@ -739,10 +729,10 @@ function buildApi(driver) {
739
729
  "query: entail/output and SPARQL 1.2 cannot be combined; that path reads SPARQL 1.1");
740
730
  }
741
731
  const output = opts.output || 'json';
742
- if (!RAW_OUTPUT_FORMATS.has(output)) {
743
- throw new TypeError(`query: output must be one of ${[...RAW_OUTPUT_FORMATS].join(', ')}`);
732
+ if (!tables.queryOutputs.includes(output)) {
733
+ throw new TypeError(`query: output must be one of ${tables.queryOutputs.join(', ')}`);
744
734
  }
745
- const form = sniffQueryForm(sparql);
735
+ const form = queryFormOf(e, sparql);
746
736
  const graphForm = form === 'construct' || form === 'describe';
747
737
  requireEntryFn(e, 'datasetQueryRaw', `query {entail/output}`);
748
738
  if (opts.output === undefined) {
@@ -791,14 +781,8 @@ function buildApi(driver) {
791
781
  c.e.datasetShexValidate(ds.handle, schema, focusStr, shapeStr), 'shexValidate').verdict;
792
782
  }
793
783
 
794
- const CLOSURE_REGIMES = {
795
- rdfs: 'rdfs', 'owl-rl': 'owl-rl', owlrl: 'owl-rl', owl_rl: 'owl-rl',
796
- 'rho-df': 'rho-df', rhodf: 'rho-df', 'core-rdfs': 'rho-df',
797
- 'rdfs-plus': 'rdfs-plus', rdfsplus: 'rdfs-plus',
798
- };
799
-
800
784
  async function closureOnHandle(c, ds, regime) {
801
- const r0 = CLOSURE_REGIMES[String(regime || '').toLowerCase()];
785
+ const r0 = c.tables().closureAliases[String(regime || '').toLowerCase()];
802
786
  if (!r0) {
803
787
  throw new TypeError(
804
788
  `closure: regime must be one of rdfs, owl-rl, rho-df, rdfs-plus (got '${regime}')`);
@@ -892,15 +876,11 @@ function buildApi(driver) {
892
876
  if (typeof sparql !== 'string') {
893
877
  throw new TypeError('queryHdt: sparql must be a string');
894
878
  }
895
- const form = sniffQueryForm(sparql);
896
- if (form === 'construct' || form === 'describe') {
897
- throw new TypeError(
898
- `queryHdt: ${form.toUpperCase()} is not supported over an HDT file (SELECT/ASK only)`);
899
- }
900
879
  const e = await entry();
901
880
  requireEntryFn(e, 'queryHdt', 'queryHdt');
902
881
  const r = entryResult(e.queryHdt(bytesToUint8Array(hdtBytes, 'queryHdt'), sparql), 'queryHdt');
903
882
  if (r.kind === 'ask') return r.boolean;
883
+ if (r.kind === 'construct' || r.kind === 'describe') return (await ctx()).fromNQuads(r.nquads, {});
904
884
  return bindingsFromSrj(r.srj);
905
885
  }
906
886
 
@@ -942,9 +922,10 @@ function buildApi(driver) {
942
922
  ['format', 'version', 'prefixes', 'literalShorthand', 'useNativeTypes',
943
923
  'useRdfType', 'rdfDirection', 'inputFormat', 'baseIRI']);
944
924
  const { inputFormat, baseIRI, ...writeOpts } = opts;
945
- if (writeOpts.format !== undefined && !WRITER_TAGS[String(writeOpts.format).toLowerCase()]) {
925
+ const writerTags = (await ctx()).tables().writerFormats;
926
+ if (writeOpts.format !== undefined && !writerTags[String(writeOpts.format).toLowerCase()]) {
946
927
  throw new TypeError(`serialize: unknown format '${writeOpts.format}'. Known formats: ` +
947
- [...new Set(Object.values(WRITER_TAGS))].join(', '));
928
+ [...new Set(Object.values(writerTags))].join(', '));
948
929
  }
949
930
  return withDataset(data, { format: inputFormat, baseIRI }, 'serialize',
950
931
  (_c, ds) => ds.serialize(writeOpts));
@@ -965,24 +946,24 @@ function buildApi(driver) {
965
946
  * The named graphs of a Dataset, each as a Dataset (default graph
966
947
  * excluded, first-seen order).
967
948
  * @param {Dataset} dataset
968
- * @returns {Array<[iri: string, graph: Dataset]>}
949
+ * @returns {Array<[name: NamedNode|BlankNode, graph: Dataset]>}
969
950
  */
970
951
  function graphs(dataset) {
971
952
  if (!(dataset instanceof Dataset)) throw new TypeError('graphs: expected a Dataset');
972
- const seen = new Map();
973
- for (const q of dataset) {
974
- const g = q.graph;
975
- if (!g || g.termType === 'DefaultGraph') continue;
976
- if (!seen.has(g.value)) seen.set(g.value, g);
977
- }
978
- const out = [];
979
- for (const [iri, gTerm] of seen) out.push([iri, dataset.match(null, null, null, gTerm)]);
980
- return out;
953
+ dataset.size; // throws the disposed error
954
+ const c = dataset._ctx;
955
+ requireEntryFn(c.e, 'datasetGraphNames', 'graphs');
956
+ const names = entryResult(c.e.datasetGraphNames(dataset.handle), 'graphs').graphs;
957
+ return names.map((g) => {
958
+ const term = termFromSrj(g);
959
+ return [term, dataset.match(null, null, null, term)];
960
+ });
981
961
  }
982
962
 
983
963
  /**
984
964
  * RDFC-1.0 canonical N-Quads of one graph: every quad's graph name is
985
- * dropped first. Text, not a digest.
965
+ * dropped first (op datasetGraphAsDefault with the union of the graphs).
966
+ * Text, not a digest.
986
967
  * @param {Dataset} datasetOrGraph
987
968
  * @returns {Promise<string>}
988
969
  */
@@ -990,9 +971,59 @@ function buildApi(driver) {
990
971
  if (!(datasetOrGraph instanceof Dataset)) {
991
972
  throw new TypeError('canonicalHash: expected a Dataset (e.g. one entry of graphs())');
992
973
  }
993
- const asDefault = datasetOrGraph.toArray()
994
- .map((q) => quadToNQuads(dataFactory.quad(q.subject, q.predicate, q.object))).join('');
995
- return canonicalize(asDefault, { format: 'nquads' });
974
+ datasetOrGraph.size; // throws the disposed error
975
+ const c = datasetOrGraph._ctx;
976
+ requireEntryFn(c.e, 'datasetGraphAsDefault', 'canonicalHash');
977
+ const merged = c.wrap(entryResult(c.e.datasetGraphAsDefault(datasetOrGraph.handle,
978
+ JSON.stringify({ type: 'union' })), 'canonicalHash'), {});
979
+ try {
980
+ return merged.toString('rdfc');
981
+ } finally {
982
+ merged.dispose();
983
+ }
984
+ }
985
+
986
+ /**
987
+ * Whether two datasets are isomorphic (RDF 1.1 Concepts 3.6 and 4; blank
988
+ * nodes inside RDF 1.2 triple terms included): the engine's
989
+ * Dataset.isomorphicOutcome (op datasetIsomorphic). Rejects when the
990
+ * blank-node search budget is exceeded.
991
+ * @param {Dataset|string|Array} a
992
+ * @param {Dataset|string|Array} b
993
+ * @param {{format?: string, baseIRI?: string}} [options] read text arguments
994
+ * @returns {Promise<boolean>}
995
+ */
996
+ async function isomorphic(a, b, options) {
997
+ const opts = checkOptions('isomorphic', optionsOf('isomorphic', options), ['format', 'baseIRI']);
998
+ return withDataset(a, opts, 'isomorphic', (c, da) =>
999
+ withDataset(b, opts, 'isomorphic', (_c, db) => {
1000
+ requireEntryFn(c.e, 'datasetIsomorphic', 'isomorphic');
1001
+ return entryResult(c.e.datasetIsomorphic(da.handle, db.handle), 'isomorphic').isomorphic;
1002
+ }));
1003
+ }
1004
+
1005
+ /**
1006
+ * A Dataset holding RDF/JS quads (op datasetFromQuadsJson). Each term is
1007
+ * sent as its components; the engine reads no text.
1008
+ * @param {Iterable<object>} quads
1009
+ * @returns {Promise<Dataset>}
1010
+ */
1011
+ async function fromQuads(quads) {
1012
+ const c = await ctx();
1013
+ requireEntryFn(c.e, 'datasetFromQuadsJson', 'fromQuads');
1014
+ const arr = [];
1015
+ for (const q of quads) arr.push(quadToJson(q));
1016
+ return c.wrap(entryResult(c.e.datasetFromQuadsJson(JSON.stringify(arr)), 'fromQuads'), {});
1017
+ }
1018
+
1019
+ /** Load the engine; afterwards the N-Quads text functions of `@factoidal/core/rdfjs` work. */
1020
+ async function ready() {
1021
+ await ctx();
1022
+ }
1023
+
1024
+ /** The engine's name tables (formats, regimes, query outputs, pack syntaxes). */
1025
+ async function engineTables() {
1026
+ return (await ctx()).tables();
996
1027
  }
997
1028
 
998
1029
  // A wasm module older than this package can lack an op: `e[fnName]` is
@@ -1051,7 +1082,8 @@ function buildApi(driver) {
1051
1082
  */
1052
1083
  async function owlClosure(data, mode, options) {
1053
1084
  const m = String(mode || '').toLowerCase();
1054
- if (!['rdfs', 'owl-rl', 'owlrl', 'owl_rl'].includes(m)) {
1085
+ const aliases = (await ctx()).tables().closureAliases;
1086
+ if (!['rdfs', 'owl-rl'].includes(aliases[m])) {
1055
1087
  throw new TypeError(`owlClosure: mode must be 'RDFS' or 'OWL-RL' (got '${mode}')`);
1056
1088
  }
1057
1089
  return closure(data, m, options);
@@ -1984,19 +2016,20 @@ function buildApi(driver) {
1984
2016
  const dataFormat = String(opts.dataFormat || 'turtle').toLowerCase();
1985
2017
  const entail = opts.entail || 'none';
1986
2018
  const output = opts.output || 'json';
1987
- if (!(dataFormat in RAW_DATA_FORMATS)) {
2019
+ const tables = (await ctx()).tables();
2020
+ if (!(dataFormat in tables.rawDataFormats)) {
1988
2021
  throw new TypeError(`queryRaw: unknown dataFormat '${opts.dataFormat}'`);
1989
2022
  }
1990
- if (!ENTAIL_VALUES.has(entail)) {
1991
- throw new TypeError(`queryRaw: entail must be one of ${[...ENTAIL_VALUES].join(', ')}`);
2023
+ if (!tables.entailRegimes.includes(entail)) {
2024
+ throw new TypeError(`queryRaw: entail must be one of ${tables.entailRegimes.join(', ')}`);
1992
2025
  }
1993
- if (!RAW_OUTPUT_FORMATS.has(output)) {
1994
- throw new TypeError(`queryRaw: output must be one of ${[...RAW_OUTPUT_FORMATS].join(', ')}`);
2026
+ if (!tables.queryOutputs.includes(output)) {
2027
+ throw new TypeError(`queryRaw: output must be one of ${tables.queryOutputs.join(', ')}`);
1995
2028
  }
1996
2029
  const e = await entry();
1997
2030
  requireEntryFn(e, 'queryRaw', 'queryRaw');
1998
2031
  const r = JSON.parse(e.queryRaw(dataString, queryString,
1999
- JSON.stringify({ dataFormat: RAW_DATA_FORMATS[dataFormat], entail, output })));
2032
+ JSON.stringify({ dataFormat: tables.rawDataFormats[dataFormat], entail, output })));
2000
2033
  if (!r.ok) {
2001
2034
  const err = new Error('SPARQL query failed: ' + r.error);
2002
2035
  err.stderr = String(r.error);
@@ -2280,6 +2313,10 @@ function buildApi(driver) {
2280
2313
  deltaLogDestroy,
2281
2314
  _deltaLogCorruptLastForTest,
2282
2315
  queryRaw,
2316
+ isomorphic,
2317
+ fromQuads,
2318
+ ready,
2319
+ engineTables,
2283
2320
  Dataset,
2284
2321
  DatasetHandle,
2285
2322
  Store,
@@ -2295,6 +2332,6 @@ function buildApi(driver) {
2295
2332
  }
2296
2333
 
2297
2334
  export {
2298
- buildApi, sniffQueryForm, bindingsFromSrj, entryResult, parseResult,
2335
+ buildApi, bindingsFromSrj, entryResult, parseResult,
2299
2336
  utf16OffsetOf, missingOpError, ParseError, DatasetHandle, Store,
2300
2337
  };
package/lib/dataset.mjs CHANGED
@@ -15,21 +15,16 @@
15
15
  // released by a FinalizationRegistry callback. Every method of a disposed
16
16
  // Dataset throws a TypeError that names the method.
17
17
 
18
- import { nquadsLineToQuad, quadToNQuads, termToNQuads, termFromSrj } from '../rdfjs.mjs';
18
+ import { dataFactory, quadFromJson, quadToJson, termToJson, termFromSrj, Literal, NamedNode } from '../rdfjs.mjs';
19
19
 
20
20
  const INTERNAL = Symbol('factoidal.Dataset.internal');
21
21
 
22
- // Writer names (lower case) -> the engine's writer tag. JSON-LD is served
22
+ // Writer names (lower case) -> the engine's writer tag: the engine's table
23
+ // (op engineTables, `writerFormats`), read once per engine. JSON-LD is served
23
24
  // by the op datasetToJsonLd; the SPARQL results formats are query outputs.
24
- const WRITER_TAGS = {
25
- nquads: 'nquads', nq: 'nquads', 'n-quads': 'nquads',
26
- ntriples: 'ntriples', nt: 'ntriples', 'n-triples': 'ntriples',
27
- turtle: 'turtle', ttl: 'turtle',
28
- trig: 'trig',
29
- rdfxml: 'rdfxml', 'rdf-xml': 'rdfxml', rdf: 'rdfxml',
30
- jsonld: 'jsonld', 'json-ld': 'jsonld',
31
- rdfc: 'rdfc', canonical: 'rdfc', 'rdfc-1.0': 'rdfc',
32
- };
25
+ // `WRITER_TAGS` stays exported for callers that read it; it is filled from
26
+ // the engine on the first Dataset.
27
+ const WRITER_TAGS = {};
33
28
 
34
29
  const VERSIONS = { '1.1': '11', '1.2': '12' };
35
30
 
@@ -57,6 +52,84 @@ function envelope(jsonText, what) {
57
52
  const SERIALIZE_OPTIONS = ['format', 'version', 'prefixes', 'literalShorthand',
58
53
  'useNativeTypes', 'useRdfType', 'rdfDirection'];
59
54
 
55
+ // The quads of the compact answer of datasetQuadsJson: each distinct term is
56
+ // built once and shared (TermJson.compactCertified).
57
+ function quadsFromCompact(terms, ixs) {
58
+ const ts = terms.map(termFromSrj);
59
+ const dg = dataFactory.defaultGraph();
60
+ return ixs.map((ix) => dataFactory.quad(ts[ix[0]], ts[ix[1]], ts[ix[2]],
61
+ ix.length > 3 ? ts[ix[3]] : dg));
62
+ }
63
+
64
+ // The region of op datasetQuadsBin, read as the engine wrote it
65
+ // (formal/lean4/L4Factoidal/RDF/QuadsBin.lean: `binSpec` is the layout,
66
+ // `decodeBin` the reader this function follows, `decodeBin_quadsBin` the
67
+ // theorem that the region reads back as exactly the quads). Every number is a
68
+ // u32 little-endian; each term is in the term codec of DLOG version 2, which
69
+ // names the term kind, the datatype IRI, the language tag and the base
70
+ // direction explicitly, so this function copies fields and decides nothing.
71
+ const UTF8 = new TextDecoder('utf-8', { fatal: true });
72
+ const DIRECTION = ['', 'ltr', 'rtl'];
73
+
74
+ function quadsFromRegion(bytes, out) {
75
+ const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
76
+ if (bytes.length < 12 || v.getUint32(0, true) !== 0x31425146) {
77
+ throw new Error('iterate: the engine answered no datasetQuadsBin region');
78
+ }
79
+ const nTerms = v.getUint32(4, true);
80
+ const nQuads = v.getUint32(8, true);
81
+ let p = 12;
82
+ // UTF-8 bytes to a string; a short run of bytes below 0x80 (one UTF-16
83
+ // unit each) is copied without a TextDecoder call.
84
+ const str = () => {
85
+ const n = v.getUint32(p, true);
86
+ const a = p + 4;
87
+ p = a + n;
88
+ if (n <= 64) {
89
+ let i = a;
90
+ while (i < p && bytes[i] < 0x80) i++;
91
+ if (i === p) return String.fromCharCode.apply(null, bytes.subarray(a, p));
92
+ }
93
+ return UTF8.decode(bytes.subarray(a, p));
94
+ };
95
+ const datatypes = new Map();
96
+ const term = () => {
97
+ switch (bytes[p++]) {
98
+ case 0: return new NamedNode(str());
99
+ case 1: return dataFactory.blankNode(str());
100
+ case 2: {
101
+ const lexical = str();
102
+ const dt = str();
103
+ let datatype = datatypes.get(dt);
104
+ if (datatype === undefined) { datatype = new NamedNode(dt); datatypes.set(dt, datatype); }
105
+ const flag = bytes[p++];
106
+ const language = flag === 0 ? '' : str();
107
+ const direction = flag === 2 ? DIRECTION[bytes[p++]] : '';
108
+ return new Literal(lexical, language, datatype, direction);
109
+ }
110
+ case 3: {
111
+ const s = term();
112
+ const pr = term();
113
+ return dataFactory.quad(s, pr, term());
114
+ }
115
+ default: throw new Error('iterate: a term tag outside the DLOG version 2 codec');
116
+ }
117
+ };
118
+ const ts = new Array(nTerms);
119
+ for (let i = 0; i < nTerms; i++) ts[i] = term();
120
+ const dg = dataFactory.defaultGraph();
121
+ for (let i = 0; i < nQuads; i++, p += 16) {
122
+ const g = v.getUint32(p + 12, true);
123
+ out.push(dataFactory.quad(ts[v.getUint32(p, true)], ts[v.getUint32(p + 4, true)],
124
+ ts[v.getUint32(p + 8, true)], g === 0 ? dg : ts[g - 1]));
125
+ }
126
+ return nQuads;
127
+ }
128
+
129
+ // Quads of the first datasetQuadsBin call while iterating: an iteration that
130
+ // stops early reads this many; the rest come in one more call.
131
+ const FIRST_PAGE = 4096;
132
+
60
133
  class Dataset {
61
134
  /** @private Datasets come from parse(), csvwToRdf() and the other producers. */
62
135
  constructor(token, ctx, handle, size, prefixes, extra) {
@@ -214,11 +287,12 @@ class Dataset {
214
287
  _write(opts) {
215
288
  const e = this._ctx.e;
216
289
  const name = String(opts.format || 'nquads').toLowerCase();
217
- const tag = WRITER_TAGS[name];
290
+ const writerTags = this._ctx.tables().writerFormats;
291
+ const tag = writerTags[name];
218
292
  if (!tag) {
219
293
  throw new TypeError(
220
294
  `Dataset.serialize: unknown format '${opts.format}'. Known formats: ` +
221
- [...new Set(Object.values(WRITER_TAGS))].join(', '));
295
+ [...new Set(Object.values(writerTags))].join(', '));
222
296
  }
223
297
  let suffix = '';
224
298
  if (opts.version !== undefined) {
@@ -273,40 +347,58 @@ class Dataset {
273
347
  }
274
348
 
275
349
  /**
276
- * RDF/JS quads, read from the engine's N-Quads text one line at a time.
350
+ * RDF/JS quads, built from the engine's term objects (op datasetQuadsJson),
351
+ * in the line order of toNQuads(). No N-Quads text is read here.
277
352
  * @returns {Iterator<object>}
278
353
  */
279
354
  *[Symbol.iterator]() {
280
355
  this._open('iterate');
281
356
  if (this._quads) { yield* this._quads; return; }
282
- const text = this.toNQuads();
283
- let start = 0;
284
- let lineNo = 0;
285
- while (start < text.length) {
286
- let end = text.indexOf('\n', start);
287
- if (end < 0) end = text.length;
288
- lineNo += 1;
289
- const line = text.slice(start, end);
290
- start = end + 1;
291
- const q = nquadsLineToQuad(line, lineNo);
292
- if (q) yield q;
357
+ const e = this._ctx.e;
358
+ if (typeof e.datasetQuadsBin !== 'function') { yield* this._allQuads(); return; }
359
+ // The engine's region (op datasetQuadsBin) in two calls, a first page and
360
+ // the rest; the whole list is cached when the rest is read.
361
+ const acc = [];
362
+ const first = e.datasetQuadsBin(this._handle, `{"offset":0,"limit":${FIRST_PAGE}}`);
363
+ quadsFromRegion(first.bytes, acc);
364
+ for (let i = 0; i < acc.length; i++) yield acc[i];
365
+ if (acc.length < first.envelope.total) {
366
+ const rest = e.datasetQuadsBin(this._handle, `{"offset":${acc.length}}`);
367
+ const from = acc.length;
368
+ quadsFromRegion(rest.bytes, acc);
369
+ for (let i = from; i < acc.length; i++) yield acc[i];
370
+ }
371
+ if (!this._quads) this._quads = Object.freeze(acc);
372
+ }
373
+
374
+ // Every quad, read once and cached.
375
+ _allQuads() {
376
+ if (!this._quads) {
377
+ const e = this._ctx.e;
378
+ if (typeof e.datasetQuadsBin === 'function') {
379
+ const acc = [];
380
+ quadsFromRegion(e.datasetQuadsBin(this._handle, '').bytes, acc);
381
+ this._quads = Object.freeze(acc);
382
+ } else {
383
+ const r = envelope(e.datasetQuadsJson(this._handle, '{"compact":true}'), 'iterate');
384
+ this._quads = Object.freeze(r.ixs ? quadsFromCompact(r.terms, r.ixs) : r.quads.map((q) => quadFromJson(q)));
385
+ }
293
386
  }
387
+ return this._quads;
294
388
  }
295
389
 
296
390
  /** Every quad as an array. */
297
391
  toArray() {
298
392
  this._open('toArray');
299
- if (!this._quads) this._quads = Object.freeze([...this]);
300
- return this._quads.slice();
393
+ return this._allQuads().slice();
301
394
  }
302
395
 
303
396
  // A quad pattern for the ops datasetMatch and datasetMatchCount: four
304
- // N-Triples term tokens, '' for an unbound position, 'DEFAULT' for the
305
- // default graph.
397
+ // engine term objects, null for an unbound position, {type:'default'} for
398
+ // the default graph.
306
399
  _pattern(subject, predicate, object, graph) {
307
- const tok = (t) => (t == null ? '' : termToNQuads(t));
308
- const g = graph == null ? '' : (graph.termType === 'DefaultGraph' ? 'DEFAULT' : termToNQuads(graph));
309
- return JSON.stringify([tok(subject), tok(predicate), tok(object), g]);
400
+ return JSON.stringify([termToJson(subject), termToJson(predicate), termToJson(object),
401
+ termToJson(graph)]);
310
402
  }
311
403
 
312
404
  /** RDF/JS DatasetCore.has: one indexed lookup in the engine. */
@@ -336,7 +428,8 @@ class Dataset {
336
428
  */
337
429
  add(quad) {
338
430
  this._open('add');
339
- const r = envelope(this._ctx.e.datasetAddQuads(this._handle, quadToNQuads(quad)), 'add');
431
+ const r = envelope(this._ctx.e.datasetAddQuadsJson(this._handle,
432
+ JSON.stringify([quadToJson(quad)])), 'add');
340
433
  this._changed(r.count);
341
434
  return this;
342
435
  }
@@ -344,7 +437,8 @@ class Dataset {
344
437
  /** RDF/JS DatasetCore.delete: remove one quad. */
345
438
  delete(quad) {
346
439
  this._open('delete');
347
- const r = envelope(this._ctx.e.datasetDeleteQuads(this._handle, quadToNQuads(quad)), 'delete');
440
+ const r = envelope(this._ctx.e.datasetDeleteQuadsJson(this._handle,
441
+ JSON.stringify([quadToJson(quad)])), 'delete');
348
442
  this._changed(r.count);
349
443
  return this;
350
444
  }