@undefineds.co/xpod 0.3.18 → 0.3.23
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/config/bun.json +57 -11
- package/config/cloud.json +14 -12
- package/config/local.json +16 -14
- package/config/xpod.json +47 -9
- package/dist/api/matrix/PodMatrixStore.d.ts +4 -7
- package/dist/api/matrix/PodMatrixStore.js +116 -148
- package/dist/api/matrix/PodMatrixStore.js.map +1 -1
- package/dist/api/matrix/types.d.ts +2 -0
- package/dist/api/matrix/types.js.map +1 -1
- package/dist/api/runs/PiAgentRuntimeDriver.d.ts +1 -0
- package/dist/api/runs/PiAgentRuntimeDriver.js +4 -1
- package/dist/api/runs/PiAgentRuntimeDriver.js.map +1 -1
- package/dist/components/components.jsonld +3 -0
- package/dist/components/context.jsonld +71 -32
- package/dist/http/SubgraphSparqlHttpHandler.d.ts +1 -0
- package/dist/http/SubgraphSparqlHttpHandler.js +27 -4
- package/dist/http/SubgraphSparqlHttpHandler.js.map +1 -1
- package/dist/http/SubgraphSparqlHttpHandler.jsonld +4 -0
- package/dist/http/vector/VectorHttpHandler.d.ts +5 -1
- package/dist/http/vector/VectorHttpHandler.js +5 -5
- package/dist/http/vector/VectorHttpHandler.js.map +1 -1
- package/dist/http/vector/VectorHttpHandler.jsonld +40 -28
- package/dist/index.d.ts +5 -2
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/Proxy.d.ts +3 -0
- package/dist/runtime/Proxy.js +31 -7
- package/dist/runtime/Proxy.js.map +1 -1
- package/dist/solidfs/LocalSolidFS.js +31 -124
- package/dist/solidfs/LocalSolidFS.js.map +1 -1
- package/dist/solidfs/SolidFsPathUtils.d.ts +13 -0
- package/dist/solidfs/SolidFsPathUtils.js +114 -0
- package/dist/solidfs/SolidFsPathUtils.js.map +1 -0
- package/dist/solidfs/SolidFsSyncJournal.d.ts +117 -0
- package/dist/solidfs/SolidFsSyncJournal.js +553 -0
- package/dist/solidfs/SolidFsSyncJournal.js.map +1 -0
- package/dist/solidfs/index.d.ts +1 -0
- package/dist/solidfs/index.js +1 -0
- package/dist/solidfs/index.js.map +1 -1
- package/dist/solidfs/types.d.ts +1 -0
- package/dist/solidfs/types.js.map +1 -1
- package/dist/storage/SparqlUpdateResourceStore.js +94 -33
- package/dist/storage/SparqlUpdateResourceStore.js.map +1 -1
- package/dist/storage/accessors/MixDataAccessor.d.ts +22 -5
- package/dist/storage/accessors/MixDataAccessor.js +376 -61
- package/dist/storage/accessors/MixDataAccessor.js.map +1 -1
- package/dist/storage/accessors/MixDataAccessor.jsonld +73 -5
- package/dist/storage/accessors/QuadstoreSparqlDataAccessor.js +32 -10
- package/dist/storage/accessors/QuadstoreSparqlDataAccessor.js.map +1 -1
- package/dist/storage/accessors/QuintStoreSparqlDataAccessor.js +28 -6
- package/dist/storage/accessors/QuintStoreSparqlDataAccessor.js.map +1 -1
- package/dist/storage/accessors/SolidRdfDataAccessor.d.ts +45 -0
- package/dist/storage/accessors/SolidRdfDataAccessor.js +277 -0
- package/dist/storage/accessors/SolidRdfDataAccessor.js.map +1 -0
- package/dist/storage/accessors/SolidRdfDataAccessor.jsonld +161 -0
- package/dist/storage/rdf/Rdf3xIndex.d.ts +122 -0
- package/dist/storage/rdf/Rdf3xIndex.js +2695 -0
- package/dist/storage/rdf/Rdf3xIndex.js.map +1 -0
- package/dist/storage/rdf/Rdf3xIndex.jsonld +528 -0
- package/dist/storage/rdf/Rdf3xSchema.d.ts +20 -0
- package/dist/storage/rdf/Rdf3xSchema.js +65 -0
- package/dist/storage/rdf/Rdf3xSchema.js.map +1 -0
- package/dist/storage/rdf/RdfLocalQueryEngine.d.ts +10 -4
- package/dist/storage/rdf/RdfLocalQueryEngine.js +607 -127
- package/dist/storage/rdf/RdfLocalQueryEngine.js.map +1 -1
- package/dist/storage/rdf/RdfQuadIndex.d.ts +12 -1
- package/dist/storage/rdf/RdfQuadIndex.js +152 -22
- package/dist/storage/rdf/RdfQuadIndex.js.map +1 -1
- package/dist/storage/rdf/RdfQuadIndex.jsonld +36 -4
- package/dist/storage/rdf/RdfSparqlAdapter.d.ts +20 -2
- package/dist/storage/rdf/RdfSparqlAdapter.js +364 -40
- package/dist/storage/rdf/RdfSparqlAdapter.js.map +1 -1
- package/dist/storage/rdf/RdfSparqlAdapter.jsonld +60 -0
- package/dist/storage/rdf/RdfTermDictionary.d.ts +8 -0
- package/dist/storage/rdf/RdfTermDictionary.js +141 -70
- package/dist/storage/rdf/RdfTermDictionary.js.map +1 -1
- package/dist/storage/rdf/RdfTermDictionary.jsonld +24 -0
- package/dist/storage/rdf/RdfTextIndex.js +10 -3
- package/dist/storage/rdf/RdfTextIndex.js.map +1 -1
- package/dist/storage/rdf/SolidRdfEngine.d.ts +15 -6
- package/dist/storage/rdf/SolidRdfEngine.js +218 -25
- package/dist/storage/rdf/SolidRdfEngine.js.map +1 -1
- package/dist/storage/rdf/SolidRdfEngine.jsonld +70 -7
- package/dist/storage/rdf/SolidRdfSparqlEngine.d.ts +11 -7
- package/dist/storage/rdf/SolidRdfSparqlEngine.js +60 -47
- package/dist/storage/rdf/SolidRdfSparqlEngine.js.map +1 -1
- package/dist/storage/rdf/SolidRdfSparqlEngine.jsonld +9 -5
- package/dist/storage/rdf/index.d.ts +2 -2
- package/dist/storage/rdf/index.js +3 -3
- package/dist/storage/rdf/index.js.map +1 -1
- package/dist/storage/rdf/models-benchmark.d.ts +12 -1
- package/dist/storage/rdf/models-benchmark.js +549 -32
- package/dist/storage/rdf/models-benchmark.js.map +1 -1
- package/dist/storage/rdf/types.d.ts +81 -7
- package/dist/storage/rdf/types.js.map +1 -1
- package/dist/storage/sparql/CompatibilitySparqlEngine.d.ts +36 -0
- package/dist/storage/sparql/CompatibilitySparqlEngine.js +96 -0
- package/dist/storage/sparql/CompatibilitySparqlEngine.js.map +1 -0
- package/dist/storage/sparql/CompatibilitySparqlEngine.jsonld +123 -0
- package/dist/storage/sparql/CompatibilitySparqlEngineImpl.d.ts +35 -0
- package/dist/storage/sparql/CompatibilitySparqlEngineImpl.js +112 -0
- package/dist/storage/sparql/CompatibilitySparqlEngineImpl.js.map +1 -0
- package/dist/storage/sparql/SubgraphQueryEngine.d.ts +1 -36
- package/dist/storage/sparql/SubgraphQueryEngine.js +2 -115
- package/dist/storage/sparql/SubgraphQueryEngine.js.map +1 -1
- package/dist/storage/sparql/SubgraphQueryEngine.jsonld +1 -124
- package/dist/terminal/AclPermissionService.d.ts +2 -1
- package/dist/terminal/AclPermissionService.js +26 -3
- package/dist/terminal/AclPermissionService.js.map +1 -1
- package/dist/terminal/TerminalSessionManager.js +25 -3
- package/dist/terminal/TerminalSessionManager.js.map +1 -1
- package/package.json +1 -1
- package/dist/storage/rdf/Rdf3xTripleIndex.d.ts +0 -55
- package/dist/storage/rdf/Rdf3xTripleIndex.js +0 -1235
- package/dist/storage/rdf/Rdf3xTripleIndex.js.map +0 -1
|
@@ -0,0 +1,2695 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Rdf3xIndex = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const n3_1 = require("n3");
|
|
7
|
+
const SqliteRuntime_1 = require("../SqliteRuntime");
|
|
8
|
+
const RdfTermDictionary_1 = require("./RdfTermDictionary");
|
|
9
|
+
const RdfTermSemantics_1 = require("./RdfTermSemantics");
|
|
10
|
+
const Rdf3xSchema_1 = require("./Rdf3xSchema");
|
|
11
|
+
const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
|
|
12
|
+
const XSD_DECIMAL = 'http://www.w3.org/2001/XMLSchema#decimal';
|
|
13
|
+
const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
|
|
14
|
+
const OBJECT_RANGE_KINDS = ['iri', 'literal', 'blank'];
|
|
15
|
+
const RDF3X_INDEX_SCHEMA_VERSION = 1;
|
|
16
|
+
const TERM_COLUMN = {
|
|
17
|
+
subject: 'subject_id',
|
|
18
|
+
predicate: 'predicate_id',
|
|
19
|
+
object: 'object_id',
|
|
20
|
+
};
|
|
21
|
+
const ORDER_COLUMN = {
|
|
22
|
+
graph: 'graph_id',
|
|
23
|
+
...TERM_COLUMN,
|
|
24
|
+
};
|
|
25
|
+
const PATTERN_COLUMNS = {
|
|
26
|
+
graph: 'graph_id',
|
|
27
|
+
...TERM_COLUMN,
|
|
28
|
+
};
|
|
29
|
+
const TERM_KEYS = ['subject', 'predicate', 'object'];
|
|
30
|
+
const RDF_FACTS_TABLE = 'rdf_quads';
|
|
31
|
+
const PERMUTATIONS = [
|
|
32
|
+
{ name: 'SPO', indexName: 'rdf_quads_spog', columns: ['subject_id', 'predicate_id', 'object_id'] },
|
|
33
|
+
{ name: 'SOP', indexName: 'rdf_quads_sopg', columns: ['subject_id', 'object_id', 'predicate_id'] },
|
|
34
|
+
{ name: 'PSO', indexName: 'rdf_quads_psog', columns: ['predicate_id', 'subject_id', 'object_id'] },
|
|
35
|
+
{ name: 'POS', indexName: 'rdf_quads_posg', columns: ['predicate_id', 'object_id', 'subject_id'] },
|
|
36
|
+
{ name: 'OSP', indexName: 'rdf_quads_ospg', columns: ['object_id', 'subject_id', 'predicate_id'] },
|
|
37
|
+
{ name: 'OPS', indexName: 'rdf_quads_opsg', columns: ['object_id', 'predicate_id', 'subject_id'] },
|
|
38
|
+
];
|
|
39
|
+
const PAIR_PROJECTIONS = [
|
|
40
|
+
{ name: 'SP', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.SP, columns: ['subject_id', 'predicate_id'], remainder: 'object_id' },
|
|
41
|
+
{ name: 'SO', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.SO, columns: ['subject_id', 'object_id'], remainder: 'predicate_id' },
|
|
42
|
+
{ name: 'PS', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.PS, columns: ['predicate_id', 'subject_id'], remainder: 'object_id' },
|
|
43
|
+
{ name: 'PO', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.PO, columns: ['predicate_id', 'object_id'], remainder: 'subject_id' },
|
|
44
|
+
{ name: 'OS', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.OS, columns: ['object_id', 'subject_id'], remainder: 'predicate_id' },
|
|
45
|
+
{ name: 'OP', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.OP, columns: ['object_id', 'predicate_id'], remainder: 'subject_id' },
|
|
46
|
+
];
|
|
47
|
+
const TERM_PROJECTIONS = [
|
|
48
|
+
{ name: 'S', table: Rdf3xSchema_1.RDF3X_TERM_PROJECTION_TABLE_BY_NAME.S, column: 'subject_id' },
|
|
49
|
+
{ name: 'P', table: Rdf3xSchema_1.RDF3X_TERM_PROJECTION_TABLE_BY_NAME.P, column: 'predicate_id' },
|
|
50
|
+
{ name: 'O', table: Rdf3xSchema_1.RDF3X_TERM_PROJECTION_TABLE_BY_NAME.O, column: 'object_id' },
|
|
51
|
+
];
|
|
52
|
+
const GRAPH_PROJECTION_TABLE = Rdf3xSchema_1.RDF3X_GRAPH_PROJECTION_TABLE;
|
|
53
|
+
class Rdf3xIndex {
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.options = options;
|
|
56
|
+
this.sqliteRuntime = (0, SqliteRuntime_1.createSqliteRuntime)();
|
|
57
|
+
this.db = null;
|
|
58
|
+
this.dictionary = null;
|
|
59
|
+
}
|
|
60
|
+
open() {
|
|
61
|
+
if (this.db) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (this.options.path !== ':memory:') {
|
|
65
|
+
const dir = (0, node_path_1.dirname)(this.options.path);
|
|
66
|
+
if (!(0, node_fs_1.existsSync)(dir)) {
|
|
67
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.db = this.sqliteRuntime.openDatabase(this.options.path);
|
|
71
|
+
this.dictionary = new RdfTermDictionary_1.RdfTermDictionary(this.db);
|
|
72
|
+
this.dictionary.initialize();
|
|
73
|
+
this.initializeSchema();
|
|
74
|
+
}
|
|
75
|
+
close() {
|
|
76
|
+
this.db?.close();
|
|
77
|
+
this.db = null;
|
|
78
|
+
this.dictionary = null;
|
|
79
|
+
}
|
|
80
|
+
clear() {
|
|
81
|
+
this.clearRdf3xTables();
|
|
82
|
+
this.setFactsDataVersion(0);
|
|
83
|
+
}
|
|
84
|
+
rebuildFromCurrentQuads() {
|
|
85
|
+
const start = Date.now();
|
|
86
|
+
const db = this.requireDb();
|
|
87
|
+
const factsDataVersion = this.currentFactsDataVersion();
|
|
88
|
+
const scannedQuads = db.prepare('SELECT COUNT(*) AS count FROM rdf_quads').get()?.count ?? 0;
|
|
89
|
+
db.transaction(() => {
|
|
90
|
+
this.clearRdf3xTables();
|
|
91
|
+
for (const projection of PAIR_PROJECTIONS) {
|
|
92
|
+
this.rebuildPairProjection(projection);
|
|
93
|
+
}
|
|
94
|
+
for (const projection of TERM_PROJECTIONS) {
|
|
95
|
+
this.rebuildTermProjection(projection);
|
|
96
|
+
}
|
|
97
|
+
this.rebuildGraphProjection();
|
|
98
|
+
this.setFactsDataVersion(factsDataVersion);
|
|
99
|
+
})();
|
|
100
|
+
const stats = this.stats();
|
|
101
|
+
return {
|
|
102
|
+
scannedQuads,
|
|
103
|
+
uniqueTriples: stats.uniqueTriples,
|
|
104
|
+
memberships: stats.membershipCount,
|
|
105
|
+
projectionRows: pairProjectionRowTotal(stats.pairProjectionRows) + termProjectionRowTotal(stats.termProjectionRows),
|
|
106
|
+
factsDataVersion,
|
|
107
|
+
durationMs: Date.now() - start,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
factsDataVersion() {
|
|
111
|
+
const row = this.requireDb()
|
|
112
|
+
.prepare("SELECT value FROM rdf3x_metadata WHERE key = 'facts_data_version'")
|
|
113
|
+
.get();
|
|
114
|
+
return Number(row?.value ?? 0) || 0;
|
|
115
|
+
}
|
|
116
|
+
isSyncedWithCurrentQuads() {
|
|
117
|
+
return this.factsDataVersion() === this.currentFactsDataVersion();
|
|
118
|
+
}
|
|
119
|
+
scan(pattern, options) {
|
|
120
|
+
return this.scanInternal(pattern, options);
|
|
121
|
+
}
|
|
122
|
+
scanWithTupleConstraints(pattern, tupleValues, options) {
|
|
123
|
+
return this.scanInternal(pattern, options, tupleValues);
|
|
124
|
+
}
|
|
125
|
+
countDistinct(pattern, distinctKey) {
|
|
126
|
+
const start = Date.now();
|
|
127
|
+
const resolved = this.resolvePattern(pattern);
|
|
128
|
+
if (resolved.unresolved) {
|
|
129
|
+
return {
|
|
130
|
+
count: 0,
|
|
131
|
+
metrics: this.metrics('none', 0, 0, start, [`unresolved ${resolved.unresolved}`]),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const useMembershipSource = shouldUseMembershipSource(resolved);
|
|
135
|
+
const permutation = this.choosePermutation(resolved.ids, {
|
|
136
|
+
idSets: resolved.idSets,
|
|
137
|
+
objectRange: Boolean(resolved.objectRange),
|
|
138
|
+
termFilters: resolved.termFilters,
|
|
139
|
+
});
|
|
140
|
+
const compiled = useMembershipSource
|
|
141
|
+
? this.compileMembershipDistinctCountSql(resolved, distinctKey)
|
|
142
|
+
: this.compileDistinctCountSql(permutation, resolved, distinctKey);
|
|
143
|
+
const count = this.requireDb()
|
|
144
|
+
.prepare(compiled.sql)
|
|
145
|
+
.get(...compiled.params)?.count ?? 0;
|
|
146
|
+
return {
|
|
147
|
+
count,
|
|
148
|
+
metrics: this.metrics(useMembershipSource ? 'source-membership' : permutation.name, count, 1, start, [
|
|
149
|
+
...(useMembershipSource ? [] : [`Rdf3xPermutationScan(${permutation.name})`]),
|
|
150
|
+
...compiled.queryPlan,
|
|
151
|
+
compiled.sql,
|
|
152
|
+
]),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
scanInternal(pattern, options, tupleValues) {
|
|
156
|
+
const start = Date.now();
|
|
157
|
+
const resolved = this.resolvePattern(pattern);
|
|
158
|
+
if (resolved.unresolved) {
|
|
159
|
+
return {
|
|
160
|
+
quads: [],
|
|
161
|
+
metrics: this.metrics('none', 0, 0, start, [`unresolved ${resolved.unresolved}`]),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const useMembershipSource = shouldUseMembershipSource(resolved);
|
|
165
|
+
const permutation = this.choosePermutation(resolved.ids, {
|
|
166
|
+
idSets: resolved.idSets,
|
|
167
|
+
objectRange: Boolean(resolved.objectRange),
|
|
168
|
+
termFilters: resolved.termFilters,
|
|
169
|
+
});
|
|
170
|
+
const compiled = useMembershipSource
|
|
171
|
+
? this.compileMembershipScanSql(resolved, options, tupleValues)
|
|
172
|
+
: this.compileScanSql(permutation, resolved, options, tupleValues);
|
|
173
|
+
const matchedRows = this.requireDb()
|
|
174
|
+
.prepare(compiled.countSql)
|
|
175
|
+
.get(...compiled.countParams)?.count ?? 0;
|
|
176
|
+
const rows = this.requireDb().prepare(compiled.sql).all(...compiled.params);
|
|
177
|
+
return {
|
|
178
|
+
quads: this.rowsToQuads(rows),
|
|
179
|
+
metrics: this.metrics(useMembershipSource ? 'source-membership' : permutation.name, matchedRows, rows.length, start, [
|
|
180
|
+
...(useMembershipSource ? [] : [`Rdf3xPermutationScan(${permutation.name})`]),
|
|
181
|
+
...compiled.queryPlan,
|
|
182
|
+
compiled.sql,
|
|
183
|
+
]),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
compileDistinctCountSql(permutation, resolved, distinctKey) {
|
|
187
|
+
const conditions = [];
|
|
188
|
+
const params = [];
|
|
189
|
+
const queryPlan = [`Permutation(${permutation.name})`];
|
|
190
|
+
const ids = resolved.ids;
|
|
191
|
+
for (const key of TERM_KEYS) {
|
|
192
|
+
const id = ids[key];
|
|
193
|
+
if (id === undefined) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
conditions.push(`idx.${TERM_COLUMN[key]} = ?`);
|
|
197
|
+
params.push(id);
|
|
198
|
+
}
|
|
199
|
+
this.appendResolvedIdSetConditions(resolved, TERM_KEYS, (key) => `idx.${TERM_COLUMN[key]}`, conditions, params, queryPlan);
|
|
200
|
+
this.appendResolvedExcludedIdSetConditions(resolved, TERM_KEYS, (key) => `idx.${TERM_COLUMN[key]}`, conditions, params, queryPlan);
|
|
201
|
+
if (ids.graph !== undefined) {
|
|
202
|
+
conditions.push('idx.graph_id = ?');
|
|
203
|
+
params.push(ids.graph);
|
|
204
|
+
queryPlan.push('GraphMembershipFilter');
|
|
205
|
+
}
|
|
206
|
+
this.appendResolvedExcludedIdSetConditions(resolved, ['graph'], () => 'idx.graph_id', conditions, params, queryPlan);
|
|
207
|
+
const graphPrefixJoin = resolved.graphPrefix
|
|
208
|
+
? ` JOIN rdf_terms graph_prefix
|
|
209
|
+
ON graph_prefix.id = idx.graph_id`
|
|
210
|
+
: '';
|
|
211
|
+
if (resolved.graphPrefix) {
|
|
212
|
+
conditions.push(`graph_prefix.kind = ?
|
|
213
|
+
AND graph_prefix.value_head >= ?
|
|
214
|
+
AND graph_prefix.value_head < ?
|
|
215
|
+
AND graph_prefix.value >= ?
|
|
216
|
+
AND graph_prefix.value < ?`);
|
|
217
|
+
params.push('iri', (0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix), `${(0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix)}\uffff`, resolved.graphPrefix, `${resolved.graphPrefix}\uffff`);
|
|
218
|
+
queryPlan.push('GraphPrefixMembershipFilter');
|
|
219
|
+
}
|
|
220
|
+
if (resolved.objectRange) {
|
|
221
|
+
this.appendObjectRangeCondition('object_range', resolved.objectRange, conditions, params, queryPlan);
|
|
222
|
+
}
|
|
223
|
+
const termFilterJoins = [];
|
|
224
|
+
this.appendTermFilterJoinsAndConditions(resolved, ['graph', ...TERM_KEYS], (key) => `idx.${PATTERN_COLUMNS[key]}`, termFilterJoins, conditions, params, queryPlan, 'distinct_term_filter');
|
|
225
|
+
const distinctColumn = distinctKey === 'graph'
|
|
226
|
+
? 'idx.graph_id'
|
|
227
|
+
: `idx.${TERM_COLUMN[distinctKey]}`;
|
|
228
|
+
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
229
|
+
const from = `
|
|
230
|
+
FROM ${this.permutationSource(permutation, 'idx')}
|
|
231
|
+
${termFilterJoins.join('')}
|
|
232
|
+
${graphPrefixJoin}
|
|
233
|
+
${resolved.objectRange ? 'JOIN rdf_terms object_range ON object_range.id = idx.object_id' : ''}
|
|
234
|
+
`;
|
|
235
|
+
return {
|
|
236
|
+
sql: `SELECT COUNT(DISTINCT ${distinctColumn}) AS count ${from} ${whereClause}`,
|
|
237
|
+
params,
|
|
238
|
+
queryPlan: [
|
|
239
|
+
...queryPlan,
|
|
240
|
+
`Rdf3xDistinctCount(?${distinctKey})`,
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
compileMembershipDistinctCountSql(resolved, distinctKey) {
|
|
245
|
+
const conditions = [];
|
|
246
|
+
const params = [];
|
|
247
|
+
const queryPlan = ['Rdf3xMembershipScan'];
|
|
248
|
+
const ids = resolved.ids;
|
|
249
|
+
const alias = 'membership';
|
|
250
|
+
const graphAlias = `${alias}_graph`;
|
|
251
|
+
const graphPrefixAlias = 'graph_prefix';
|
|
252
|
+
for (const key of ['graph', ...TERM_KEYS]) {
|
|
253
|
+
const id = ids[key];
|
|
254
|
+
if (id === undefined) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
conditions.push(`${alias}.${PATTERN_COLUMNS[key]} = ?`);
|
|
258
|
+
params.push(id);
|
|
259
|
+
}
|
|
260
|
+
this.appendResolvedIdSetConditions(resolved, ['graph', ...TERM_KEYS], (key) => `${alias}.${PATTERN_COLUMNS[key]}`, conditions, params, queryPlan);
|
|
261
|
+
this.appendResolvedExcludedIdSetConditions(resolved, ['graph', ...TERM_KEYS], (key) => `${alias}.${PATTERN_COLUMNS[key]}`, conditions, params, queryPlan);
|
|
262
|
+
if (ids.graph !== undefined) {
|
|
263
|
+
queryPlan.push('GraphMembershipFilter');
|
|
264
|
+
}
|
|
265
|
+
const useGraphPrefixSource = resolved.graphPrefix !== undefined
|
|
266
|
+
&& ids.graph === undefined
|
|
267
|
+
&& !resolved.idSets?.graph?.length
|
|
268
|
+
&& !resolved.excludedIdSets?.graph?.length;
|
|
269
|
+
let from = useGraphPrefixSource
|
|
270
|
+
? `${GRAPH_PROJECTION_TABLE} ${graphAlias}
|
|
271
|
+
JOIN rdf_terms ${graphPrefixAlias}
|
|
272
|
+
ON ${graphPrefixAlias}.id = ${graphAlias}.graph_id
|
|
273
|
+
JOIN ${this.factSource(alias)}
|
|
274
|
+
ON ${alias}.graph_id = ${graphAlias}.graph_id`
|
|
275
|
+
: this.factSource(alias);
|
|
276
|
+
if (resolved.graphPrefix !== undefined) {
|
|
277
|
+
if (!useGraphPrefixSource) {
|
|
278
|
+
from += ` JOIN rdf_terms ${graphPrefixAlias}
|
|
279
|
+
ON ${graphPrefixAlias}.id = ${alias}.graph_id`;
|
|
280
|
+
}
|
|
281
|
+
conditions.push(`${graphPrefixAlias}.kind = ?
|
|
282
|
+
AND ${graphPrefixAlias}.value_head >= ?
|
|
283
|
+
AND ${graphPrefixAlias}.value_head < ?
|
|
284
|
+
AND ${graphPrefixAlias}.value >= ?
|
|
285
|
+
AND ${graphPrefixAlias}.value < ?`);
|
|
286
|
+
params.push('iri', (0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix), `${(0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix)}\uffff`, resolved.graphPrefix, `${resolved.graphPrefix}\uffff`);
|
|
287
|
+
queryPlan.push('GraphPrefixMembershipFilter');
|
|
288
|
+
}
|
|
289
|
+
if (resolved.objectRange) {
|
|
290
|
+
from += ` JOIN rdf_terms object_range
|
|
291
|
+
ON object_range.id = ${alias}.object_id`;
|
|
292
|
+
this.appendObjectRangeCondition('object_range', resolved.objectRange, conditions, params, queryPlan);
|
|
293
|
+
}
|
|
294
|
+
const termFilterJoins = [];
|
|
295
|
+
this.appendTermFilterJoinsAndConditions(resolved, ['graph', ...TERM_KEYS], (key) => `${alias}.${PATTERN_COLUMNS[key]}`, termFilterJoins, conditions, params, queryPlan, 'membership_distinct_term_filter');
|
|
296
|
+
from += termFilterJoins.join('');
|
|
297
|
+
const distinctColumn = `${alias}.${PATTERN_COLUMNS[distinctKey]}`;
|
|
298
|
+
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
299
|
+
return {
|
|
300
|
+
sql: `SELECT COUNT(DISTINCT ${distinctColumn}) AS count FROM ${from} ${whereClause}`,
|
|
301
|
+
params,
|
|
302
|
+
queryPlan: [
|
|
303
|
+
...queryPlan,
|
|
304
|
+
`Rdf3xDistinctCount(?${distinctKey})`,
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
joinPatterns(patterns, options) {
|
|
309
|
+
const start = Date.now();
|
|
310
|
+
if (patterns.length === 0) {
|
|
311
|
+
return {
|
|
312
|
+
bindings: [],
|
|
313
|
+
metrics: this.joinMetrics('none', 0, 0, start, ['Rdf3xJoinBGP(empty)']),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const compiled = this.compileJoinPatterns(patterns, options);
|
|
317
|
+
if (compiled.unresolved) {
|
|
318
|
+
return {
|
|
319
|
+
bindings: [],
|
|
320
|
+
metrics: this.joinMetrics('none', 0, 0, start, [
|
|
321
|
+
...compiled.queryPlan,
|
|
322
|
+
`unresolved ${compiled.unresolved}`,
|
|
323
|
+
]),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const rows = this.requireDb().prepare(compiled.sql).all(...compiled.params);
|
|
327
|
+
const matchedRows = compiled.countSql
|
|
328
|
+
? this.requireDb().prepare(compiled.countSql).get(...compiled.countParams)?.count ?? 0
|
|
329
|
+
: rows.length;
|
|
330
|
+
return {
|
|
331
|
+
bindings: this.joinRowsToBindings(rows, compiled.variableAliases),
|
|
332
|
+
metrics: this.joinMetrics(compiled.indexChoice, matchedRows, rows.length, start, [...compiled.queryPlan, compiled.sql]),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
countJoinPatterns(patterns, options) {
|
|
336
|
+
return this.aggregateJoinPatternsInternal(patterns, options, 'Rdf3xJoinCount');
|
|
337
|
+
}
|
|
338
|
+
aggregateJoinPatterns(patterns, options) {
|
|
339
|
+
return this.aggregateJoinPatternsInternal(patterns, options, 'Rdf3xJoinAggregate');
|
|
340
|
+
}
|
|
341
|
+
groupCountJoinPatterns(patterns, options) {
|
|
342
|
+
return this.groupAggregateJoinPatternsInternal(patterns, options, 'Rdf3xJoinGroupCount');
|
|
343
|
+
}
|
|
344
|
+
groupAggregateJoinPatterns(patterns, options) {
|
|
345
|
+
return this.groupAggregateJoinPatternsInternal(patterns, options, 'Rdf3xJoinGroupAggregate');
|
|
346
|
+
}
|
|
347
|
+
aggregateJoinPatternsInternal(patterns, options, label) {
|
|
348
|
+
const start = Date.now();
|
|
349
|
+
if (patterns.length === 0) {
|
|
350
|
+
return {
|
|
351
|
+
bindings: [],
|
|
352
|
+
metrics: this.joinMetrics('none', 0, 0, start, [`${label}(empty)`]),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const compiled = this.compileJoinPatterns(patterns);
|
|
356
|
+
if (compiled.unresolved) {
|
|
357
|
+
return {
|
|
358
|
+
bindings: [],
|
|
359
|
+
metrics: this.joinMetrics('none', 0, 0, start, [...compiled.queryPlan, `unresolved ${compiled.unresolved}`]),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const aggregateAliases = new Map();
|
|
363
|
+
const aggregateTypes = new Map();
|
|
364
|
+
const numericJoins = new Map();
|
|
365
|
+
const numericJoinSql = [];
|
|
366
|
+
const projection = options.aggregates.map((aggregate, index) => {
|
|
367
|
+
const alias = `a${index}`;
|
|
368
|
+
aggregateAliases.set(aggregate.as, alias);
|
|
369
|
+
return this.buildJoinAggregateColumn(aggregate, alias, compiled.variableColumns, aggregateTypes, numericJoins, numericJoinSql, 'RDF-3X BGP');
|
|
370
|
+
}).join(', ');
|
|
371
|
+
const aggregateJoins = numericJoinSql.join('');
|
|
372
|
+
const sql = `SELECT ${projection} FROM ${compiled.from}${compiled.joins}${aggregateJoins}${compiled.whereClause}`;
|
|
373
|
+
const rows = this.requireDb().prepare(sql).all(...compiled.params);
|
|
374
|
+
const matchedRows = this.requireDb()
|
|
375
|
+
.prepare(`SELECT COUNT(*) AS count FROM ${compiled.from}${compiled.joins}${aggregateJoins}${compiled.whereClause}`)
|
|
376
|
+
.get(...compiled.params)?.count ?? 0;
|
|
377
|
+
return {
|
|
378
|
+
bindings: this.joinRowsToBindings(rows, compiled.variableAliases, aggregateAliases, aggregateTypes),
|
|
379
|
+
metrics: this.joinMetrics(compiled.indexChoice, matchedRows, rows.length, start, [
|
|
380
|
+
...compiled.queryPlan,
|
|
381
|
+
...(numericJoinSql.length > 0 ? [`Rdf3xJoinAggregateNumeric(${[...numericJoins.keys()].map((variableName) => `?${variableName}`).join(',')})`] : []),
|
|
382
|
+
`${label}(${options.aggregates.map((aggregate) => (`${aggregate.type}${aggregate.distinct ? ':DISTINCT' : ''}(${aggregate.variable ? `?${aggregate.variable}` : '*'})`)).join(',')})`,
|
|
383
|
+
sql,
|
|
384
|
+
]),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
groupAggregateJoinPatternsInternal(patterns, options, label) {
|
|
388
|
+
const start = Date.now();
|
|
389
|
+
if (patterns.length === 0) {
|
|
390
|
+
return {
|
|
391
|
+
bindings: [],
|
|
392
|
+
metrics: this.joinMetrics('none', 0, 0, start, [`${label}(empty)`]),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const compiled = this.compileJoinPatterns(patterns);
|
|
396
|
+
if (compiled.unresolved) {
|
|
397
|
+
return {
|
|
398
|
+
bindings: [],
|
|
399
|
+
metrics: this.joinMetrics('none', 0, 0, start, [...compiled.queryPlan, `unresolved ${compiled.unresolved}`]),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const aggregateAliases = new Map();
|
|
403
|
+
const aggregateSqlAliases = new Map();
|
|
404
|
+
const aggregateTypes = new Map();
|
|
405
|
+
const numericJoins = new Map();
|
|
406
|
+
const numericJoinSql = [];
|
|
407
|
+
const groupColumns = options.groupBy.map((variableName) => {
|
|
408
|
+
const column = compiled.variableColumns.get(variableName);
|
|
409
|
+
if (!column) {
|
|
410
|
+
throw new Error(`RDF-3X BGP group aggregate cannot group by unbound variable: ${variableName}`);
|
|
411
|
+
}
|
|
412
|
+
return column;
|
|
413
|
+
});
|
|
414
|
+
const aggregateColumns = options.aggregates.map((aggregate, index) => {
|
|
415
|
+
const alias = `a${index}`;
|
|
416
|
+
aggregateAliases.set(aggregate.as, alias);
|
|
417
|
+
aggregateSqlAliases.set(aggregate.as, alias);
|
|
418
|
+
return this.buildJoinAggregateColumn(aggregate, alias, compiled.variableColumns, aggregateTypes, numericJoins, numericJoinSql, 'RDF-3X BGP group aggregate');
|
|
419
|
+
});
|
|
420
|
+
const projection = [
|
|
421
|
+
...options.groupBy.map((variableName) => {
|
|
422
|
+
const alias = compiled.variableAliases.get(variableName);
|
|
423
|
+
const column = compiled.variableColumns.get(variableName);
|
|
424
|
+
if (!alias || !column) {
|
|
425
|
+
throw new Error(`RDF-3X BGP group aggregate cannot project unbound group variable: ${variableName}`);
|
|
426
|
+
}
|
|
427
|
+
return `${column} AS ${alias}`;
|
|
428
|
+
}),
|
|
429
|
+
...aggregateColumns,
|
|
430
|
+
].join(', ');
|
|
431
|
+
const groupBy = groupColumns.join(', ');
|
|
432
|
+
const aggregateJoins = numericJoinSql.join('');
|
|
433
|
+
const havingClause = this.buildGroupAggregateHavingClause(options.having, aggregateSqlAliases);
|
|
434
|
+
const orderScope = this.buildGroupAggregateOrderScope(options, compiled.variableColumns, aggregateSqlAliases);
|
|
435
|
+
const fromSql = `${compiled.from}${compiled.joins}${aggregateJoins}${compiled.whereClause}`;
|
|
436
|
+
const sourceFromSql = `${compiled.from}${compiled.joins}${aggregateJoins}${orderScope.joins}${compiled.whereClause}`;
|
|
437
|
+
const orderClause = orderScope.orderBy;
|
|
438
|
+
let sql = `SELECT ${projection} FROM ${sourceFromSql} GROUP BY ${groupBy}${havingClause.sql}${orderClause}`;
|
|
439
|
+
const params = [...compiled.params, ...havingClause.params];
|
|
440
|
+
const paginated = options.limit !== undefined || options.offset !== undefined;
|
|
441
|
+
if (options.limit !== undefined) {
|
|
442
|
+
sql += ' LIMIT ?';
|
|
443
|
+
params.push(options.limit);
|
|
444
|
+
}
|
|
445
|
+
if (options.offset !== undefined) {
|
|
446
|
+
if (options.limit === undefined) {
|
|
447
|
+
sql += ' LIMIT -1';
|
|
448
|
+
}
|
|
449
|
+
sql += ' OFFSET ?';
|
|
450
|
+
params.push(options.offset);
|
|
451
|
+
}
|
|
452
|
+
const rows = this.requireDb().prepare(sql).all(...params);
|
|
453
|
+
const matchedRows = this.requireDb()
|
|
454
|
+
.prepare(`SELECT COUNT(*) AS count FROM ${fromSql}`)
|
|
455
|
+
.get(...compiled.params)?.count ?? 0;
|
|
456
|
+
return {
|
|
457
|
+
bindings: this.joinRowsToBindings(rows, compiled.variableAliases, aggregateAliases, aggregateTypes),
|
|
458
|
+
metrics: this.joinMetrics(compiled.indexChoice, matchedRows, rows.length, start, [
|
|
459
|
+
...compiled.queryPlan,
|
|
460
|
+
...(numericJoinSql.length > 0 ? [`Rdf3xJoinGroupAggregateNumeric(${[...numericJoins.keys()].map((variableName) => `?${variableName}`).join(',')})`] : []),
|
|
461
|
+
`${label}(${options.groupBy.map((variableName) => `?${variableName}`).join(',')})`,
|
|
462
|
+
...(havingClause.sql ? [`${label}Having(${(options.having ?? []).map((entry) => `${entry.aggregate}${entry.operator}`).join(',')})`] : []),
|
|
463
|
+
...(orderClause ? [`${label}Order(${(options.orderBy ?? []).map((entry) => `${entry.direction ?? 'asc'}:${entry.variable}`).join(',')})`] : []),
|
|
464
|
+
...(paginated ? [`${label}Limit`] : []),
|
|
465
|
+
sql,
|
|
466
|
+
]),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
estimateCardinality(pattern) {
|
|
470
|
+
const resolved = this.resolvePattern(pattern);
|
|
471
|
+
if (resolved.unresolved) {
|
|
472
|
+
return {
|
|
473
|
+
uniqueTriples: 0,
|
|
474
|
+
matchingQuads: 0,
|
|
475
|
+
source: 'exact-membership',
|
|
476
|
+
indexChoice: 'none',
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (resolved.objectRange || hasResolvedTermFilters(resolved)) {
|
|
480
|
+
return this.estimateObjectRangeCardinality(resolved);
|
|
481
|
+
}
|
|
482
|
+
if (hasResolvedIdSets(resolved) || hasResolvedExcludedIdSets(resolved)) {
|
|
483
|
+
return this.estimateResolvedMembershipCardinality(resolved);
|
|
484
|
+
}
|
|
485
|
+
if (resolved.ids.graph !== undefined || resolved.graphPrefix !== undefined) {
|
|
486
|
+
return this.estimateResolvedMembershipCardinality(resolved);
|
|
487
|
+
}
|
|
488
|
+
const termIds = TERM_KEYS.filter((key) => resolved.ids[key] !== undefined);
|
|
489
|
+
const permutation = this.choosePermutation(resolved.ids, {
|
|
490
|
+
idSets: resolved.idSets,
|
|
491
|
+
termFilters: resolved.termFilters,
|
|
492
|
+
});
|
|
493
|
+
if (termIds.length === 3) {
|
|
494
|
+
return this.estimateExactTriple(resolved.ids, permutation.name);
|
|
495
|
+
}
|
|
496
|
+
if (termIds.length === 2) {
|
|
497
|
+
return this.estimatePairProjection(resolved.ids, permutation.name);
|
|
498
|
+
}
|
|
499
|
+
if (termIds.length === 1) {
|
|
500
|
+
return this.estimateTermProjection(resolved.ids, permutation.name);
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
uniqueTriples: this.uniqueTripleCount(),
|
|
504
|
+
matchingQuads: this.rowCount(RDF_FACTS_TABLE),
|
|
505
|
+
source: 'full-count',
|
|
506
|
+
indexChoice: permutation.name,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
stats() {
|
|
510
|
+
const spaceObjects = this.collectSpaceObjects();
|
|
511
|
+
const accountedBytes = spaceObjects.reduce((sum, object) => sum + object.bytes, 0);
|
|
512
|
+
const databaseBytes = accountedBytes || this.estimateDatabaseBytes();
|
|
513
|
+
const uniqueTriples = this.uniqueTripleCount();
|
|
514
|
+
return {
|
|
515
|
+
uniqueTriples,
|
|
516
|
+
membershipCount: this.rowCount(RDF_FACTS_TABLE),
|
|
517
|
+
graphCount: this.rowCount(GRAPH_PROJECTION_TABLE),
|
|
518
|
+
factsDataVersion: this.factsDataVersion(),
|
|
519
|
+
permutationRows: Object.fromEntries(PERMUTATIONS.map((permutation) => [
|
|
520
|
+
permutation.name,
|
|
521
|
+
uniqueTriples,
|
|
522
|
+
])),
|
|
523
|
+
pairProjectionRows: Object.fromEntries(PAIR_PROJECTIONS.map((projection) => [
|
|
524
|
+
projection.name,
|
|
525
|
+
this.rowCount(projection.table),
|
|
526
|
+
])),
|
|
527
|
+
termProjectionRows: Object.fromEntries(TERM_PROJECTIONS.map((projection) => [
|
|
528
|
+
projection.name,
|
|
529
|
+
this.rowCount(projection.table),
|
|
530
|
+
])),
|
|
531
|
+
databaseBytes,
|
|
532
|
+
tableBytes: sumSpaceObjects(spaceObjects, 'table'),
|
|
533
|
+
indexBytes: sumSpaceObjects(spaceObjects, 'index'),
|
|
534
|
+
spaceObjects,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
collectSpaceObjects() {
|
|
538
|
+
const db = this.requireDb();
|
|
539
|
+
try {
|
|
540
|
+
const schemaRows = db.prepare(`
|
|
541
|
+
SELECT name, type, tbl_name
|
|
542
|
+
FROM sqlite_schema
|
|
543
|
+
WHERE type IN ('table', 'index')
|
|
544
|
+
AND (name LIKE 'rdf3x_%' OR tbl_name LIKE 'rdf3x_%')
|
|
545
|
+
`).all();
|
|
546
|
+
const schema = new Map(schemaRows.map((row) => [row.name, row]));
|
|
547
|
+
try {
|
|
548
|
+
const rows = db.prepare(`
|
|
549
|
+
SELECT name, COUNT(*) AS pages, SUM(pgsize) AS bytes
|
|
550
|
+
FROM dbstat
|
|
551
|
+
WHERE name LIKE 'rdf3x_%'
|
|
552
|
+
OR name LIKE 'sqlite_autoindex_rdf3x_%'
|
|
553
|
+
GROUP BY name
|
|
554
|
+
ORDER BY name
|
|
555
|
+
`).all();
|
|
556
|
+
if (rows.length > 0) {
|
|
557
|
+
return rows.map((row) => {
|
|
558
|
+
const object = schema.get(row.name);
|
|
559
|
+
const kind = rdf3xSpaceObjectKind(row.name, object?.type, object?.tbl_name);
|
|
560
|
+
return {
|
|
561
|
+
name: row.name,
|
|
562
|
+
kind,
|
|
563
|
+
...(object?.tbl_name && object.tbl_name !== row.name ? { tableName: object.tbl_name } : {}),
|
|
564
|
+
pages: row.pages,
|
|
565
|
+
bytes: row.bytes ?? 0,
|
|
566
|
+
};
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
// dbstat is optional in SQLite builds and often unavailable for in-memory databases.
|
|
572
|
+
}
|
|
573
|
+
return this.estimateSpaceObjectsFromSchema(schemaRows);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
initializeSchema() {
|
|
580
|
+
this.dropMaterializedFactCopies();
|
|
581
|
+
this.dropLegacyRowidTables();
|
|
582
|
+
this.prepareSchemaVersion();
|
|
583
|
+
const pairProjectionTables = PAIR_PROJECTIONS.map((projection) => `
|
|
584
|
+
CREATE TABLE IF NOT EXISTS ${projection.table} (
|
|
585
|
+
${projection.columns[0]} INTEGER NOT NULL,
|
|
586
|
+
${projection.columns[1]} INTEGER NOT NULL,
|
|
587
|
+
triple_count INTEGER NOT NULL,
|
|
588
|
+
membership_count INTEGER NOT NULL,
|
|
589
|
+
min_${projection.remainder} INTEGER,
|
|
590
|
+
max_${projection.remainder} INTEGER,
|
|
591
|
+
PRIMARY KEY (${projection.columns.join(', ')})
|
|
592
|
+
) WITHOUT ROWID;
|
|
593
|
+
`).join('\n');
|
|
594
|
+
const termProjectionTables = TERM_PROJECTIONS.map((projection) => `
|
|
595
|
+
CREATE TABLE IF NOT EXISTS ${projection.table} (
|
|
596
|
+
${projection.column} INTEGER NOT NULL PRIMARY KEY,
|
|
597
|
+
triple_count INTEGER NOT NULL,
|
|
598
|
+
membership_count INTEGER NOT NULL
|
|
599
|
+
) WITHOUT ROWID;
|
|
600
|
+
`).join('\n');
|
|
601
|
+
this.requireDb().exec(`
|
|
602
|
+
CREATE TABLE IF NOT EXISTS ${GRAPH_PROJECTION_TABLE} (
|
|
603
|
+
graph_id INTEGER NOT NULL PRIMARY KEY,
|
|
604
|
+
membership_count INTEGER NOT NULL
|
|
605
|
+
) WITHOUT ROWID;
|
|
606
|
+
|
|
607
|
+
CREATE TABLE IF NOT EXISTS rdf3x_metadata (
|
|
608
|
+
key TEXT PRIMARY KEY,
|
|
609
|
+
value TEXT NOT NULL
|
|
610
|
+
) WITHOUT ROWID;
|
|
611
|
+
|
|
612
|
+
${pairProjectionTables}
|
|
613
|
+
${termProjectionTables}
|
|
614
|
+
`);
|
|
615
|
+
this.setMetadataValue('schema_version', String(RDF3X_INDEX_SCHEMA_VERSION));
|
|
616
|
+
}
|
|
617
|
+
dropMaterializedFactCopies() {
|
|
618
|
+
(0, Rdf3xSchema_1.dropRdf3xMaterializedFactCopies)(this.requireDb());
|
|
619
|
+
}
|
|
620
|
+
dropLegacyRowidTables() {
|
|
621
|
+
const db = this.requireDb();
|
|
622
|
+
try {
|
|
623
|
+
const rows = db.prepare(`
|
|
624
|
+
SELECT name, wr
|
|
625
|
+
FROM pragma_table_list
|
|
626
|
+
WHERE name IN (${Rdf3xSchema_1.RDF3X_DERIVED_TABLES.map(() => '?').join(', ')})
|
|
627
|
+
`).all(...Rdf3xSchema_1.RDF3X_DERIVED_TABLES);
|
|
628
|
+
if (!rows.some((row) => row.wr === 0)) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
db.exec([
|
|
636
|
+
...Rdf3xSchema_1.RDF3X_DERIVED_INDEXES.map((index) => `DROP INDEX IF EXISTS ${index};`),
|
|
637
|
+
...Rdf3xSchema_1.RDF3X_DERIVED_TABLES.map((table) => `DROP TABLE IF EXISTS ${table};`),
|
|
638
|
+
].join('\n'));
|
|
639
|
+
}
|
|
640
|
+
clearRdf3xTables() {
|
|
641
|
+
const db = this.requireDb();
|
|
642
|
+
db.exec([
|
|
643
|
+
...PAIR_PROJECTIONS.map((projection) => `DELETE FROM ${projection.table};`),
|
|
644
|
+
...TERM_PROJECTIONS.map((projection) => `DELETE FROM ${projection.table};`),
|
|
645
|
+
`DELETE FROM ${GRAPH_PROJECTION_TABLE};`,
|
|
646
|
+
].join('\n'));
|
|
647
|
+
}
|
|
648
|
+
prepareSchemaVersion() {
|
|
649
|
+
this.ensureMetadataTable();
|
|
650
|
+
const row = this.requireDb()
|
|
651
|
+
.prepare("SELECT value FROM rdf3x_metadata WHERE key = 'schema_version'")
|
|
652
|
+
.get();
|
|
653
|
+
if (!row || row.value === String(RDF3X_INDEX_SCHEMA_VERSION)) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
this.dropRdf3xSchema();
|
|
657
|
+
this.ensureMetadataTable();
|
|
658
|
+
}
|
|
659
|
+
ensureMetadataTable() {
|
|
660
|
+
this.requireDb().exec(`
|
|
661
|
+
CREATE TABLE IF NOT EXISTS rdf3x_metadata (
|
|
662
|
+
key TEXT PRIMARY KEY,
|
|
663
|
+
value TEXT NOT NULL
|
|
664
|
+
) WITHOUT ROWID;
|
|
665
|
+
`);
|
|
666
|
+
}
|
|
667
|
+
dropRdf3xSchema() {
|
|
668
|
+
(0, Rdf3xSchema_1.dropRdf3xDerivedSchemaObjects)(this.requireDb());
|
|
669
|
+
}
|
|
670
|
+
currentFactsDataVersion() {
|
|
671
|
+
try {
|
|
672
|
+
const row = this.requireDb()
|
|
673
|
+
.prepare("SELECT value FROM rdf_index_metadata WHERE key = 'data_version'")
|
|
674
|
+
.get();
|
|
675
|
+
return Number(row?.value ?? 0) || 0;
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return 0;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
setFactsDataVersion(version) {
|
|
682
|
+
this.requireDb().prepare(`
|
|
683
|
+
INSERT INTO rdf3x_metadata (key, value)
|
|
684
|
+
VALUES ('facts_data_version', ?)
|
|
685
|
+
ON CONFLICT (key)
|
|
686
|
+
DO UPDATE SET value = excluded.value
|
|
687
|
+
`).run(String(version));
|
|
688
|
+
}
|
|
689
|
+
setMetadataValue(key, value) {
|
|
690
|
+
this.requireDb().prepare(`
|
|
691
|
+
INSERT INTO rdf3x_metadata (key, value)
|
|
692
|
+
VALUES (?, ?)
|
|
693
|
+
ON CONFLICT (key)
|
|
694
|
+
DO UPDATE SET value = excluded.value
|
|
695
|
+
`).run(key, value);
|
|
696
|
+
}
|
|
697
|
+
rebuildPairProjection(projection) {
|
|
698
|
+
const [left, right] = projection.columns;
|
|
699
|
+
this.requireDb().prepare(`
|
|
700
|
+
INSERT INTO ${projection.table} (
|
|
701
|
+
${left},
|
|
702
|
+
${right},
|
|
703
|
+
triple_count,
|
|
704
|
+
membership_count,
|
|
705
|
+
min_${projection.remainder},
|
|
706
|
+
max_${projection.remainder}
|
|
707
|
+
)
|
|
708
|
+
SELECT
|
|
709
|
+
triple.${left},
|
|
710
|
+
triple.${right},
|
|
711
|
+
triple.triple_count,
|
|
712
|
+
COALESCE(member.membership_count, 0) AS membership_count,
|
|
713
|
+
triple.min_remainder,
|
|
714
|
+
triple.max_remainder
|
|
715
|
+
FROM (
|
|
716
|
+
SELECT
|
|
717
|
+
${left},
|
|
718
|
+
${right},
|
|
719
|
+
COUNT(DISTINCT ${projection.remainder}) AS triple_count,
|
|
720
|
+
MIN(${projection.remainder}) AS min_remainder,
|
|
721
|
+
MAX(${projection.remainder}) AS max_remainder
|
|
722
|
+
FROM ${RDF_FACTS_TABLE}
|
|
723
|
+
GROUP BY ${left}, ${right}
|
|
724
|
+
) triple
|
|
725
|
+
LEFT JOIN (
|
|
726
|
+
SELECT
|
|
727
|
+
${left},
|
|
728
|
+
${right},
|
|
729
|
+
COUNT(*) AS membership_count
|
|
730
|
+
FROM ${RDF_FACTS_TABLE}
|
|
731
|
+
GROUP BY ${left}, ${right}
|
|
732
|
+
) member
|
|
733
|
+
ON member.${left} = triple.${left}
|
|
734
|
+
AND member.${right} = triple.${right}
|
|
735
|
+
`).run();
|
|
736
|
+
}
|
|
737
|
+
rebuildTermProjection(projection) {
|
|
738
|
+
this.requireDb().prepare(`
|
|
739
|
+
INSERT INTO ${projection.table} (
|
|
740
|
+
${projection.column},
|
|
741
|
+
triple_count,
|
|
742
|
+
membership_count
|
|
743
|
+
)
|
|
744
|
+
SELECT
|
|
745
|
+
triple.${projection.column},
|
|
746
|
+
triple.triple_count,
|
|
747
|
+
COALESCE(member.membership_count, 0) AS membership_count
|
|
748
|
+
FROM (
|
|
749
|
+
SELECT
|
|
750
|
+
${projection.column},
|
|
751
|
+
COUNT(*) AS triple_count
|
|
752
|
+
FROM (
|
|
753
|
+
SELECT DISTINCT subject_id, predicate_id, object_id
|
|
754
|
+
FROM ${RDF_FACTS_TABLE}
|
|
755
|
+
) distinct_triples
|
|
756
|
+
GROUP BY ${projection.column}
|
|
757
|
+
) triple
|
|
758
|
+
LEFT JOIN (
|
|
759
|
+
SELECT
|
|
760
|
+
${projection.column},
|
|
761
|
+
COUNT(*) AS membership_count
|
|
762
|
+
FROM ${RDF_FACTS_TABLE}
|
|
763
|
+
GROUP BY ${projection.column}
|
|
764
|
+
) member
|
|
765
|
+
ON member.${projection.column} = triple.${projection.column}
|
|
766
|
+
`).run();
|
|
767
|
+
}
|
|
768
|
+
rebuildGraphProjection() {
|
|
769
|
+
this.requireDb().prepare(`
|
|
770
|
+
INSERT INTO ${GRAPH_PROJECTION_TABLE} (
|
|
771
|
+
graph_id,
|
|
772
|
+
membership_count
|
|
773
|
+
)
|
|
774
|
+
SELECT
|
|
775
|
+
graph_id,
|
|
776
|
+
COUNT(*) AS membership_count
|
|
777
|
+
FROM ${RDF_FACTS_TABLE}
|
|
778
|
+
GROUP BY graph_id
|
|
779
|
+
`).run();
|
|
780
|
+
}
|
|
781
|
+
compileScanSql(permutation, resolved, options, tupleValues) {
|
|
782
|
+
const conditions = [];
|
|
783
|
+
const params = [];
|
|
784
|
+
const queryPlan = [`Permutation(${permutation.name})`];
|
|
785
|
+
const ids = resolved.ids;
|
|
786
|
+
for (const key of TERM_KEYS) {
|
|
787
|
+
const id = ids[key];
|
|
788
|
+
if (id === undefined) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
conditions.push(`idx.${TERM_COLUMN[key]} = ?`);
|
|
792
|
+
params.push(id);
|
|
793
|
+
}
|
|
794
|
+
this.appendResolvedIdSetConditions(resolved, TERM_KEYS, (key) => `idx.${TERM_COLUMN[key]}`, conditions, params, queryPlan);
|
|
795
|
+
this.appendResolvedExcludedIdSetConditions(resolved, TERM_KEYS, (key) => `idx.${TERM_COLUMN[key]}`, conditions, params, queryPlan);
|
|
796
|
+
if (ids.graph !== undefined) {
|
|
797
|
+
conditions.push('idx.graph_id = ?');
|
|
798
|
+
params.push(ids.graph);
|
|
799
|
+
queryPlan.push('GraphMembershipFilter');
|
|
800
|
+
}
|
|
801
|
+
this.appendResolvedExcludedIdSetConditions(resolved, ['graph'], () => 'idx.graph_id', conditions, params, queryPlan);
|
|
802
|
+
const graphPrefixJoin = resolved.graphPrefix
|
|
803
|
+
? ` JOIN rdf_terms graph_prefix
|
|
804
|
+
ON graph_prefix.id = idx.graph_id`
|
|
805
|
+
: '';
|
|
806
|
+
const tupleJoin = tupleValues
|
|
807
|
+
? this.buildTupleConstraintJoin(tupleValues, 'rdf3x_tuple_values_scan', 'idx', 'idx')
|
|
808
|
+
: { join: '', queryPlan: [] };
|
|
809
|
+
if (resolved.graphPrefix) {
|
|
810
|
+
conditions.push(`graph_prefix.kind = ?
|
|
811
|
+
AND graph_prefix.value_head >= ?
|
|
812
|
+
AND graph_prefix.value_head < ?
|
|
813
|
+
AND graph_prefix.value >= ?
|
|
814
|
+
AND graph_prefix.value < ?`);
|
|
815
|
+
params.push('iri', (0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix), `${(0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix)}\uffff`, resolved.graphPrefix, `${resolved.graphPrefix}\uffff`);
|
|
816
|
+
queryPlan.push('GraphPrefixMembershipFilter');
|
|
817
|
+
}
|
|
818
|
+
if (resolved.objectRange) {
|
|
819
|
+
this.appendObjectRangeCondition('object_range', resolved.objectRange, conditions, params, queryPlan);
|
|
820
|
+
}
|
|
821
|
+
const termFilterJoins = [];
|
|
822
|
+
this.appendTermFilterJoinsAndConditions(resolved, ['graph', ...TERM_KEYS], (key) => `idx.${PATTERN_COLUMNS[key]}`, termFilterJoins, conditions, params, queryPlan, 'scan_term_filter');
|
|
823
|
+
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
824
|
+
const orderClause = this.buildOrderClause(options, {
|
|
825
|
+
graph: 'idx.graph_id',
|
|
826
|
+
subject: 'idx.subject_id',
|
|
827
|
+
predicate: 'idx.predicate_id',
|
|
828
|
+
object: 'idx.object_id',
|
|
829
|
+
});
|
|
830
|
+
const from = `
|
|
831
|
+
FROM ${this.permutationSource(permutation, 'idx')}
|
|
832
|
+
${tupleJoin.join}
|
|
833
|
+
${termFilterJoins.join('')}
|
|
834
|
+
${graphPrefixJoin}
|
|
835
|
+
${resolved.objectRange ? 'JOIN rdf_terms object_range ON object_range.id = idx.object_id' : ''}
|
|
836
|
+
`;
|
|
837
|
+
const pagination = this.buildPagination(options);
|
|
838
|
+
return {
|
|
839
|
+
sql: `
|
|
840
|
+
SELECT
|
|
841
|
+
idx.graph_id,
|
|
842
|
+
idx.subject_id,
|
|
843
|
+
idx.predicate_id,
|
|
844
|
+
idx.object_id
|
|
845
|
+
${from}
|
|
846
|
+
${orderClause.joins}
|
|
847
|
+
${whereClause}
|
|
848
|
+
${orderClause.orderBy || ` ORDER BY ${permutation.columns.map((column) => `idx.${column}`).join(', ')}, idx.graph_id`}
|
|
849
|
+
${pagination.sql}
|
|
850
|
+
`,
|
|
851
|
+
params: [...params, ...pagination.params],
|
|
852
|
+
countSql: `SELECT COUNT(*) AS count ${from} ${whereClause}`,
|
|
853
|
+
countParams: params,
|
|
854
|
+
queryPlan: [
|
|
855
|
+
...queryPlan,
|
|
856
|
+
...(orderClause.orderBy ? [`Rdf3xJoinOrder(${describeScanOrder(options)})`] : []),
|
|
857
|
+
...tupleJoin.queryPlan,
|
|
858
|
+
...(pagination.sql ? ['Pagination'] : []),
|
|
859
|
+
],
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
compileMembershipScanSql(resolved, options, tupleValues) {
|
|
863
|
+
const conditions = [];
|
|
864
|
+
const params = [];
|
|
865
|
+
const queryPlan = ['Rdf3xMembershipScan'];
|
|
866
|
+
const ids = resolved.ids;
|
|
867
|
+
const alias = 'membership';
|
|
868
|
+
const graphAlias = `${alias}_graph`;
|
|
869
|
+
const graphPrefixAlias = 'graph_prefix';
|
|
870
|
+
for (const key of ['graph', ...TERM_KEYS]) {
|
|
871
|
+
const id = ids[key];
|
|
872
|
+
if (id === undefined) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
conditions.push(`${alias}.${PATTERN_COLUMNS[key]} = ?`);
|
|
876
|
+
params.push(id);
|
|
877
|
+
}
|
|
878
|
+
this.appendResolvedIdSetConditions(resolved, ['graph', ...TERM_KEYS], (key) => `${alias}.${PATTERN_COLUMNS[key]}`, conditions, params, queryPlan);
|
|
879
|
+
this.appendResolvedExcludedIdSetConditions(resolved, ['graph', ...TERM_KEYS], (key) => `${alias}.${PATTERN_COLUMNS[key]}`, conditions, params, queryPlan);
|
|
880
|
+
if (ids.graph !== undefined) {
|
|
881
|
+
queryPlan.push('GraphMembershipFilter');
|
|
882
|
+
}
|
|
883
|
+
const useGraphPrefixSource = resolved.graphPrefix !== undefined
|
|
884
|
+
&& ids.graph === undefined
|
|
885
|
+
&& !resolved.idSets?.graph?.length
|
|
886
|
+
&& !resolved.excludedIdSets?.graph?.length;
|
|
887
|
+
let from = useGraphPrefixSource
|
|
888
|
+
? `${GRAPH_PROJECTION_TABLE} ${graphAlias}
|
|
889
|
+
JOIN rdf_terms ${graphPrefixAlias}
|
|
890
|
+
ON ${graphPrefixAlias}.id = ${graphAlias}.graph_id
|
|
891
|
+
JOIN ${this.factSource(alias)}
|
|
892
|
+
ON ${alias}.graph_id = ${graphAlias}.graph_id`
|
|
893
|
+
: this.factSource(alias);
|
|
894
|
+
const tupleJoin = tupleValues
|
|
895
|
+
? this.buildTupleConstraintJoin(tupleValues, 'rdf3x_tuple_values_scan', alias, alias)
|
|
896
|
+
: { join: '', queryPlan: [] };
|
|
897
|
+
from += tupleJoin.join;
|
|
898
|
+
if (resolved.graphPrefix !== undefined) {
|
|
899
|
+
if (!useGraphPrefixSource) {
|
|
900
|
+
from += ` JOIN rdf_terms ${graphPrefixAlias}
|
|
901
|
+
ON ${graphPrefixAlias}.id = ${alias}.graph_id`;
|
|
902
|
+
}
|
|
903
|
+
conditions.push(`${graphPrefixAlias}.kind = ?
|
|
904
|
+
AND ${graphPrefixAlias}.value_head >= ?
|
|
905
|
+
AND ${graphPrefixAlias}.value_head < ?
|
|
906
|
+
AND ${graphPrefixAlias}.value >= ?
|
|
907
|
+
AND ${graphPrefixAlias}.value < ?`);
|
|
908
|
+
params.push('iri', (0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix), `${(0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix)}\uffff`, resolved.graphPrefix, `${resolved.graphPrefix}\uffff`);
|
|
909
|
+
queryPlan.push('GraphPrefixMembershipFilter');
|
|
910
|
+
}
|
|
911
|
+
if (resolved.objectRange) {
|
|
912
|
+
from += ` JOIN rdf_terms object_range
|
|
913
|
+
ON object_range.id = ${alias}.object_id`;
|
|
914
|
+
this.appendObjectRangeCondition('object_range', resolved.objectRange, conditions, params, queryPlan);
|
|
915
|
+
}
|
|
916
|
+
const termFilterJoins = [];
|
|
917
|
+
this.appendTermFilterJoinsAndConditions(resolved, ['graph', ...TERM_KEYS], (key) => `${alias}.${PATTERN_COLUMNS[key]}`, termFilterJoins, conditions, params, queryPlan, 'membership_scan_term_filter');
|
|
918
|
+
from += termFilterJoins.join('');
|
|
919
|
+
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
920
|
+
const orderClause = this.buildOrderClause(options, {
|
|
921
|
+
graph: `${alias}.graph_id`,
|
|
922
|
+
subject: `${alias}.subject_id`,
|
|
923
|
+
predicate: `${alias}.predicate_id`,
|
|
924
|
+
object: `${alias}.object_id`,
|
|
925
|
+
});
|
|
926
|
+
const pagination = this.buildPagination(options);
|
|
927
|
+
return {
|
|
928
|
+
sql: `
|
|
929
|
+
SELECT
|
|
930
|
+
${alias}.graph_id,
|
|
931
|
+
${alias}.subject_id,
|
|
932
|
+
${alias}.predicate_id,
|
|
933
|
+
${alias}.object_id
|
|
934
|
+
FROM ${from}
|
|
935
|
+
${orderClause.joins}
|
|
936
|
+
${whereClause}
|
|
937
|
+
${orderClause.orderBy || ` ORDER BY ${alias}.graph_id, ${alias}.subject_id, ${alias}.predicate_id, ${alias}.object_id`}
|
|
938
|
+
${pagination.sql}
|
|
939
|
+
`,
|
|
940
|
+
params: [...params, ...pagination.params],
|
|
941
|
+
countSql: `SELECT COUNT(*) AS count FROM ${from} ${whereClause}`,
|
|
942
|
+
countParams: params,
|
|
943
|
+
queryPlan: [
|
|
944
|
+
...queryPlan,
|
|
945
|
+
...(orderClause.orderBy ? [`Rdf3xJoinOrder(${describeScanOrder(options)})`] : []),
|
|
946
|
+
...tupleJoin.queryPlan,
|
|
947
|
+
...(pagination.sql ? ['Pagination'] : []),
|
|
948
|
+
],
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
compileJoinPatterns(patterns, options) {
|
|
952
|
+
const sources = patterns.map((entry, inputIndex) => {
|
|
953
|
+
const resolved = this.resolveJoinPattern(entry.pattern);
|
|
954
|
+
const permutation = this.choosePermutation(resolved.ids, {
|
|
955
|
+
idSets: resolved.idSets,
|
|
956
|
+
objectRange: Boolean(resolved.objectRange),
|
|
957
|
+
termFilters: resolved.termFilters,
|
|
958
|
+
});
|
|
959
|
+
const estimate = resolved.unresolved
|
|
960
|
+
? {
|
|
961
|
+
uniqueTriples: 0,
|
|
962
|
+
matchingQuads: 0,
|
|
963
|
+
source: 'full-count',
|
|
964
|
+
indexChoice: 'none',
|
|
965
|
+
}
|
|
966
|
+
: this.estimateResolvedCardinality(resolved);
|
|
967
|
+
return {
|
|
968
|
+
inputIndex,
|
|
969
|
+
alias: `q${inputIndex}`,
|
|
970
|
+
membershipAlias: `m${inputIndex}`,
|
|
971
|
+
sourceKind: shouldUseMembershipSource(resolved) ? 'membership' : 'permutation',
|
|
972
|
+
entry,
|
|
973
|
+
resolved,
|
|
974
|
+
permutation,
|
|
975
|
+
estimate,
|
|
976
|
+
};
|
|
977
|
+
});
|
|
978
|
+
const orderedSources = this.orderJoinSources(sources);
|
|
979
|
+
const indexOnly = this.canUseIndexOnlyJoin(sources, options);
|
|
980
|
+
const queryPlan = [
|
|
981
|
+
`Rdf3xJoinBGP(${patterns.length})`,
|
|
982
|
+
`Rdf3xJoinOrder(${orderedSources.map((source) => `?${source.inputIndex}:${source.estimate.indexChoice}`).join('>')})`,
|
|
983
|
+
...(indexOnly ? ['Rdf3xIndexOnlyJoin'] : []),
|
|
984
|
+
];
|
|
985
|
+
const variableColumns = new Map();
|
|
986
|
+
const variableAliases = new Map();
|
|
987
|
+
const conditions = [];
|
|
988
|
+
const params = [];
|
|
989
|
+
const countParams = [];
|
|
990
|
+
const indexChoices = [];
|
|
991
|
+
const fromFragments = [];
|
|
992
|
+
for (const [position, source] of orderedSources.entries()) {
|
|
993
|
+
if (source.resolved.unresolved) {
|
|
994
|
+
return {
|
|
995
|
+
from: '',
|
|
996
|
+
joins: '',
|
|
997
|
+
whereClause: '',
|
|
998
|
+
sql: '',
|
|
999
|
+
params: [],
|
|
1000
|
+
countParams: [],
|
|
1001
|
+
indexChoice: 'none',
|
|
1002
|
+
queryPlan,
|
|
1003
|
+
variableColumns,
|
|
1004
|
+
variableAliases,
|
|
1005
|
+
unresolved: source.resolved.unresolved,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
const mergeJoin = this.buildMergeJoinPlan(source, variableColumns);
|
|
1009
|
+
const scanSql = this.joinSourceSql(source, position === 0, mergeJoin, indexOnly);
|
|
1010
|
+
fromFragments.push(scanSql.from);
|
|
1011
|
+
conditions.push(...scanSql.conditions);
|
|
1012
|
+
params.push(...scanSql.params);
|
|
1013
|
+
countParams.push(...scanSql.params);
|
|
1014
|
+
queryPlan.push(...scanSql.queryPlan);
|
|
1015
|
+
indexChoices.push(source.estimate.indexChoice);
|
|
1016
|
+
for (const key of ['graph', ...TERM_KEYS]) {
|
|
1017
|
+
const variableName = source.entry.variables[key];
|
|
1018
|
+
if (!variableName) {
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
const column = this.joinSourceColumnRef(source, key);
|
|
1022
|
+
const existing = variableColumns.get(variableName);
|
|
1023
|
+
if (existing) {
|
|
1024
|
+
if (!mergeJoin.keys.has(key)) {
|
|
1025
|
+
conditions.push(`${existing} = ${column}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
variableColumns.set(variableName, column);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const projectVariables = options?.project ?? [...variableColumns.keys()];
|
|
1034
|
+
const projectionColumns = projectVariables.map((variableName) => {
|
|
1035
|
+
const column = variableColumns.get(variableName);
|
|
1036
|
+
if (!column) {
|
|
1037
|
+
throw new Error(`Rdf3x BGP join cannot project unbound variable: ${variableName}`);
|
|
1038
|
+
}
|
|
1039
|
+
const alias = `v${variableAliases.size}`;
|
|
1040
|
+
variableAliases.set(variableName, alias);
|
|
1041
|
+
return `${column} AS ${alias}`;
|
|
1042
|
+
});
|
|
1043
|
+
const projection = projectionColumns.length > 0
|
|
1044
|
+
? `${options?.distinct ? 'DISTINCT ' : ''}${projectionColumns.join(', ')}`
|
|
1045
|
+
: `${options?.distinct ? 'DISTINCT ' : ''}1 AS __empty`;
|
|
1046
|
+
const valueJoins = this.buildJoinValuesJoins(options?.values ?? [], variableColumns);
|
|
1047
|
+
const orderClause = this.buildJoinOrderClause(options, variableColumns);
|
|
1048
|
+
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
1049
|
+
const from = `${fromFragments.join('')}${valueJoins.joins}`;
|
|
1050
|
+
let sql = `SELECT ${projection} FROM ${from}${orderClause.joins}${whereClause}${orderClause.orderBy}`;
|
|
1051
|
+
const sqlParams = [...params];
|
|
1052
|
+
const paginated = options?.limit !== undefined || options?.offset !== undefined;
|
|
1053
|
+
const countMatchedRows = options?.countMatchedRows ?? true;
|
|
1054
|
+
if (options?.limit !== undefined) {
|
|
1055
|
+
sql += ' LIMIT ?';
|
|
1056
|
+
sqlParams.push(options.limit);
|
|
1057
|
+
}
|
|
1058
|
+
if (options?.offset !== undefined) {
|
|
1059
|
+
if (options.limit === undefined) {
|
|
1060
|
+
sql += ' LIMIT -1';
|
|
1061
|
+
}
|
|
1062
|
+
sql += ' OFFSET ?';
|
|
1063
|
+
sqlParams.push(options.offset);
|
|
1064
|
+
}
|
|
1065
|
+
if (orderClause.orderBy) {
|
|
1066
|
+
queryPlan.push(`Rdf3xJoinOrderBy(${(options?.orderBy ?? []).map((entry) => `${entry.direction ?? 'asc'}:${entry.variable}`).join(',')})`);
|
|
1067
|
+
}
|
|
1068
|
+
queryPlan.push(...valueJoins.queryPlan);
|
|
1069
|
+
if (options?.distinct) {
|
|
1070
|
+
queryPlan.push(`Rdf3xJoinDistinct(${projectVariables.map((variableName) => `?${variableName}`).join(',')})`);
|
|
1071
|
+
}
|
|
1072
|
+
if (paginated) {
|
|
1073
|
+
queryPlan.push('Rdf3xJoinLimit');
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
from,
|
|
1077
|
+
joins: orderClause.joins,
|
|
1078
|
+
whereClause,
|
|
1079
|
+
sql,
|
|
1080
|
+
params: sqlParams,
|
|
1081
|
+
countSql: paginated && countMatchedRows ? `SELECT COUNT(*) AS count FROM ${from}${orderClause.joins}${whereClause}` : undefined,
|
|
1082
|
+
countParams,
|
|
1083
|
+
indexChoice: `Rdf3xJoinBGP(${indexChoices.join('>')})`,
|
|
1084
|
+
queryPlan,
|
|
1085
|
+
variableColumns,
|
|
1086
|
+
variableAliases,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
joinSourceSql(source, first, mergeJoin, indexOnly) {
|
|
1090
|
+
if (source.sourceKind === 'membership') {
|
|
1091
|
+
return this.membershipJoinSourceSql(source, first, mergeJoin);
|
|
1092
|
+
}
|
|
1093
|
+
const conditions = [];
|
|
1094
|
+
const params = [];
|
|
1095
|
+
const queryPlan = [`Rdf3xPermutationScan(${source.permutation.name})`];
|
|
1096
|
+
const alias = source.alias;
|
|
1097
|
+
const graphPrefixAlias = `${source.membershipAlias}_graph_prefix`;
|
|
1098
|
+
for (const key of TERM_KEYS) {
|
|
1099
|
+
const id = source.resolved.ids[key];
|
|
1100
|
+
if (id === undefined) {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
conditions.push(`${alias}.${TERM_COLUMN[key]} = ?`);
|
|
1104
|
+
params.push(id);
|
|
1105
|
+
}
|
|
1106
|
+
this.appendResolvedIdSetConditions(source.resolved, TERM_KEYS, (key) => `${alias}.${TERM_COLUMN[key]}`, conditions, params, queryPlan);
|
|
1107
|
+
this.appendResolvedExcludedIdSetConditions(source.resolved, TERM_KEYS, (key) => `${alias}.${TERM_COLUMN[key]}`, conditions, params, queryPlan);
|
|
1108
|
+
let from = first
|
|
1109
|
+
? this.permutationSource(source.permutation, alias)
|
|
1110
|
+
: ` JOIN ${this.permutationSource(source.permutation, alias)}
|
|
1111
|
+
ON ${mergeJoin.conditions.length > 0 ? mergeJoin.conditions.join(' AND ') : '1 = 1'}`;
|
|
1112
|
+
if (!first && mergeJoin.variables.length > 0) {
|
|
1113
|
+
queryPlan.push(`Rdf3xMergeJoin(${mergeJoin.variables.map((variableName) => `?${variableName}`).join(',')})`);
|
|
1114
|
+
}
|
|
1115
|
+
if (source.resolved.ids.graph !== undefined) {
|
|
1116
|
+
conditions.push(`${alias}.graph_id = ?`);
|
|
1117
|
+
params.push(source.resolved.ids.graph);
|
|
1118
|
+
queryPlan.push('GraphMembershipFilter');
|
|
1119
|
+
}
|
|
1120
|
+
this.appendResolvedIdSetConditions(source.resolved, ['graph'], () => `${alias}.graph_id`, conditions, params, queryPlan);
|
|
1121
|
+
this.appendResolvedExcludedIdSetConditions(source.resolved, ['graph'], () => `${alias}.graph_id`, conditions, params, queryPlan);
|
|
1122
|
+
if (source.resolved.graphPrefix !== undefined) {
|
|
1123
|
+
from += ` JOIN rdf_terms ${graphPrefixAlias}
|
|
1124
|
+
ON ${graphPrefixAlias}.id = ${alias}.graph_id`;
|
|
1125
|
+
conditions.push(`${graphPrefixAlias}.kind = ?
|
|
1126
|
+
AND ${graphPrefixAlias}.value_head >= ?
|
|
1127
|
+
AND ${graphPrefixAlias}.value_head < ?
|
|
1128
|
+
AND ${graphPrefixAlias}.value >= ?
|
|
1129
|
+
AND ${graphPrefixAlias}.value < ?`);
|
|
1130
|
+
params.push('iri', (0, RdfTermDictionary_1.rdfTermValueHead)(source.resolved.graphPrefix), `${(0, RdfTermDictionary_1.rdfTermValueHead)(source.resolved.graphPrefix)}\uffff`, source.resolved.graphPrefix, `${source.resolved.graphPrefix}\uffff`);
|
|
1131
|
+
queryPlan.push('GraphPrefixMembershipFilter');
|
|
1132
|
+
}
|
|
1133
|
+
if (source.resolved.objectRange) {
|
|
1134
|
+
const alias = `${source.alias}_object_range`;
|
|
1135
|
+
from += ` JOIN rdf_terms ${alias}
|
|
1136
|
+
ON ${alias}.id = ${source.alias}.object_id`;
|
|
1137
|
+
this.appendObjectRangeCondition(alias, source.resolved.objectRange, conditions, params, queryPlan);
|
|
1138
|
+
}
|
|
1139
|
+
const termFilterJoins = [];
|
|
1140
|
+
this.appendTermFilterJoinsAndConditions(source.resolved, ['graph', ...TERM_KEYS], (key) => this.joinSourceColumnRef(source, key), termFilterJoins, conditions, params, queryPlan, `${source.alias}_term_filter`);
|
|
1141
|
+
from += termFilterJoins.join('');
|
|
1142
|
+
return {
|
|
1143
|
+
from,
|
|
1144
|
+
conditions,
|
|
1145
|
+
params,
|
|
1146
|
+
queryPlan,
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
membershipJoinSourceSql(source, first, mergeJoin) {
|
|
1150
|
+
const conditions = [];
|
|
1151
|
+
const params = [];
|
|
1152
|
+
const queryPlan = ['Rdf3xMembershipScan'];
|
|
1153
|
+
const alias = source.membershipAlias;
|
|
1154
|
+
const graphAlias = `${alias}_graph`;
|
|
1155
|
+
const graphPrefixAlias = `${alias}_graph_prefix`;
|
|
1156
|
+
const useGraphPrefixSource = first
|
|
1157
|
+
&& source.resolved.graphPrefix !== undefined
|
|
1158
|
+
&& source.resolved.ids.graph === undefined
|
|
1159
|
+
&& !source.resolved.idSets?.graph?.length
|
|
1160
|
+
&& !source.resolved.excludedIdSets?.graph?.length;
|
|
1161
|
+
for (const key of ['graph', ...TERM_KEYS]) {
|
|
1162
|
+
const id = source.resolved.ids[key];
|
|
1163
|
+
if (id === undefined) {
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
conditions.push(`${this.joinSourceColumnRef(source, key)} = ?`);
|
|
1167
|
+
params.push(id);
|
|
1168
|
+
}
|
|
1169
|
+
this.appendResolvedIdSetConditions(source.resolved, ['graph', ...TERM_KEYS], (key) => this.joinSourceColumnRef(source, key), conditions, params, queryPlan);
|
|
1170
|
+
this.appendResolvedExcludedIdSetConditions(source.resolved, ['graph', ...TERM_KEYS], (key) => this.joinSourceColumnRef(source, key), conditions, params, queryPlan);
|
|
1171
|
+
let from = '';
|
|
1172
|
+
if (useGraphPrefixSource) {
|
|
1173
|
+
from = `${GRAPH_PROJECTION_TABLE} ${graphAlias}
|
|
1174
|
+
JOIN rdf_terms ${graphPrefixAlias}
|
|
1175
|
+
ON ${graphPrefixAlias}.id = ${graphAlias}.graph_id
|
|
1176
|
+
JOIN ${this.factSource(alias)}
|
|
1177
|
+
ON ${alias}.graph_id = ${graphAlias}.graph_id`;
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
from = first
|
|
1181
|
+
? this.factSource(alias)
|
|
1182
|
+
: ` JOIN ${this.factSource(alias)}
|
|
1183
|
+
ON ${mergeJoin.conditions.length > 0 ? mergeJoin.conditions.join(' AND ') : '1 = 1'}`;
|
|
1184
|
+
}
|
|
1185
|
+
if (!first && mergeJoin.variables.length > 0) {
|
|
1186
|
+
queryPlan.push(`Rdf3xMergeJoin(${mergeJoin.variables.map((variableName) => `?${variableName}`).join(',')})`);
|
|
1187
|
+
}
|
|
1188
|
+
if (source.resolved.ids.graph !== undefined) {
|
|
1189
|
+
queryPlan.push('GraphMembershipFilter');
|
|
1190
|
+
}
|
|
1191
|
+
if (source.resolved.graphPrefix !== undefined) {
|
|
1192
|
+
if (!useGraphPrefixSource) {
|
|
1193
|
+
from += ` JOIN rdf_terms ${graphPrefixAlias}
|
|
1194
|
+
ON ${graphPrefixAlias}.id = ${alias}.graph_id`;
|
|
1195
|
+
}
|
|
1196
|
+
conditions.push(`${graphPrefixAlias}.kind = ?
|
|
1197
|
+
AND ${graphPrefixAlias}.value_head >= ?
|
|
1198
|
+
AND ${graphPrefixAlias}.value_head < ?
|
|
1199
|
+
AND ${graphPrefixAlias}.value >= ?
|
|
1200
|
+
AND ${graphPrefixAlias}.value < ?`);
|
|
1201
|
+
params.push('iri', (0, RdfTermDictionary_1.rdfTermValueHead)(source.resolved.graphPrefix), `${(0, RdfTermDictionary_1.rdfTermValueHead)(source.resolved.graphPrefix)}\uffff`, source.resolved.graphPrefix, `${source.resolved.graphPrefix}\uffff`);
|
|
1202
|
+
queryPlan.push('GraphPrefixMembershipFilter');
|
|
1203
|
+
}
|
|
1204
|
+
if (source.resolved.objectRange) {
|
|
1205
|
+
const rangeAlias = `${alias}_object_range`;
|
|
1206
|
+
from += ` JOIN rdf_terms ${rangeAlias}
|
|
1207
|
+
ON ${rangeAlias}.id = ${alias}.object_id`;
|
|
1208
|
+
this.appendObjectRangeCondition(rangeAlias, source.resolved.objectRange, conditions, params, queryPlan);
|
|
1209
|
+
}
|
|
1210
|
+
const termFilterJoins = [];
|
|
1211
|
+
this.appendTermFilterJoinsAndConditions(source.resolved, ['graph', ...TERM_KEYS], (key) => this.joinSourceColumnRef(source, key), termFilterJoins, conditions, params, queryPlan, `${alias}_term_filter`);
|
|
1212
|
+
from += termFilterJoins.join('');
|
|
1213
|
+
return {
|
|
1214
|
+
from,
|
|
1215
|
+
conditions,
|
|
1216
|
+
params,
|
|
1217
|
+
queryPlan,
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
canUseIndexOnlyJoin(sources, options) {
|
|
1221
|
+
if (!options?.distinct) {
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
if (options.limit !== undefined || options.offset !== undefined) {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
return sources.every((source) => (!source.resolved.unresolved
|
|
1228
|
+
&& source.resolved.ids.graph === undefined
|
|
1229
|
+
&& !source.resolved.idSets?.graph?.length
|
|
1230
|
+
&& !source.resolved.excludedIdSets?.graph?.length
|
|
1231
|
+
&& !source.resolved.termFilters?.graph
|
|
1232
|
+
&& source.resolved.graphPrefix === undefined
|
|
1233
|
+
&& !source.entry.variables.graph));
|
|
1234
|
+
}
|
|
1235
|
+
buildMergeJoinPlan(source, variableColumns) {
|
|
1236
|
+
const conditions = [];
|
|
1237
|
+
const keys = new Set();
|
|
1238
|
+
const variables = new Set();
|
|
1239
|
+
for (const key of TERM_KEYS) {
|
|
1240
|
+
const variableName = source.entry.variables[key];
|
|
1241
|
+
if (!variableName) {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
const existing = variableColumns.get(variableName);
|
|
1245
|
+
if (!existing) {
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
conditions.push(`${existing} = ${this.joinSourceColumnRef(source, key)}`);
|
|
1249
|
+
keys.add(key);
|
|
1250
|
+
variables.add(variableName);
|
|
1251
|
+
}
|
|
1252
|
+
return {
|
|
1253
|
+
conditions,
|
|
1254
|
+
keys,
|
|
1255
|
+
variables: [...variables],
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
joinSourceColumnRef(source, key) {
|
|
1259
|
+
const alias = source.sourceKind === 'membership' ? source.membershipAlias : source.alias;
|
|
1260
|
+
return `${alias}.${PATTERN_COLUMNS[key]}`;
|
|
1261
|
+
}
|
|
1262
|
+
buildTupleConstraintJoin(source, tableName, indexAlias, membershipAlias) {
|
|
1263
|
+
const columns = uniquePatternKeys(source.columns);
|
|
1264
|
+
if (columns.length === 0) {
|
|
1265
|
+
return { join: '', queryPlan: [] };
|
|
1266
|
+
}
|
|
1267
|
+
this.populateTupleConstraintTable(tableName, columns, source.rows);
|
|
1268
|
+
const alias = 'tuple_values';
|
|
1269
|
+
const onClause = columns
|
|
1270
|
+
.map((key) => `${alias}.${this.tupleColumnName(key)} = ${this.tupleColumnRef(key, indexAlias, membershipAlias)}`)
|
|
1271
|
+
.join(' AND ');
|
|
1272
|
+
return {
|
|
1273
|
+
join: ` JOIN ${tableName} ${alias} ON ${onClause}`,
|
|
1274
|
+
queryPlan: [`TupleValuesJoin(${columns.join(',')})`],
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
buildJoinValuesJoins(sources, variableColumns) {
|
|
1278
|
+
if (sources.length === 0) {
|
|
1279
|
+
return { joins: '', queryPlan: [] };
|
|
1280
|
+
}
|
|
1281
|
+
const joins = [];
|
|
1282
|
+
const queryPlan = [];
|
|
1283
|
+
sources.forEach((source, sourceIndex) => {
|
|
1284
|
+
const tableName = `rdf3x_join_values_${sourceIndex}`;
|
|
1285
|
+
const alias = `join_values_${sourceIndex}`;
|
|
1286
|
+
this.populateJoinValuesTable(tableName, source);
|
|
1287
|
+
const onClause = source.variables.map((variableName, variableIndex) => {
|
|
1288
|
+
const column = variableColumns.get(variableName);
|
|
1289
|
+
if (!column) {
|
|
1290
|
+
throw new Error(`Rdf3x BGP join VALUES cannot constrain unbound variable: ${variableName}`);
|
|
1291
|
+
}
|
|
1292
|
+
return `${alias}.${this.joinValueColumnName(variableIndex)} = ${column}`;
|
|
1293
|
+
}).join(' AND ');
|
|
1294
|
+
joins.push(` JOIN ${tableName} ${alias} ON ${onClause}`);
|
|
1295
|
+
queryPlan.push(`Rdf3xJoinTupleValues(${source.variables.map((variableName) => `?${variableName}`).join(',')})`);
|
|
1296
|
+
});
|
|
1297
|
+
return {
|
|
1298
|
+
joins: joins.join(''),
|
|
1299
|
+
queryPlan,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
populateJoinValuesTable(tableName, source) {
|
|
1303
|
+
const db = this.requireDb();
|
|
1304
|
+
const columnDefs = source.variables
|
|
1305
|
+
.map((_variableName, index) => `${this.joinValueColumnName(index)} INTEGER NOT NULL`)
|
|
1306
|
+
.join(', ');
|
|
1307
|
+
db.exec(`DROP TABLE IF EXISTS ${tableName}`);
|
|
1308
|
+
db.exec(`CREATE TEMP TABLE ${tableName} (${columnDefs})`);
|
|
1309
|
+
const valueRows = source.rows
|
|
1310
|
+
.map((row) => source.variables.map((variableName) => this.termIdForTupleConstraint(row[variableName])))
|
|
1311
|
+
.filter((ids) => ids.every((id) => id !== undefined));
|
|
1312
|
+
if (valueRows.length === 0) {
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const insertColumns = source.variables
|
|
1316
|
+
.map((_variableName, index) => this.joinValueColumnName(index))
|
|
1317
|
+
.join(', ');
|
|
1318
|
+
const placeholders = `(${source.variables.map(() => '?').join(', ')})`;
|
|
1319
|
+
const insert = db.prepare(`INSERT INTO ${tableName} (${insertColumns}) VALUES ${placeholders}`);
|
|
1320
|
+
for (const valueRow of valueRows) {
|
|
1321
|
+
insert.run(...valueRow);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
joinValueColumnName(index) {
|
|
1325
|
+
return `value_${index}_id`;
|
|
1326
|
+
}
|
|
1327
|
+
populateTupleConstraintTable(tableName, columns, rows) {
|
|
1328
|
+
const db = this.requireDb();
|
|
1329
|
+
const columnDefs = columns.map((key) => `${this.tupleColumnName(key)} INTEGER NOT NULL`).join(', ');
|
|
1330
|
+
const primaryKey = columns.map((key) => this.tupleColumnName(key)).join(', ');
|
|
1331
|
+
db.exec(`DROP TABLE IF EXISTS ${tableName}`);
|
|
1332
|
+
db.exec(`CREATE TEMP TABLE ${tableName} (${columnDefs}, PRIMARY KEY (${primaryKey}))`);
|
|
1333
|
+
const valueRows = rows
|
|
1334
|
+
.map((row) => columns.map((key) => this.termIdForTupleConstraint(row[key])))
|
|
1335
|
+
.filter((ids) => ids.every((id) => id !== undefined));
|
|
1336
|
+
if (valueRows.length === 0) {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const insertColumns = columns.map((key) => this.tupleColumnName(key)).join(', ');
|
|
1340
|
+
const placeholders = `(${columns.map(() => '?').join(', ')})`;
|
|
1341
|
+
const insert = db.prepare(`INSERT OR IGNORE INTO ${tableName} (${insertColumns}) VALUES ${placeholders}`);
|
|
1342
|
+
for (const valueRow of valueRows) {
|
|
1343
|
+
insert.run(...valueRow);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
termIdForTupleConstraint(term) {
|
|
1347
|
+
if (!term) {
|
|
1348
|
+
return undefined;
|
|
1349
|
+
}
|
|
1350
|
+
return this.requireDictionary().find(term);
|
|
1351
|
+
}
|
|
1352
|
+
tupleColumnName(key) {
|
|
1353
|
+
return key === 'graph' ? 'graph_id' : TERM_COLUMN[key];
|
|
1354
|
+
}
|
|
1355
|
+
tupleColumnRef(key, indexAlias, membershipAlias) {
|
|
1356
|
+
return key === 'graph' ? `${membershipAlias}.graph_id` : `${indexAlias}.${TERM_COLUMN[key]}`;
|
|
1357
|
+
}
|
|
1358
|
+
buildJoinOrderClause(options, variableColumns) {
|
|
1359
|
+
if (!options?.orderBy || options.orderBy.length === 0) {
|
|
1360
|
+
return { joins: '', orderBy: '' };
|
|
1361
|
+
}
|
|
1362
|
+
const joins = options.orderBy.map((entry, index) => {
|
|
1363
|
+
const column = variableColumns.get(entry.variable);
|
|
1364
|
+
if (!column) {
|
|
1365
|
+
throw new Error(`Rdf3x join cannot order by unbound variable: ${entry.variable}`);
|
|
1366
|
+
}
|
|
1367
|
+
const alias = `join_order_t${index}`;
|
|
1368
|
+
return {
|
|
1369
|
+
join: ` JOIN rdf_terms ${alias} ON ${alias}.id = ${column}`,
|
|
1370
|
+
order: `${alias}.value${entry.direction === 'desc' ? ' DESC' : ''}`,
|
|
1371
|
+
};
|
|
1372
|
+
});
|
|
1373
|
+
return {
|
|
1374
|
+
joins: joins.map((entry) => entry.join).join(''),
|
|
1375
|
+
orderBy: ` ORDER BY ${joins.map((entry) => entry.order).join(', ')}`,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
buildJoinAggregateColumn(aggregate, alias, variableColumns, aggregateTypes, numericJoins, numericJoinSql, errorPrefix) {
|
|
1379
|
+
if (aggregate.type === 'count' && !aggregate.variable) {
|
|
1380
|
+
aggregateTypes.set(aggregate.as, 'integer');
|
|
1381
|
+
return `${aggregate.distinct ? `COUNT(DISTINCT ${joinSolutionMappingKeyExpression(variableColumns, aggregate.distinctVariables, errorPrefix)})` : 'COUNT(*)'} AS ${alias}`;
|
|
1382
|
+
}
|
|
1383
|
+
if (!aggregate.variable) {
|
|
1384
|
+
throw new Error(`${errorPrefix} ${aggregate.type} aggregate requires a bound variable`);
|
|
1385
|
+
}
|
|
1386
|
+
const column = variableColumns.get(aggregate.variable);
|
|
1387
|
+
if (!column) {
|
|
1388
|
+
throw new Error(`${errorPrefix} aggregate cannot read unbound variable: ${aggregate.variable}`);
|
|
1389
|
+
}
|
|
1390
|
+
if (aggregate.type === 'count') {
|
|
1391
|
+
aggregateTypes.set(aggregate.as, 'integer');
|
|
1392
|
+
return `COUNT(${aggregate.distinct ? 'DISTINCT ' : ''}${column}) AS ${alias}`;
|
|
1393
|
+
}
|
|
1394
|
+
if (aggregate.distinct) {
|
|
1395
|
+
throw new Error(`${errorPrefix} ${aggregate.type} DISTINCT aggregate is not supported in SQL aggregate path`);
|
|
1396
|
+
}
|
|
1397
|
+
aggregateTypes.set(aggregate.as, 'decimal');
|
|
1398
|
+
const termAlias = numericJoins.get(aggregate.variable) ?? `rdf3x_agg_numeric_t${numericJoins.size}`;
|
|
1399
|
+
if (!numericJoins.has(aggregate.variable)) {
|
|
1400
|
+
numericJoins.set(aggregate.variable, termAlias);
|
|
1401
|
+
numericJoinSql.push(` JOIN rdf_terms ${termAlias} ON ${termAlias}.id = ${column} AND ${termAlias}.kind = 'literal' AND ${termAlias}.numeric_value IS NOT NULL`);
|
|
1402
|
+
}
|
|
1403
|
+
switch (aggregate.type) {
|
|
1404
|
+
case 'sum':
|
|
1405
|
+
return `COALESCE(SUM(${termAlias}.numeric_value), 0) AS ${alias}`;
|
|
1406
|
+
case 'avg':
|
|
1407
|
+
return `AVG(${termAlias}.numeric_value) AS ${alias}`;
|
|
1408
|
+
case 'min':
|
|
1409
|
+
return `MIN(${termAlias}.numeric_value) AS ${alias}`;
|
|
1410
|
+
case 'max':
|
|
1411
|
+
return `MAX(${termAlias}.numeric_value) AS ${alias}`;
|
|
1412
|
+
default: {
|
|
1413
|
+
const exhaustive = aggregate.type;
|
|
1414
|
+
throw new Error(`Unsupported RDF-3X BGP aggregate type: ${exhaustive}`);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
buildGroupAggregateHavingClause(having, aggregateAliases) {
|
|
1419
|
+
if (!having || having.length === 0) {
|
|
1420
|
+
return { sql: '', params: [] };
|
|
1421
|
+
}
|
|
1422
|
+
const conditions = [];
|
|
1423
|
+
const params = [];
|
|
1424
|
+
for (const entry of having) {
|
|
1425
|
+
const alias = aggregateAliases.get(entry.aggregate);
|
|
1426
|
+
if (!alias) {
|
|
1427
|
+
throw new Error(`RDF-3X BGP group aggregate cannot HAVING on unknown aggregate: ${entry.aggregate}`);
|
|
1428
|
+
}
|
|
1429
|
+
conditions.push(`${alias} ${this.havingSqlOperator(entry.operator)} ?`);
|
|
1430
|
+
params.push(entry.value);
|
|
1431
|
+
}
|
|
1432
|
+
return {
|
|
1433
|
+
sql: ` HAVING ${conditions.join(' AND ')}`,
|
|
1434
|
+
params,
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
havingSqlOperator(operator) {
|
|
1438
|
+
switch (operator) {
|
|
1439
|
+
case '$eq':
|
|
1440
|
+
return '=';
|
|
1441
|
+
case '$ne':
|
|
1442
|
+
return '!=';
|
|
1443
|
+
case '$gt':
|
|
1444
|
+
return '>';
|
|
1445
|
+
case '$gte':
|
|
1446
|
+
return '>=';
|
|
1447
|
+
case '$lt':
|
|
1448
|
+
return '<';
|
|
1449
|
+
case '$lte':
|
|
1450
|
+
return '<=';
|
|
1451
|
+
default: {
|
|
1452
|
+
const exhaustive = operator;
|
|
1453
|
+
throw new Error(`Unsupported RDF-3X BGP group aggregate HAVING operator: ${exhaustive}`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
buildGroupAggregateOrderScope(options, variableColumns, aggregateAliases) {
|
|
1458
|
+
if (!options.orderBy || options.orderBy.length === 0) {
|
|
1459
|
+
return { joins: '', orderBy: '' };
|
|
1460
|
+
}
|
|
1461
|
+
const joins = [];
|
|
1462
|
+
const orders = options.orderBy.map((entry, index) => {
|
|
1463
|
+
const aggregateAlias = aggregateAliases.get(entry.variable);
|
|
1464
|
+
if (aggregateAlias) {
|
|
1465
|
+
return `${aggregateAlias}${entry.direction === 'desc' ? ' DESC' : ''}`;
|
|
1466
|
+
}
|
|
1467
|
+
const column = variableColumns.get(entry.variable);
|
|
1468
|
+
if (!column) {
|
|
1469
|
+
throw new Error(`RDF-3X BGP group aggregate cannot order by unbound variable: ${entry.variable}`);
|
|
1470
|
+
}
|
|
1471
|
+
const alias = `rdf3x_group_order_t${index}`;
|
|
1472
|
+
joins.push(` JOIN rdf_terms ${alias} ON ${alias}.id = ${column}`);
|
|
1473
|
+
return `${alias}.value${entry.direction === 'desc' ? ' DESC' : ''}`;
|
|
1474
|
+
});
|
|
1475
|
+
return {
|
|
1476
|
+
joins: joins.join(''),
|
|
1477
|
+
orderBy: ` ORDER BY ${orders.join(', ')}`,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
buildOrderClause(options, columnRefs) {
|
|
1481
|
+
if (!options?.order || options.order.length === 0) {
|
|
1482
|
+
return { joins: '', orderBy: '' };
|
|
1483
|
+
}
|
|
1484
|
+
const joins = options.order.map((termName, index) => {
|
|
1485
|
+
const column = ORDER_COLUMN[termName];
|
|
1486
|
+
const alias = `order_t${index}`;
|
|
1487
|
+
const direction = options.orderDirections?.[index] ?? (options.reverse ? 'desc' : 'asc');
|
|
1488
|
+
const columnRef = columnRefs?.[termName] ?? (termName === 'graph' ? 'membership.graph_id' : `idx.${column}`);
|
|
1489
|
+
return {
|
|
1490
|
+
join: ` JOIN rdf_terms ${alias} ON ${alias}.id = ${columnRef}`,
|
|
1491
|
+
order: `${alias}.value${direction === 'desc' ? ' DESC' : ''}`,
|
|
1492
|
+
};
|
|
1493
|
+
});
|
|
1494
|
+
return {
|
|
1495
|
+
joins: joins.map((entry) => entry.join).join(''),
|
|
1496
|
+
orderBy: ` ORDER BY ${joins.map((entry) => entry.order).join(', ')}`,
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
chooseJoinStart(sources) {
|
|
1500
|
+
if (sources.length === 0) {
|
|
1501
|
+
throw new Error('Rdf3x join requires at least one source');
|
|
1502
|
+
}
|
|
1503
|
+
return [...sources].sort((left, right) => this.compareJoinSources(left, right))[0];
|
|
1504
|
+
}
|
|
1505
|
+
orderJoinSources(sources) {
|
|
1506
|
+
const remaining = [...sources];
|
|
1507
|
+
const ordered = [];
|
|
1508
|
+
const boundVariables = new Set();
|
|
1509
|
+
while (remaining.length > 0) {
|
|
1510
|
+
const next = ordered.length === 0
|
|
1511
|
+
? this.chooseJoinStart(remaining)
|
|
1512
|
+
: this.chooseNextJoinSource(remaining, boundVariables);
|
|
1513
|
+
ordered.push(next);
|
|
1514
|
+
for (const variableName of this.sourceVariables(next)) {
|
|
1515
|
+
boundVariables.add(variableName);
|
|
1516
|
+
}
|
|
1517
|
+
remaining.splice(remaining.findIndex((source) => source.inputIndex === next.inputIndex), 1);
|
|
1518
|
+
}
|
|
1519
|
+
return ordered;
|
|
1520
|
+
}
|
|
1521
|
+
chooseNextJoinSource(sources, boundVariables) {
|
|
1522
|
+
return [...sources].sort((left, right) => (this.compareJoinConnectivity(left, right, boundVariables)
|
|
1523
|
+
|| this.compareJoinFanout(left, right, boundVariables)
|
|
1524
|
+
|| this.compareJoinSources(left, right)))[0];
|
|
1525
|
+
}
|
|
1526
|
+
compareJoinConnectivity(left, right, boundVariables) {
|
|
1527
|
+
const leftConnected = this.boundVariableCount(left, boundVariables);
|
|
1528
|
+
const rightConnected = this.boundVariableCount(right, boundVariables);
|
|
1529
|
+
if (leftConnected !== rightConnected) {
|
|
1530
|
+
return rightConnected - leftConnected;
|
|
1531
|
+
}
|
|
1532
|
+
return 0;
|
|
1533
|
+
}
|
|
1534
|
+
boundVariableCount(source, boundVariables) {
|
|
1535
|
+
return this.sourceVariables(source).filter((variableName) => boundVariables.has(variableName)).length;
|
|
1536
|
+
}
|
|
1537
|
+
compareJoinFanout(left, right, boundVariables) {
|
|
1538
|
+
const leftFanout = this.estimateJoinFanout(left, boundVariables);
|
|
1539
|
+
const rightFanout = this.estimateJoinFanout(right, boundVariables);
|
|
1540
|
+
if (leftFanout !== rightFanout) {
|
|
1541
|
+
return leftFanout - rightFanout;
|
|
1542
|
+
}
|
|
1543
|
+
return 0;
|
|
1544
|
+
}
|
|
1545
|
+
estimateJoinFanout(source, boundVariables) {
|
|
1546
|
+
if (source.resolved.unresolved) {
|
|
1547
|
+
return Number.POSITIVE_INFINITY;
|
|
1548
|
+
}
|
|
1549
|
+
const boundKeys = this.boundPatternKeys(source, boundVariables);
|
|
1550
|
+
if (boundKeys.length === 0) {
|
|
1551
|
+
return source.estimate.matchingQuads;
|
|
1552
|
+
}
|
|
1553
|
+
const distinctBoundTuples = this.countDistinctResolvedMembershipTuple(source.resolved, boundKeys);
|
|
1554
|
+
if (distinctBoundTuples === 0) {
|
|
1555
|
+
return source.estimate.matchingQuads === 0 ? 0 : Number.POSITIVE_INFINITY;
|
|
1556
|
+
}
|
|
1557
|
+
return source.estimate.matchingQuads / distinctBoundTuples;
|
|
1558
|
+
}
|
|
1559
|
+
boundPatternKeys(source, boundVariables) {
|
|
1560
|
+
return uniquePatternKeys(['graph', ...TERM_KEYS].filter((key) => {
|
|
1561
|
+
const variableName = source.entry.variables[key];
|
|
1562
|
+
return variableName ? boundVariables.has(variableName) : false;
|
|
1563
|
+
}));
|
|
1564
|
+
}
|
|
1565
|
+
sourceVariables(source) {
|
|
1566
|
+
return [...new Set(Object.values(source.entry.variables).filter((value) => Boolean(value)))];
|
|
1567
|
+
}
|
|
1568
|
+
compareJoinSources(left, right) {
|
|
1569
|
+
const leftResolved = left.resolved.unresolved ? Number.POSITIVE_INFINITY : left.estimate.matchingQuads;
|
|
1570
|
+
const rightResolved = right.resolved.unresolved ? Number.POSITIVE_INFINITY : right.estimate.matchingQuads;
|
|
1571
|
+
if (leftResolved !== rightResolved) {
|
|
1572
|
+
return leftResolved - rightResolved;
|
|
1573
|
+
}
|
|
1574
|
+
if (left.estimate.uniqueTriples !== right.estimate.uniqueTriples) {
|
|
1575
|
+
return left.estimate.uniqueTriples - right.estimate.uniqueTriples;
|
|
1576
|
+
}
|
|
1577
|
+
return left.inputIndex - right.inputIndex;
|
|
1578
|
+
}
|
|
1579
|
+
estimateResolvedCardinality(resolved) {
|
|
1580
|
+
const ids = resolved.ids;
|
|
1581
|
+
if (resolved.objectRange || hasResolvedTermFilters(resolved)) {
|
|
1582
|
+
return this.estimateObjectRangeCardinality(resolved);
|
|
1583
|
+
}
|
|
1584
|
+
if (hasResolvedIdSets(resolved) || hasResolvedExcludedIdSets(resolved)) {
|
|
1585
|
+
return this.estimateResolvedMembershipCardinality(resolved);
|
|
1586
|
+
}
|
|
1587
|
+
const termIds = TERM_KEYS.filter((key) => ids[key] !== undefined);
|
|
1588
|
+
if (ids.graph !== undefined || resolved.graphPrefix !== undefined) {
|
|
1589
|
+
return this.estimateResolvedMembershipCardinality(resolved);
|
|
1590
|
+
}
|
|
1591
|
+
if (termIds.length === 3) {
|
|
1592
|
+
return this.estimateExactTriple(ids, this.choosePermutation(ids).name);
|
|
1593
|
+
}
|
|
1594
|
+
if (termIds.length === 2) {
|
|
1595
|
+
return this.estimatePairProjection(ids, this.choosePermutation(ids).name);
|
|
1596
|
+
}
|
|
1597
|
+
if (termIds.length === 1) {
|
|
1598
|
+
return this.estimateTermProjection(ids, this.choosePermutation(ids).name);
|
|
1599
|
+
}
|
|
1600
|
+
return {
|
|
1601
|
+
uniqueTriples: this.uniqueTripleCount(),
|
|
1602
|
+
matchingQuads: this.rowCount(RDF_FACTS_TABLE),
|
|
1603
|
+
source: 'full-count',
|
|
1604
|
+
indexChoice: this.choosePermutation(ids, {
|
|
1605
|
+
idSets: resolved.idSets,
|
|
1606
|
+
objectRange: Boolean(resolved.objectRange),
|
|
1607
|
+
termFilters: resolved.termFilters,
|
|
1608
|
+
}).name,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
resolveJoinPattern(pattern) {
|
|
1612
|
+
const ids = {};
|
|
1613
|
+
const idSets = {};
|
|
1614
|
+
const excludedIdSets = {};
|
|
1615
|
+
const termFilters = {};
|
|
1616
|
+
let graphPrefix;
|
|
1617
|
+
let objectRange;
|
|
1618
|
+
for (const key of ['graph', ...TERM_KEYS]) {
|
|
1619
|
+
const match = pattern[key];
|
|
1620
|
+
if (!match) {
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
if (key === 'graph' && isGraphPrefixPattern(match)) {
|
|
1624
|
+
graphPrefix = match.$startsWith;
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (isTermInPattern(match)) {
|
|
1628
|
+
const resolvedIds = this.resolveTermInIds(match);
|
|
1629
|
+
if (resolvedIds.length === 0) {
|
|
1630
|
+
return { ids, idSets, excludedIdSets, graphPrefix, objectRange, unresolved: key };
|
|
1631
|
+
}
|
|
1632
|
+
idSets[key] = resolvedIds;
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
if (isTermNotInPattern(match)) {
|
|
1636
|
+
const resolvedIds = this.resolveTermNotInIds(match);
|
|
1637
|
+
if (resolvedIds.length > 0) {
|
|
1638
|
+
excludedIdSets[key] = resolvedIds;
|
|
1639
|
+
}
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
if (isOperatorPattern(match)) {
|
|
1643
|
+
const resolved = this.resolveOperatorPattern(key, match);
|
|
1644
|
+
if (resolved.unresolved) {
|
|
1645
|
+
return { ids, idSets, excludedIdSets, termFilters, graphPrefix, objectRange, unresolved: key };
|
|
1646
|
+
}
|
|
1647
|
+
if (resolved.graphPrefix !== undefined)
|
|
1648
|
+
graphPrefix = resolved.graphPrefix;
|
|
1649
|
+
if (resolved.idSet)
|
|
1650
|
+
idSets[key] = resolved.idSet;
|
|
1651
|
+
if (resolved.excludedIdSet)
|
|
1652
|
+
excludedIdSets[key] = resolved.excludedIdSet;
|
|
1653
|
+
if (resolved.objectRange)
|
|
1654
|
+
objectRange = resolved.objectRange;
|
|
1655
|
+
if (resolved.termFilter)
|
|
1656
|
+
termFilters[key] = resolved.termFilter;
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (key === 'object' && isObjectRangePattern(match)) {
|
|
1660
|
+
const resolvedRange = this.resolveObjectRange(match);
|
|
1661
|
+
if (!resolvedRange) {
|
|
1662
|
+
return { ids, idSets, excludedIdSets, termFilters, graphPrefix, objectRange: resolvedRange, unresolved: key };
|
|
1663
|
+
}
|
|
1664
|
+
objectRange = resolvedRange;
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
if (!isRdfTerm(match)) {
|
|
1668
|
+
return { ids, idSets, excludedIdSets, termFilters, graphPrefix, unresolved: key };
|
|
1669
|
+
}
|
|
1670
|
+
const id = this.requireDictionary().find(match);
|
|
1671
|
+
if (id === undefined) {
|
|
1672
|
+
return { ids, idSets, excludedIdSets, termFilters, graphPrefix, unresolved: key };
|
|
1673
|
+
}
|
|
1674
|
+
ids[key] = id;
|
|
1675
|
+
}
|
|
1676
|
+
return {
|
|
1677
|
+
ids,
|
|
1678
|
+
...(Object.keys(idSets).length > 0 ? { idSets } : {}),
|
|
1679
|
+
...(Object.keys(excludedIdSets).length > 0 ? { excludedIdSets } : {}),
|
|
1680
|
+
...(Object.keys(termFilters).length > 0 ? { termFilters } : {}),
|
|
1681
|
+
...(graphPrefix !== undefined ? { graphPrefix } : {}),
|
|
1682
|
+
...(objectRange !== undefined ? { objectRange } : {}),
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
joinMetrics(indexChoice, matchedRows, returnedRows, start, queryPlan) {
|
|
1686
|
+
return {
|
|
1687
|
+
engine: 'solid-rdf3x',
|
|
1688
|
+
indexChoice,
|
|
1689
|
+
matchedRows,
|
|
1690
|
+
returnedRows,
|
|
1691
|
+
durationMs: Date.now() - start,
|
|
1692
|
+
queryPlan,
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
joinRowsToBindings(rows, variableAliases, aggregateAliases, aggregateTypes) {
|
|
1696
|
+
const aliases = [...variableAliases.entries()];
|
|
1697
|
+
const termMap = this.requireDictionary().rowsForIds(rows.flatMap((row) => (aliases
|
|
1698
|
+
.map(([, alias]) => row[alias])
|
|
1699
|
+
.filter((id) => typeof id === 'number'))));
|
|
1700
|
+
return rows.map((row) => {
|
|
1701
|
+
const binding = {};
|
|
1702
|
+
for (const [variableName, alias] of aliases) {
|
|
1703
|
+
const id = row[alias];
|
|
1704
|
+
if (typeof id !== 'number') {
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
binding[variableName] = requiredTerm(termMap, id);
|
|
1708
|
+
}
|
|
1709
|
+
for (const [variableName, alias] of aggregateAliases ?? []) {
|
|
1710
|
+
const value = row[alias];
|
|
1711
|
+
if (typeof value === 'number') {
|
|
1712
|
+
const datatype = aggregateTypes?.get(variableName) === 'decimal' ? XSD_DECIMAL : XSD_INTEGER;
|
|
1713
|
+
binding[variableName] = n3_1.DataFactory.literal(String(value), n3_1.DataFactory.namedNode(datatype));
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return binding;
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
buildPagination(options) {
|
|
1720
|
+
if (!options) {
|
|
1721
|
+
return { sql: '', params: [] };
|
|
1722
|
+
}
|
|
1723
|
+
const clauses = [];
|
|
1724
|
+
const params = [];
|
|
1725
|
+
if (options.limit !== undefined) {
|
|
1726
|
+
clauses.push('LIMIT ?');
|
|
1727
|
+
params.push(Math.max(0, options.limit));
|
|
1728
|
+
}
|
|
1729
|
+
if (options.offset !== undefined) {
|
|
1730
|
+
if (options.limit === undefined) {
|
|
1731
|
+
clauses.push('LIMIT -1');
|
|
1732
|
+
}
|
|
1733
|
+
clauses.push('OFFSET ?');
|
|
1734
|
+
params.push(Math.max(0, options.offset));
|
|
1735
|
+
}
|
|
1736
|
+
return {
|
|
1737
|
+
sql: clauses.length > 0 ? ` ${clauses.join(' ')}` : '',
|
|
1738
|
+
params,
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
estimateExactTriple(ids, indexChoice) {
|
|
1742
|
+
const row = this.requireDb().prepare(`
|
|
1743
|
+
SELECT COUNT(*) AS count
|
|
1744
|
+
FROM (
|
|
1745
|
+
SELECT 1
|
|
1746
|
+
FROM ${RDF_FACTS_TABLE}
|
|
1747
|
+
WHERE subject_id = ?
|
|
1748
|
+
AND predicate_id = ?
|
|
1749
|
+
AND object_id = ?
|
|
1750
|
+
LIMIT 1
|
|
1751
|
+
) exact_triple
|
|
1752
|
+
`).get(ids.subject, ids.predicate, ids.object);
|
|
1753
|
+
const membership = this.requireDb().prepare(`
|
|
1754
|
+
SELECT COUNT(*) AS count
|
|
1755
|
+
FROM ${RDF_FACTS_TABLE}
|
|
1756
|
+
WHERE subject_id = ?
|
|
1757
|
+
AND predicate_id = ?
|
|
1758
|
+
AND object_id = ?
|
|
1759
|
+
`).get(ids.subject, ids.predicate, ids.object);
|
|
1760
|
+
return {
|
|
1761
|
+
uniqueTriples: row?.count ?? 0,
|
|
1762
|
+
matchingQuads: membership?.count ?? 0,
|
|
1763
|
+
source: 'exact-triple',
|
|
1764
|
+
indexChoice,
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
estimatePairProjection(ids, indexChoice) {
|
|
1768
|
+
const projection = this.pairProjectionFor(ids);
|
|
1769
|
+
if (!projection) {
|
|
1770
|
+
return this.estimateMembershipCardinality(ids);
|
|
1771
|
+
}
|
|
1772
|
+
const [left, right] = projection.columns;
|
|
1773
|
+
const row = this.requireDb().prepare(`
|
|
1774
|
+
SELECT triple_count, membership_count
|
|
1775
|
+
FROM ${projection.table}
|
|
1776
|
+
WHERE ${left} = ?
|
|
1777
|
+
AND ${right} = ?
|
|
1778
|
+
`).get(ids[keyForColumn(left)], ids[keyForColumn(right)]);
|
|
1779
|
+
return {
|
|
1780
|
+
uniqueTriples: row?.triple_count ?? 0,
|
|
1781
|
+
matchingQuads: row?.membership_count ?? 0,
|
|
1782
|
+
source: 'projection-stat',
|
|
1783
|
+
indexChoice,
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
estimateTermProjection(ids, indexChoice) {
|
|
1787
|
+
const key = TERM_KEYS.find((candidate) => ids[candidate] !== undefined);
|
|
1788
|
+
if (!key) {
|
|
1789
|
+
return this.estimateMembershipCardinality(ids);
|
|
1790
|
+
}
|
|
1791
|
+
const projection = TERM_PROJECTIONS.find((candidate) => candidate.column === TERM_COLUMN[key]);
|
|
1792
|
+
if (!projection) {
|
|
1793
|
+
return this.estimateMembershipCardinality(ids);
|
|
1794
|
+
}
|
|
1795
|
+
const row = this.requireDb().prepare(`
|
|
1796
|
+
SELECT triple_count, membership_count
|
|
1797
|
+
FROM ${projection.table}
|
|
1798
|
+
WHERE ${projection.column} = ?
|
|
1799
|
+
`).get(ids[key]);
|
|
1800
|
+
return {
|
|
1801
|
+
uniqueTriples: row?.triple_count ?? 0,
|
|
1802
|
+
matchingQuads: row?.membership_count ?? 0,
|
|
1803
|
+
source: 'term-stat',
|
|
1804
|
+
indexChoice,
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
estimateMembershipCardinality(ids) {
|
|
1808
|
+
return this.estimateResolvedMembershipCardinality({ ids });
|
|
1809
|
+
}
|
|
1810
|
+
estimateResolvedMembershipCardinality(resolved) {
|
|
1811
|
+
const { from, whereClause, params } = this.buildMembershipWhere(resolved);
|
|
1812
|
+
const matchingQuads = this.requireDb().prepare(`
|
|
1813
|
+
SELECT COUNT(*) AS count
|
|
1814
|
+
FROM ${from}
|
|
1815
|
+
${whereClause}
|
|
1816
|
+
`).get(...params)?.count ?? 0;
|
|
1817
|
+
const uniqueTriples = this.requireDb().prepare(`
|
|
1818
|
+
SELECT COUNT(*) AS count
|
|
1819
|
+
FROM (
|
|
1820
|
+
SELECT DISTINCT subject_id, predicate_id, object_id
|
|
1821
|
+
FROM ${from}
|
|
1822
|
+
${whereClause}
|
|
1823
|
+
) distinct_triples
|
|
1824
|
+
`).get(...params)?.count ?? 0;
|
|
1825
|
+
return {
|
|
1826
|
+
uniqueTriples,
|
|
1827
|
+
matchingQuads,
|
|
1828
|
+
source: 'exact-membership',
|
|
1829
|
+
indexChoice: 'source-membership',
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
buildMembershipWhere(resolved) {
|
|
1833
|
+
const ids = resolved.ids;
|
|
1834
|
+
const conditions = [];
|
|
1835
|
+
const params = [];
|
|
1836
|
+
const alias = 'membership';
|
|
1837
|
+
const useGraphPrefixSource = resolved.graphPrefix !== undefined
|
|
1838
|
+
&& ids.graph === undefined
|
|
1839
|
+
&& !resolved.idSets?.graph?.length
|
|
1840
|
+
&& !resolved.excludedIdSets?.graph?.length;
|
|
1841
|
+
for (const key of ['graph', ...TERM_KEYS]) {
|
|
1842
|
+
const id = ids[key];
|
|
1843
|
+
if (id === undefined) {
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1846
|
+
conditions.push(`${alias}.${PATTERN_COLUMNS[key]} = ?`);
|
|
1847
|
+
params.push(id);
|
|
1848
|
+
}
|
|
1849
|
+
this.appendResolvedIdSetConditions(resolved, ['graph', ...TERM_KEYS], (key) => `${alias}.${PATTERN_COLUMNS[key]}`, conditions, params);
|
|
1850
|
+
this.appendResolvedExcludedIdSetConditions(resolved, ['graph', ...TERM_KEYS], (key) => `${alias}.${PATTERN_COLUMNS[key]}`, conditions, params);
|
|
1851
|
+
let from = this.factSource(alias);
|
|
1852
|
+
if (useGraphPrefixSource) {
|
|
1853
|
+
from = `${GRAPH_PROJECTION_TABLE} membership_graph
|
|
1854
|
+
JOIN rdf_terms membership_graph_prefix
|
|
1855
|
+
ON membership_graph_prefix.id = membership_graph.graph_id
|
|
1856
|
+
JOIN ${this.factSource(alias)}
|
|
1857
|
+
ON ${alias}.graph_id = membership_graph.graph_id`;
|
|
1858
|
+
}
|
|
1859
|
+
else if (resolved.graphPrefix !== undefined) {
|
|
1860
|
+
from += ` JOIN rdf_terms membership_graph_prefix
|
|
1861
|
+
ON membership_graph_prefix.id = ${alias}.graph_id`;
|
|
1862
|
+
}
|
|
1863
|
+
if (resolved.graphPrefix !== undefined) {
|
|
1864
|
+
conditions.push(`membership_graph_prefix.kind = ?
|
|
1865
|
+
AND membership_graph_prefix.value_head >= ?
|
|
1866
|
+
AND membership_graph_prefix.value_head < ?
|
|
1867
|
+
AND membership_graph_prefix.value >= ?
|
|
1868
|
+
AND membership_graph_prefix.value < ?`);
|
|
1869
|
+
params.push('iri', (0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix), `${(0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix)}\uffff`, resolved.graphPrefix, `${resolved.graphPrefix}\uffff`);
|
|
1870
|
+
}
|
|
1871
|
+
const termFilterJoins = [];
|
|
1872
|
+
this.appendTermFilterJoinsAndConditions(resolved, ['graph', ...TERM_KEYS], (key) => `${alias}.${PATTERN_COLUMNS[key]}`, termFilterJoins, conditions, params, undefined, 'membership_estimate_term_filter');
|
|
1873
|
+
from += termFilterJoins.join('');
|
|
1874
|
+
return {
|
|
1875
|
+
whereClause: conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '',
|
|
1876
|
+
from,
|
|
1877
|
+
params,
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
pairProjectionFor(ids) {
|
|
1881
|
+
const columns = TERM_KEYS
|
|
1882
|
+
.filter((key) => ids[key] !== undefined)
|
|
1883
|
+
.map((key) => TERM_COLUMN[key]);
|
|
1884
|
+
return PAIR_PROJECTIONS.find((projection) => (projection.columns.every((column) => columns.includes(column))));
|
|
1885
|
+
}
|
|
1886
|
+
appendResolvedIdSetConditions(resolved, keys, columnForKey, conditions, params, queryPlan) {
|
|
1887
|
+
for (const key of keys) {
|
|
1888
|
+
const ids = resolved.idSets?.[key];
|
|
1889
|
+
if (!ids || ids.length === 0) {
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
conditions.push(`${columnForKey(key)} IN (${ids.map(() => '?').join(', ')})`);
|
|
1893
|
+
params.push(...ids);
|
|
1894
|
+
queryPlan?.push(`TermIn(${key})`);
|
|
1895
|
+
if (key === 'graph') {
|
|
1896
|
+
queryPlan?.push('GraphMembershipFilter');
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
appendResolvedExcludedIdSetConditions(resolved, keys, columnForKey, conditions, params, queryPlan) {
|
|
1901
|
+
for (const key of keys) {
|
|
1902
|
+
const ids = resolved.excludedIdSets?.[key];
|
|
1903
|
+
if (!ids || ids.length === 0) {
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
conditions.push(`${columnForKey(key)} NOT IN (${ids.map(() => '?').join(', ')})`);
|
|
1907
|
+
params.push(...ids);
|
|
1908
|
+
queryPlan?.push(`TermNotIn(${key})`);
|
|
1909
|
+
if (key === 'graph') {
|
|
1910
|
+
queryPlan?.push('GraphMembershipFilter');
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
resolvePattern(pattern) {
|
|
1915
|
+
const ids = {};
|
|
1916
|
+
const idSets = {};
|
|
1917
|
+
const excludedIdSets = {};
|
|
1918
|
+
const termFilters = {};
|
|
1919
|
+
let graphPrefix;
|
|
1920
|
+
let objectRange;
|
|
1921
|
+
for (const key of ['graph', ...TERM_KEYS]) {
|
|
1922
|
+
const term = pattern[key];
|
|
1923
|
+
if (!term) {
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
if (key === 'graph' && isGraphPrefixPattern(term)) {
|
|
1927
|
+
graphPrefix = term.$startsWith;
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
if (isTermInPattern(term)) {
|
|
1931
|
+
const resolvedIds = this.resolveTermInIds(term);
|
|
1932
|
+
if (resolvedIds.length === 0) {
|
|
1933
|
+
return { ids, idSets, graphPrefix, objectRange, unresolved: key };
|
|
1934
|
+
}
|
|
1935
|
+
idSets[key] = resolvedIds;
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
if (isTermNotInPattern(term)) {
|
|
1939
|
+
const resolvedIds = this.resolveTermNotInIds(term);
|
|
1940
|
+
if (resolvedIds.length > 0) {
|
|
1941
|
+
excludedIdSets[key] = resolvedIds;
|
|
1942
|
+
}
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
if (isOperatorPattern(term)) {
|
|
1946
|
+
const resolved = this.resolveOperatorPattern(key, term);
|
|
1947
|
+
if (resolved.unresolved) {
|
|
1948
|
+
return { ids, idSets, termFilters, graphPrefix, objectRange, unresolved: key };
|
|
1949
|
+
}
|
|
1950
|
+
if (resolved.graphPrefix !== undefined)
|
|
1951
|
+
graphPrefix = resolved.graphPrefix;
|
|
1952
|
+
if (resolved.idSet)
|
|
1953
|
+
idSets[key] = resolved.idSet;
|
|
1954
|
+
if (resolved.excludedIdSet)
|
|
1955
|
+
excludedIdSets[key] = resolved.excludedIdSet;
|
|
1956
|
+
if (resolved.objectRange)
|
|
1957
|
+
objectRange = resolved.objectRange;
|
|
1958
|
+
if (resolved.termFilter)
|
|
1959
|
+
termFilters[key] = resolved.termFilter;
|
|
1960
|
+
continue;
|
|
1961
|
+
}
|
|
1962
|
+
if (key === 'object' && isObjectRangePattern(term)) {
|
|
1963
|
+
const resolvedRange = this.resolveObjectRange(term);
|
|
1964
|
+
if (!resolvedRange) {
|
|
1965
|
+
return { ids, idSets, termFilters, graphPrefix, objectRange: resolvedRange, unresolved: key };
|
|
1966
|
+
}
|
|
1967
|
+
objectRange = resolvedRange;
|
|
1968
|
+
continue;
|
|
1969
|
+
}
|
|
1970
|
+
if (key === 'graph' && !isRdfTerm(term)) {
|
|
1971
|
+
return { ids, idSets, termFilters, graphPrefix, unresolved: key };
|
|
1972
|
+
}
|
|
1973
|
+
if (key !== 'graph' && !isRdfTerm(term)) {
|
|
1974
|
+
return { ids, idSets, termFilters, graphPrefix, unresolved: key };
|
|
1975
|
+
}
|
|
1976
|
+
const rdfTerm = term;
|
|
1977
|
+
const id = this.requireDictionary().find(rdfTerm);
|
|
1978
|
+
if (id === undefined) {
|
|
1979
|
+
return { ids, idSets, termFilters, graphPrefix, unresolved: key };
|
|
1980
|
+
}
|
|
1981
|
+
ids[key] = id;
|
|
1982
|
+
}
|
|
1983
|
+
return {
|
|
1984
|
+
ids,
|
|
1985
|
+
...(Object.keys(idSets).length > 0 ? { idSets } : {}),
|
|
1986
|
+
...(Object.keys(excludedIdSets).length > 0 ? { excludedIdSets } : {}),
|
|
1987
|
+
...(Object.keys(termFilters).length > 0 ? { termFilters } : {}),
|
|
1988
|
+
...(graphPrefix !== undefined ? { graphPrefix } : {}),
|
|
1989
|
+
...(objectRange !== undefined ? { objectRange } : {}),
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
resolveTermInIds(pattern) {
|
|
1993
|
+
return uniqueNumbers(pattern.$in
|
|
1994
|
+
.map((term) => this.requireDictionary().find(term))
|
|
1995
|
+
.filter((id) => id !== undefined));
|
|
1996
|
+
}
|
|
1997
|
+
resolveTermNotInIds(pattern) {
|
|
1998
|
+
return uniqueNumbers(pattern.$notIn
|
|
1999
|
+
.map((term) => this.requireDictionary().find(term))
|
|
2000
|
+
.filter((id) => id !== undefined));
|
|
2001
|
+
}
|
|
2002
|
+
resolveOperatorPattern(key, pattern) {
|
|
2003
|
+
if (!isSupportedOperatorPattern(key, pattern)) {
|
|
2004
|
+
return { unresolved: true };
|
|
2005
|
+
}
|
|
2006
|
+
const result = {};
|
|
2007
|
+
if (key === 'graph' && pattern.$startsWith !== undefined) {
|
|
2008
|
+
result.graphPrefix = pattern.$startsWith;
|
|
2009
|
+
}
|
|
2010
|
+
if (pattern.$in !== undefined) {
|
|
2011
|
+
const resolvedIds = this.resolveTermInIds({ $in: pattern.$in });
|
|
2012
|
+
if (resolvedIds.length === 0) {
|
|
2013
|
+
return { unresolved: true };
|
|
2014
|
+
}
|
|
2015
|
+
result.idSet = resolvedIds;
|
|
2016
|
+
}
|
|
2017
|
+
if (pattern.$notIn !== undefined) {
|
|
2018
|
+
const resolvedIds = this.resolveTermNotInIds({ $notIn: pattern.$notIn });
|
|
2019
|
+
if (resolvedIds.length > 0) {
|
|
2020
|
+
result.excludedIdSet = resolvedIds;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
if (key === 'object' && hasObjectRangeOperator(pattern)) {
|
|
2024
|
+
const objectRange = this.resolveObjectRange(pattern);
|
|
2025
|
+
if (!objectRange) {
|
|
2026
|
+
return { unresolved: true };
|
|
2027
|
+
}
|
|
2028
|
+
result.objectRange = objectRange;
|
|
2029
|
+
}
|
|
2030
|
+
const termFilter = this.resolveTermMetadataFilter(pattern);
|
|
2031
|
+
if (termFilter) {
|
|
2032
|
+
result.termFilter = termFilter;
|
|
2033
|
+
}
|
|
2034
|
+
return result;
|
|
2035
|
+
}
|
|
2036
|
+
resolveTermMetadataFilter(pattern) {
|
|
2037
|
+
const filter = {};
|
|
2038
|
+
if (pattern.$termType !== undefined) {
|
|
2039
|
+
filter.termType = pattern.$termType;
|
|
2040
|
+
}
|
|
2041
|
+
if (pattern.$language !== undefined) {
|
|
2042
|
+
filter.language = pattern.$language;
|
|
2043
|
+
}
|
|
2044
|
+
if (pattern.$notLanguage !== undefined) {
|
|
2045
|
+
filter.notLanguage = pattern.$notLanguage;
|
|
2046
|
+
}
|
|
2047
|
+
if (pattern.$langMatches !== undefined) {
|
|
2048
|
+
filter.langMatches = pattern.$langMatches;
|
|
2049
|
+
}
|
|
2050
|
+
if (pattern.$datatype !== undefined) {
|
|
2051
|
+
filter.datatype = this.resolveDatatypeFilter(pattern.$datatype);
|
|
2052
|
+
}
|
|
2053
|
+
if (pattern.$notDatatype !== undefined) {
|
|
2054
|
+
filter.notDatatype = this.resolveDatatypeFilter(pattern.$notDatatype);
|
|
2055
|
+
}
|
|
2056
|
+
const textSearches = this.resolveTextSearchFilter(pattern);
|
|
2057
|
+
if (textSearches) {
|
|
2058
|
+
filter.textSearches = textSearches;
|
|
2059
|
+
}
|
|
2060
|
+
return Object.keys(filter).length > 0 ? filter : undefined;
|
|
2061
|
+
}
|
|
2062
|
+
resolveTextSearchFilter(pattern) {
|
|
2063
|
+
const searches = [];
|
|
2064
|
+
if (pattern.$contains !== undefined) {
|
|
2065
|
+
searches.push({ operator: '$contains', value: pattern.$contains });
|
|
2066
|
+
}
|
|
2067
|
+
if (pattern.$endsWith !== undefined) {
|
|
2068
|
+
searches.push({ operator: '$endsWith', value: pattern.$endsWith });
|
|
2069
|
+
}
|
|
2070
|
+
return searches.length > 0 ? searches : undefined;
|
|
2071
|
+
}
|
|
2072
|
+
resolveDatatypeFilter(datatype) {
|
|
2073
|
+
if (datatype.termType !== 'NamedNode') {
|
|
2074
|
+
return { kind: 'unknown' };
|
|
2075
|
+
}
|
|
2076
|
+
if (datatype.value === XSD_STRING) {
|
|
2077
|
+
return { kind: 'xsd-string' };
|
|
2078
|
+
}
|
|
2079
|
+
const id = this.requireDictionary().find(datatype);
|
|
2080
|
+
return id === undefined ? { kind: 'unknown' } : { kind: 'id', id };
|
|
2081
|
+
}
|
|
2082
|
+
countDistinctResolvedMembershipTuple(resolved, keys) {
|
|
2083
|
+
const distinctKeys = uniquePatternKeys(keys);
|
|
2084
|
+
if (distinctKeys.length === 0) {
|
|
2085
|
+
return 0;
|
|
2086
|
+
}
|
|
2087
|
+
const { from, whereClause, params } = this.buildMembershipWhere({
|
|
2088
|
+
ids: resolved.ids,
|
|
2089
|
+
...(resolved.idSets !== undefined ? { idSets: resolved.idSets } : {}),
|
|
2090
|
+
...(resolved.excludedIdSets !== undefined ? { excludedIdSets: resolved.excludedIdSets } : {}),
|
|
2091
|
+
...(resolved.graphPrefix !== undefined ? { graphPrefix: resolved.graphPrefix } : {}),
|
|
2092
|
+
});
|
|
2093
|
+
const rangeConditions = [];
|
|
2094
|
+
const rangeParams = [];
|
|
2095
|
+
const rangePlan = [];
|
|
2096
|
+
const rangeJoin = resolved.objectRange
|
|
2097
|
+
? ' JOIN rdf_terms fanout_object_range ON fanout_object_range.id = membership.object_id'
|
|
2098
|
+
: '';
|
|
2099
|
+
if (resolved.objectRange) {
|
|
2100
|
+
this.appendObjectRangeCondition('fanout_object_range', resolved.objectRange, rangeConditions, rangeParams, rangePlan);
|
|
2101
|
+
}
|
|
2102
|
+
const combinedWhereClause = rangeConditions.length > 0
|
|
2103
|
+
? `${whereClause || ' WHERE 1 = 1'} AND ${rangeConditions.join(' AND ')}`
|
|
2104
|
+
: whereClause;
|
|
2105
|
+
const projection = distinctKeys
|
|
2106
|
+
.map((key) => `membership.${PATTERN_COLUMNS[key]}`)
|
|
2107
|
+
.join(', ');
|
|
2108
|
+
return this.requireDb().prepare(`
|
|
2109
|
+
SELECT COUNT(*) AS count
|
|
2110
|
+
FROM (
|
|
2111
|
+
SELECT DISTINCT ${projection}
|
|
2112
|
+
FROM ${from}
|
|
2113
|
+
${rangeJoin}
|
|
2114
|
+
${combinedWhereClause}
|
|
2115
|
+
) distinct_bound_tuples
|
|
2116
|
+
`).get(...params, ...rangeParams)?.count ?? 0;
|
|
2117
|
+
}
|
|
2118
|
+
estimateObjectRangeCardinality(resolved) {
|
|
2119
|
+
const range = resolved.objectRange;
|
|
2120
|
+
if (!range) {
|
|
2121
|
+
return this.estimateResolvedMembershipCardinality(resolved);
|
|
2122
|
+
}
|
|
2123
|
+
const { from, whereClause, params } = this.buildMembershipWhere({
|
|
2124
|
+
ids: resolved.ids,
|
|
2125
|
+
...(resolved.idSets !== undefined ? { idSets: resolved.idSets } : {}),
|
|
2126
|
+
...(resolved.excludedIdSets !== undefined ? { excludedIdSets: resolved.excludedIdSets } : {}),
|
|
2127
|
+
...(resolved.graphPrefix !== undefined ? { graphPrefix: resolved.graphPrefix } : {}),
|
|
2128
|
+
});
|
|
2129
|
+
const rangeConditions = [];
|
|
2130
|
+
const rangeParams = [];
|
|
2131
|
+
const rangePlan = [];
|
|
2132
|
+
this.appendObjectRangeCondition('object_range', range, rangeConditions, rangeParams, rangePlan);
|
|
2133
|
+
const membershipWhere = whereClause
|
|
2134
|
+
? `${whereClause} AND ${rangeConditions.join(' AND ')}`
|
|
2135
|
+
: ` WHERE ${rangeConditions.join(' AND ')}`;
|
|
2136
|
+
const matchingQuads = this.requireDb().prepare(`
|
|
2137
|
+
SELECT COUNT(*) AS count
|
|
2138
|
+
FROM ${from}
|
|
2139
|
+
JOIN rdf_terms object_range ON object_range.id = membership.object_id
|
|
2140
|
+
${membershipWhere}
|
|
2141
|
+
`).get(...params, ...rangeParams)?.count ?? 0;
|
|
2142
|
+
const uniqueTriples = this.requireDb().prepare(`
|
|
2143
|
+
SELECT COUNT(*) AS count
|
|
2144
|
+
FROM (
|
|
2145
|
+
SELECT DISTINCT subject_id, predicate_id, object_id
|
|
2146
|
+
FROM ${from}
|
|
2147
|
+
JOIN rdf_terms object_range ON object_range.id = membership.object_id
|
|
2148
|
+
${membershipWhere}
|
|
2149
|
+
) distinct_triples
|
|
2150
|
+
`).get(...params, ...rangeParams)?.count ?? 0;
|
|
2151
|
+
return {
|
|
2152
|
+
uniqueTriples,
|
|
2153
|
+
matchingQuads,
|
|
2154
|
+
source: 'exact-membership',
|
|
2155
|
+
indexChoice: 'source-membership',
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
appendObjectRangeCondition(alias, range, conditions, params, queryPlan) {
|
|
2159
|
+
if (range.mode === 'numeric') {
|
|
2160
|
+
conditions.push(`${alias}.kind = ?`);
|
|
2161
|
+
params.push('literal');
|
|
2162
|
+
conditions.push(`${alias}.numeric_value IS NOT NULL`);
|
|
2163
|
+
if (range.min !== undefined) {
|
|
2164
|
+
conditions.push(`${alias}.numeric_value ${range.minInclusive ? '>=' : '>'} ?`);
|
|
2165
|
+
params.push(range.min);
|
|
2166
|
+
}
|
|
2167
|
+
if (range.max !== undefined) {
|
|
2168
|
+
conditions.push(`${alias}.numeric_value ${range.maxInclusive ? '<=' : '<'} ?`);
|
|
2169
|
+
params.push(range.max);
|
|
2170
|
+
}
|
|
2171
|
+
queryPlan.push(`NumericRange(object${rangeSuffix(range)})`);
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
conditions.push(`${alias}.kind IN (${OBJECT_RANGE_KINDS.map(() => '?').join(', ')})`);
|
|
2175
|
+
params.push(...OBJECT_RANGE_KINDS);
|
|
2176
|
+
if (range.min !== undefined) {
|
|
2177
|
+
conditions.push(`${alias}.value ${range.minInclusive ? '>=' : '>'} ?`);
|
|
2178
|
+
params.push(range.min);
|
|
2179
|
+
}
|
|
2180
|
+
if (range.max !== undefined) {
|
|
2181
|
+
conditions.push(`${alias}.value ${range.maxInclusive ? '<=' : '<'} ?`);
|
|
2182
|
+
params.push(range.max);
|
|
2183
|
+
}
|
|
2184
|
+
queryPlan.push(`LexicalRange(object${rangeSuffix(range)})`);
|
|
2185
|
+
}
|
|
2186
|
+
appendTermFilterJoinsAndConditions(resolved, keys, columnForKey, joins, conditions, params, queryPlan, aliasPrefix = 'term_filter') {
|
|
2187
|
+
for (const key of keys) {
|
|
2188
|
+
const filter = resolved.termFilters?.[key];
|
|
2189
|
+
if (!filter) {
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
const alias = `${aliasPrefix}_${key}`;
|
|
2193
|
+
joins.push(` JOIN rdf_terms ${alias} ON ${alias}.id = ${columnForKey(key)}`);
|
|
2194
|
+
this.appendTermFilterCondition(key, alias, filter, conditions, params, queryPlan);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
appendTermFilterCondition(key, alias, filter, conditions, params, queryPlan) {
|
|
2198
|
+
if (filter.termType !== undefined) {
|
|
2199
|
+
this.appendTermTypeCondition(key, alias, filter.termType, conditions, params);
|
|
2200
|
+
queryPlan?.push(`TermType(${key}:${filter.termType})`);
|
|
2201
|
+
}
|
|
2202
|
+
if (filter.language !== undefined) {
|
|
2203
|
+
this.appendLanguageCondition(key, alias, '$language', filter.language, conditions, params);
|
|
2204
|
+
queryPlan?.push(`Language(${key}$language)`);
|
|
2205
|
+
}
|
|
2206
|
+
if (filter.notLanguage !== undefined) {
|
|
2207
|
+
this.appendLanguageCondition(key, alias, '$notLanguage', filter.notLanguage, conditions, params);
|
|
2208
|
+
queryPlan?.push(`Language(${key}$notLanguage)`);
|
|
2209
|
+
}
|
|
2210
|
+
if (filter.langMatches !== undefined) {
|
|
2211
|
+
this.appendLanguageCondition(key, alias, '$langMatches', filter.langMatches, conditions, params);
|
|
2212
|
+
queryPlan?.push(`Language(${key}$langMatches)`);
|
|
2213
|
+
}
|
|
2214
|
+
if (filter.datatype !== undefined) {
|
|
2215
|
+
this.appendDatatypeCondition(key, alias, '$datatype', filter.datatype, conditions, params);
|
|
2216
|
+
queryPlan?.push(`Datatype(${key}$datatype)`);
|
|
2217
|
+
}
|
|
2218
|
+
if (filter.notDatatype !== undefined) {
|
|
2219
|
+
this.appendDatatypeCondition(key, alias, '$notDatatype', filter.notDatatype, conditions, params);
|
|
2220
|
+
queryPlan?.push(`Datatype(${key}$notDatatype)`);
|
|
2221
|
+
}
|
|
2222
|
+
for (const search of filter.textSearches ?? []) {
|
|
2223
|
+
this.appendTextSearchCondition(key, alias, search, conditions, params, queryPlan);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
appendTextSearchCondition(key, alias, search, conditions, params, queryPlan) {
|
|
2227
|
+
const kinds = termKindsForPatternKey(key);
|
|
2228
|
+
const kindPlaceholders = kinds.map(() => '?').join(', ');
|
|
2229
|
+
const normalized = search.value.toLowerCase();
|
|
2230
|
+
switch (search.operator) {
|
|
2231
|
+
case '$contains':
|
|
2232
|
+
conditions.push(`${alias}.kind IN (${kindPlaceholders})
|
|
2233
|
+
AND ${alias}.normalized_text LIKE ? ESCAPE '\\'
|
|
2234
|
+
AND instr(${alias}.value, ?) > 0`);
|
|
2235
|
+
params.push(...kinds, `%${escapeLikePattern(normalized)}%`, search.value);
|
|
2236
|
+
queryPlan?.push(`TextSearch(${key}$contains)`);
|
|
2237
|
+
return;
|
|
2238
|
+
case '$endsWith':
|
|
2239
|
+
conditions.push(`${alias}.kind IN (${kindPlaceholders})
|
|
2240
|
+
AND ${alias}.normalized_text LIKE ? ESCAPE '\\'
|
|
2241
|
+
AND substr(${alias}.value, -length(?)) = ?`);
|
|
2242
|
+
params.push(...kinds, `%${escapeLikePattern(normalized)}`, search.value, search.value);
|
|
2243
|
+
queryPlan?.push(`TextSearch(${key}$endsWith)`);
|
|
2244
|
+
return;
|
|
2245
|
+
default: {
|
|
2246
|
+
const exhaustive = search.operator;
|
|
2247
|
+
throw new Error(`Unsupported RDF-3X text search operator: ${exhaustive}`);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
appendTermTypeCondition(key, alias, termType, conditions, params) {
|
|
2252
|
+
const possibleKinds = termKindsForPatternKey(key);
|
|
2253
|
+
if (termType === 'numeric') {
|
|
2254
|
+
conditions.push(possibleKinds.includes('literal')
|
|
2255
|
+
? `${alias}.kind = 'literal' AND ${alias}.numeric_value IS NOT NULL`
|
|
2256
|
+
: '1 = 0');
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
if (!possibleKinds.includes(termType)) {
|
|
2260
|
+
conditions.push('1 = 0');
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
conditions.push(`${alias}.kind = ?`);
|
|
2264
|
+
params.push(termType);
|
|
2265
|
+
}
|
|
2266
|
+
appendLanguageCondition(key, alias, operator, language, conditions, params) {
|
|
2267
|
+
if (!termKindsForPatternKey(key).includes('literal')) {
|
|
2268
|
+
conditions.push('1 = 0');
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
if (operator === '$language') {
|
|
2272
|
+
conditions.push(`${alias}.kind = 'literal' AND COALESCE(${alias}.lang, '') = ?`);
|
|
2273
|
+
params.push(language);
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
if (operator === '$notLanguage') {
|
|
2277
|
+
conditions.push(`${alias}.kind = 'literal' AND COALESCE(${alias}.lang, '') != ?`);
|
|
2278
|
+
params.push(language);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
if (language === '*') {
|
|
2282
|
+
conditions.push(`${alias}.kind = 'literal' AND ${alias}.lang IS NOT NULL AND ${alias}.lang != ''`);
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
conditions.push(`${alias}.kind = 'literal'
|
|
2286
|
+
AND (lower(${alias}.lang) = lower(?) OR lower(${alias}.lang) LIKE lower(?) ESCAPE '\\')`);
|
|
2287
|
+
params.push(language, `${escapeLikePattern(language)}-%`);
|
|
2288
|
+
}
|
|
2289
|
+
appendDatatypeCondition(key, alias, operator, datatype, conditions, params) {
|
|
2290
|
+
if (!termKindsForPatternKey(key).includes('literal')) {
|
|
2291
|
+
conditions.push('1 = 0');
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
if (datatype.kind === 'xsd-string') {
|
|
2295
|
+
conditions.push(operator === '$datatype'
|
|
2296
|
+
? `${alias}.kind = 'literal' AND ${alias}.lang IS NULL AND ${alias}.datatype_id IS NULL`
|
|
2297
|
+
: `${alias}.kind = 'literal' AND NOT (${alias}.lang IS NULL AND ${alias}.datatype_id IS NULL)`);
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (datatype.kind === 'unknown') {
|
|
2301
|
+
conditions.push(operator === '$datatype' ? '1 = 0' : `${alias}.kind = 'literal'`);
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
conditions.push(operator === '$datatype'
|
|
2305
|
+
? `${alias}.kind = 'literal' AND ${alias}.datatype_id = ?`
|
|
2306
|
+
: `${alias}.kind = 'literal' AND (${alias}.datatype_id IS NULL OR ${alias}.datatype_id != ?)`);
|
|
2307
|
+
params.push(datatype.id);
|
|
2308
|
+
}
|
|
2309
|
+
resolveObjectRange(match) {
|
|
2310
|
+
const numericRange = { mode: 'numeric' };
|
|
2311
|
+
const lexicalRange = { mode: 'lexical' };
|
|
2312
|
+
let hasRange = false;
|
|
2313
|
+
let allNumeric = true;
|
|
2314
|
+
for (const [operator, inclusive] of [
|
|
2315
|
+
['$gt', false],
|
|
2316
|
+
['$gte', true],
|
|
2317
|
+
['$lt', false],
|
|
2318
|
+
['$lte', true],
|
|
2319
|
+
]) {
|
|
2320
|
+
const value = match[operator];
|
|
2321
|
+
if (value === undefined) {
|
|
2322
|
+
continue;
|
|
2323
|
+
}
|
|
2324
|
+
hasRange = true;
|
|
2325
|
+
const numericValue = this.numericValueForPattern(value);
|
|
2326
|
+
const lexicalValue = this.lexicalValueForPattern(value);
|
|
2327
|
+
allNumeric = allNumeric && numericValue !== undefined;
|
|
2328
|
+
if (lexicalValue === undefined)
|
|
2329
|
+
return undefined;
|
|
2330
|
+
if (operator === '$gt' || operator === '$gte') {
|
|
2331
|
+
if (numericValue !== undefined)
|
|
2332
|
+
numericRange.min = numericValue;
|
|
2333
|
+
numericRange.minInclusive = inclusive;
|
|
2334
|
+
lexicalRange.min = lexicalValue;
|
|
2335
|
+
lexicalRange.minInclusive = inclusive;
|
|
2336
|
+
}
|
|
2337
|
+
else {
|
|
2338
|
+
if (numericValue !== undefined)
|
|
2339
|
+
numericRange.max = numericValue;
|
|
2340
|
+
numericRange.maxInclusive = inclusive;
|
|
2341
|
+
lexicalRange.max = lexicalValue;
|
|
2342
|
+
lexicalRange.maxInclusive = inclusive;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
if (!hasRange)
|
|
2346
|
+
return undefined;
|
|
2347
|
+
return allNumeric ? numericRange : lexicalRange;
|
|
2348
|
+
}
|
|
2349
|
+
numericValueForPattern(value) {
|
|
2350
|
+
if (typeof value === 'number') {
|
|
2351
|
+
return Number.isFinite(value) ? value : undefined;
|
|
2352
|
+
}
|
|
2353
|
+
if (typeof value === 'string') {
|
|
2354
|
+
const parsed = Number(value);
|
|
2355
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2356
|
+
}
|
|
2357
|
+
if (value.termType !== 'Literal' || !(0, RdfTermSemantics_1.isRdfNumericDatatype)(value.datatype.value)) {
|
|
2358
|
+
return undefined;
|
|
2359
|
+
}
|
|
2360
|
+
const parsed = (0, RdfTermSemantics_1.rdfNumericValue)(value.value);
|
|
2361
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2362
|
+
}
|
|
2363
|
+
lexicalValueForPattern(value) {
|
|
2364
|
+
if (typeof value === 'number') {
|
|
2365
|
+
return Number.isFinite(value) ? String(value) : undefined;
|
|
2366
|
+
}
|
|
2367
|
+
if (typeof value === 'string') {
|
|
2368
|
+
return value;
|
|
2369
|
+
}
|
|
2370
|
+
return value.value;
|
|
2371
|
+
}
|
|
2372
|
+
choosePermutation(ids, constraints) {
|
|
2373
|
+
const has = (key) => ids[key] !== undefined || Boolean(constraints?.idSets?.[key]?.length);
|
|
2374
|
+
const hasObjectConstraint = has('object')
|
|
2375
|
+
|| Boolean(constraints?.objectRange)
|
|
2376
|
+
|| Boolean(constraints?.termFilters?.object);
|
|
2377
|
+
if (has('subject') && has('predicate'))
|
|
2378
|
+
return this.permutation('SPO');
|
|
2379
|
+
if (has('subject') && hasObjectConstraint)
|
|
2380
|
+
return this.permutation('SOP');
|
|
2381
|
+
if (has('predicate') && has('subject'))
|
|
2382
|
+
return this.permutation('PSO');
|
|
2383
|
+
if (has('predicate') && hasObjectConstraint)
|
|
2384
|
+
return this.permutation('POS');
|
|
2385
|
+
if (hasObjectConstraint && has('subject'))
|
|
2386
|
+
return this.permutation('OSP');
|
|
2387
|
+
if (hasObjectConstraint && has('predicate'))
|
|
2388
|
+
return this.permutation('OPS');
|
|
2389
|
+
if (has('subject'))
|
|
2390
|
+
return this.permutation('SPO');
|
|
2391
|
+
if (has('predicate'))
|
|
2392
|
+
return this.permutation('PSO');
|
|
2393
|
+
if (hasObjectConstraint)
|
|
2394
|
+
return this.permutation('OSP');
|
|
2395
|
+
return this.permutation('SPO');
|
|
2396
|
+
}
|
|
2397
|
+
permutation(name) {
|
|
2398
|
+
const permutation = PERMUTATIONS.find((candidate) => candidate.name === name);
|
|
2399
|
+
if (!permutation) {
|
|
2400
|
+
throw new Error(`Unknown RDF-3X permutation: ${name}`);
|
|
2401
|
+
}
|
|
2402
|
+
return permutation;
|
|
2403
|
+
}
|
|
2404
|
+
rowsToQuads(rows) {
|
|
2405
|
+
const termMap = this.requireDictionary().rowsForIds(rows.flatMap((row) => [
|
|
2406
|
+
row.graph_id,
|
|
2407
|
+
row.subject_id,
|
|
2408
|
+
row.predicate_id,
|
|
2409
|
+
row.object_id,
|
|
2410
|
+
]));
|
|
2411
|
+
return rows.map((row) => n3_1.DataFactory.quad(requiredTerm(termMap, row.subject_id), requiredTerm(termMap, row.predicate_id), requiredTerm(termMap, row.object_id), requiredTerm(termMap, row.graph_id)));
|
|
2412
|
+
}
|
|
2413
|
+
permutationSource(permutation, alias) {
|
|
2414
|
+
return `${RDF_FACTS_TABLE} AS ${alias} INDEXED BY ${permutation.indexName}`;
|
|
2415
|
+
}
|
|
2416
|
+
factSource(alias) {
|
|
2417
|
+
return `${RDF_FACTS_TABLE} AS ${alias}`;
|
|
2418
|
+
}
|
|
2419
|
+
uniqueTripleCount() {
|
|
2420
|
+
return this.requireDb().prepare(`
|
|
2421
|
+
SELECT COUNT(*) AS count
|
|
2422
|
+
FROM (
|
|
2423
|
+
SELECT DISTINCT subject_id, predicate_id, object_id
|
|
2424
|
+
FROM ${RDF_FACTS_TABLE}
|
|
2425
|
+
) distinct_triples
|
|
2426
|
+
`).get()?.count ?? 0;
|
|
2427
|
+
}
|
|
2428
|
+
rowCount(table) {
|
|
2429
|
+
return this.requireDb().prepare(`SELECT COUNT(*) AS count FROM ${table}`).get()?.count ?? 0;
|
|
2430
|
+
}
|
|
2431
|
+
collectPageCount() {
|
|
2432
|
+
try {
|
|
2433
|
+
return this.requireDb().prepare('PRAGMA page_count').get()?.page_count ?? 0;
|
|
2434
|
+
}
|
|
2435
|
+
catch {
|
|
2436
|
+
return 0;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
estimateDatabaseBytes() {
|
|
2440
|
+
const pageSize = this.estimatePageSize();
|
|
2441
|
+
const pageCount = this.collectPageCount();
|
|
2442
|
+
return pageSize * pageCount;
|
|
2443
|
+
}
|
|
2444
|
+
estimateSpaceObjectsFromSchema(schemaRows) {
|
|
2445
|
+
const pageSize = this.estimatePageSize();
|
|
2446
|
+
return schemaRows.map((object) => ({
|
|
2447
|
+
name: object.name,
|
|
2448
|
+
kind: rdf3xSpaceObjectKind(object.name, object.type, object.tbl_name),
|
|
2449
|
+
...(object.tbl_name && object.tbl_name !== object.name ? { tableName: object.tbl_name } : {}),
|
|
2450
|
+
pages: 1,
|
|
2451
|
+
bytes: pageSize,
|
|
2452
|
+
estimated: true,
|
|
2453
|
+
}));
|
|
2454
|
+
}
|
|
2455
|
+
estimatePageSize() {
|
|
2456
|
+
try {
|
|
2457
|
+
return this.requireDb().prepare('PRAGMA page_size').get()?.page_size ?? 4096;
|
|
2458
|
+
}
|
|
2459
|
+
catch {
|
|
2460
|
+
return 4096;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
metrics(indexChoice, matchedRows, returnedRows, start, queryPlan) {
|
|
2464
|
+
return {
|
|
2465
|
+
engine: 'solid-rdf3x',
|
|
2466
|
+
indexChoice,
|
|
2467
|
+
matchedRows,
|
|
2468
|
+
returnedRows,
|
|
2469
|
+
durationMs: Date.now() - start,
|
|
2470
|
+
queryPlan,
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
requireDb() {
|
|
2474
|
+
if (!this.db) {
|
|
2475
|
+
throw new Error('Rdf3xIndex is not open');
|
|
2476
|
+
}
|
|
2477
|
+
return this.db;
|
|
2478
|
+
}
|
|
2479
|
+
requireDictionary() {
|
|
2480
|
+
if (!this.dictionary) {
|
|
2481
|
+
throw new Error('Rdf3xIndex is not open');
|
|
2482
|
+
}
|
|
2483
|
+
return this.dictionary;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
exports.Rdf3xIndex = Rdf3xIndex;
|
|
2487
|
+
function keyForColumn(column) {
|
|
2488
|
+
if (column === 'subject_id')
|
|
2489
|
+
return 'subject';
|
|
2490
|
+
if (column === 'predicate_id')
|
|
2491
|
+
return 'predicate';
|
|
2492
|
+
return 'object';
|
|
2493
|
+
}
|
|
2494
|
+
function shouldUseMembershipSource(resolved) {
|
|
2495
|
+
return resolved.ids.graph !== undefined
|
|
2496
|
+
|| Boolean(resolved.idSets?.graph?.length)
|
|
2497
|
+
|| Boolean(resolved.excludedIdSets?.graph?.length)
|
|
2498
|
+
|| resolved.graphPrefix !== undefined;
|
|
2499
|
+
}
|
|
2500
|
+
function requiredTerm(termMap, id) {
|
|
2501
|
+
const term = termMap.get(id);
|
|
2502
|
+
if (!term) {
|
|
2503
|
+
throw new Error(`RDF term not found while reading RDF-3X index: ${id}`);
|
|
2504
|
+
}
|
|
2505
|
+
return term;
|
|
2506
|
+
}
|
|
2507
|
+
function isRdfTerm(value) {
|
|
2508
|
+
return value !== null && typeof value === 'object' && 'termType' in value;
|
|
2509
|
+
}
|
|
2510
|
+
function isTermInPattern(value) {
|
|
2511
|
+
return value !== null
|
|
2512
|
+
&& typeof value === 'object'
|
|
2513
|
+
&& !('termType' in value)
|
|
2514
|
+
&& Object.keys(value).length === 1
|
|
2515
|
+
&& Array.isArray(value.$in)
|
|
2516
|
+
&& (value.$in).every(isRdfTerm);
|
|
2517
|
+
}
|
|
2518
|
+
function isTermNotInPattern(value) {
|
|
2519
|
+
return value !== null
|
|
2520
|
+
&& typeof value === 'object'
|
|
2521
|
+
&& !('termType' in value)
|
|
2522
|
+
&& Object.keys(value).length === 1
|
|
2523
|
+
&& Array.isArray(value.$notIn)
|
|
2524
|
+
&& (value.$notIn).every(isRdfTerm);
|
|
2525
|
+
}
|
|
2526
|
+
function isOperatorPattern(value) {
|
|
2527
|
+
return value !== null && typeof value === 'object' && !('termType' in value);
|
|
2528
|
+
}
|
|
2529
|
+
function isSupportedOperatorPattern(key, value) {
|
|
2530
|
+
const allowed = new Set([
|
|
2531
|
+
'$in',
|
|
2532
|
+
'$notIn',
|
|
2533
|
+
'$termType',
|
|
2534
|
+
'$language',
|
|
2535
|
+
'$notLanguage',
|
|
2536
|
+
'$langMatches',
|
|
2537
|
+
'$datatype',
|
|
2538
|
+
'$notDatatype',
|
|
2539
|
+
...(key === 'graph' ? ['$startsWith'] : []),
|
|
2540
|
+
...(key === 'object' ? ['$gt', '$gte', '$lt', '$lte', '$contains', '$endsWith'] : []),
|
|
2541
|
+
]);
|
|
2542
|
+
if (Object.keys(value).some((operator) => !allowed.has(operator))) {
|
|
2543
|
+
return false;
|
|
2544
|
+
}
|
|
2545
|
+
if (value.$in !== undefined && (!Array.isArray(value.$in) || !value.$in.every(isRdfTerm) || value.$in.length === 0)) {
|
|
2546
|
+
return false;
|
|
2547
|
+
}
|
|
2548
|
+
if (value.$notIn !== undefined && (!Array.isArray(value.$notIn) || !value.$notIn.every(isRdfTerm) || value.$notIn.length === 0)) {
|
|
2549
|
+
return false;
|
|
2550
|
+
}
|
|
2551
|
+
if (value.$startsWith !== undefined && typeof value.$startsWith !== 'string') {
|
|
2552
|
+
return false;
|
|
2553
|
+
}
|
|
2554
|
+
if (value.$termType !== undefined && !['iri', 'blank', 'literal', 'numeric'].includes(value.$termType)) {
|
|
2555
|
+
return false;
|
|
2556
|
+
}
|
|
2557
|
+
for (const languageOperator of ['$language', '$notLanguage', '$langMatches']) {
|
|
2558
|
+
if (value[languageOperator] !== undefined && typeof value[languageOperator] !== 'string') {
|
|
2559
|
+
return false;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
for (const datatypeOperator of ['$datatype', '$notDatatype']) {
|
|
2563
|
+
if (value[datatypeOperator] !== undefined) {
|
|
2564
|
+
const datatype = value[datatypeOperator];
|
|
2565
|
+
if (!isRdfTerm(datatype) || datatype.termType !== 'NamedNode') {
|
|
2566
|
+
return false;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
if (key === 'object') {
|
|
2571
|
+
for (const rangeOperator of ['$gt', '$gte', '$lt', '$lte']) {
|
|
2572
|
+
const rangeValue = value[rangeOperator];
|
|
2573
|
+
if (rangeValue !== undefined && !isRdf3xObjectRangeValue(rangeValue)) {
|
|
2574
|
+
return false;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
for (const textOperator of ['$contains', '$endsWith']) {
|
|
2578
|
+
if (value[textOperator] !== undefined && typeof value[textOperator] !== 'string') {
|
|
2579
|
+
return false;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return Object.keys(value).length > 0;
|
|
2584
|
+
}
|
|
2585
|
+
function hasObjectRangeOperator(value) {
|
|
2586
|
+
return value.$gt !== undefined
|
|
2587
|
+
|| value.$gte !== undefined
|
|
2588
|
+
|| value.$lt !== undefined
|
|
2589
|
+
|| value.$lte !== undefined;
|
|
2590
|
+
}
|
|
2591
|
+
function termKindsForPatternKey(key) {
|
|
2592
|
+
switch (key) {
|
|
2593
|
+
case 'object':
|
|
2594
|
+
return ['iri', 'literal', 'blank'];
|
|
2595
|
+
case 'subject':
|
|
2596
|
+
return ['iri', 'blank'];
|
|
2597
|
+
case 'graph':
|
|
2598
|
+
return ['iri', 'default_graph'];
|
|
2599
|
+
case 'predicate':
|
|
2600
|
+
return ['iri'];
|
|
2601
|
+
default: {
|
|
2602
|
+
const exhaustive = key;
|
|
2603
|
+
throw new Error(`Unsupported RDF-3X pattern key: ${exhaustive}`);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
function escapeLikePattern(value) {
|
|
2608
|
+
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
2609
|
+
}
|
|
2610
|
+
function hasResolvedIdSets(resolved) {
|
|
2611
|
+
return Object.values(resolved.idSets ?? {}).some((ids) => (ids?.length ?? 0) > 0);
|
|
2612
|
+
}
|
|
2613
|
+
function hasResolvedExcludedIdSets(resolved) {
|
|
2614
|
+
return Object.values(resolved.excludedIdSets ?? {}).some((ids) => (ids?.length ?? 0) > 0);
|
|
2615
|
+
}
|
|
2616
|
+
function hasResolvedTermFilters(resolved) {
|
|
2617
|
+
return Object.values(resolved.termFilters ?? {}).some(Boolean);
|
|
2618
|
+
}
|
|
2619
|
+
function uniqueNumbers(values) {
|
|
2620
|
+
return [...new Set(values)];
|
|
2621
|
+
}
|
|
2622
|
+
function pairProjectionRowTotal(rows) {
|
|
2623
|
+
return Object.values(rows).reduce((sum, count) => sum + count, 0);
|
|
2624
|
+
}
|
|
2625
|
+
function termProjectionRowTotal(rows) {
|
|
2626
|
+
return Object.values(rows).reduce((sum, count) => sum + count, 0);
|
|
2627
|
+
}
|
|
2628
|
+
function sumSpaceObjects(objects, kind) {
|
|
2629
|
+
return objects
|
|
2630
|
+
.filter((object) => object.kind === kind)
|
|
2631
|
+
.reduce((sum, object) => sum + object.bytes, 0);
|
|
2632
|
+
}
|
|
2633
|
+
function uniquePatternKeys(values) {
|
|
2634
|
+
return ['graph', 'subject', 'predicate', 'object']
|
|
2635
|
+
.filter((key) => values.includes(key));
|
|
2636
|
+
}
|
|
2637
|
+
function uniqueVariableNames(values) {
|
|
2638
|
+
return [...new Set(values)];
|
|
2639
|
+
}
|
|
2640
|
+
function joinSolutionMappingKeyExpression(variableColumns, variables, errorPrefix) {
|
|
2641
|
+
const variableNames = uniqueVariableNames(variables ?? [...variableColumns.keys()]);
|
|
2642
|
+
if (variableNames.length === 0) {
|
|
2643
|
+
return '1';
|
|
2644
|
+
}
|
|
2645
|
+
return variableNames.map((variableName) => {
|
|
2646
|
+
const column = variableColumns.get(variableName);
|
|
2647
|
+
if (!column) {
|
|
2648
|
+
throw new Error(`${errorPrefix} COUNT(DISTINCT *) cannot read unbound variable: ${variableName}`);
|
|
2649
|
+
}
|
|
2650
|
+
return column;
|
|
2651
|
+
}).join(` || ':' || `);
|
|
2652
|
+
}
|
|
2653
|
+
function rdf3xSpaceObjectKind(name, schemaType, tableName) {
|
|
2654
|
+
if (schemaType === 'table' && name.startsWith('rdf3x_')) {
|
|
2655
|
+
return 'table';
|
|
2656
|
+
}
|
|
2657
|
+
if (schemaType === 'index' && (name.startsWith('rdf3x_') || tableName?.startsWith('rdf3x_'))) {
|
|
2658
|
+
return 'index';
|
|
2659
|
+
}
|
|
2660
|
+
if (name.startsWith('sqlite_')) {
|
|
2661
|
+
return 'internal';
|
|
2662
|
+
}
|
|
2663
|
+
return 'unknown';
|
|
2664
|
+
}
|
|
2665
|
+
function isGraphPrefixPattern(value) {
|
|
2666
|
+
return value !== null
|
|
2667
|
+
&& typeof value === 'object'
|
|
2668
|
+
&& Object.keys(value).length === 1
|
|
2669
|
+
&& '$startsWith' in value
|
|
2670
|
+
&& typeof value.$startsWith === 'string';
|
|
2671
|
+
}
|
|
2672
|
+
function rangeSuffix(range) {
|
|
2673
|
+
return `${range.min !== undefined ? (range.minInclusive ? '$gte' : '$gt') : ''}${range.max !== undefined ? (range.maxInclusive ? '$lte' : '$lt') : ''}`;
|
|
2674
|
+
}
|
|
2675
|
+
function describeScanOrder(options) {
|
|
2676
|
+
const order = options?.order ?? [];
|
|
2677
|
+
const directions = options?.orderDirections ?? order.map(() => (options?.reverse ? 'desc' : 'asc'));
|
|
2678
|
+
return order.map((entry, index) => `${directions[index] ?? 'asc'}:${entry}`).join(',');
|
|
2679
|
+
}
|
|
2680
|
+
function isObjectRangePattern(value) {
|
|
2681
|
+
return value !== null
|
|
2682
|
+
&& typeof value === 'object'
|
|
2683
|
+
&& !('termType' in value)
|
|
2684
|
+
&& ['$gt', '$gte', '$lt', '$lte'].some((operator) => operator in value);
|
|
2685
|
+
}
|
|
2686
|
+
function isRdf3xObjectRangeValue(value) {
|
|
2687
|
+
if (typeof value === 'number') {
|
|
2688
|
+
return Number.isFinite(value);
|
|
2689
|
+
}
|
|
2690
|
+
if (typeof value === 'string') {
|
|
2691
|
+
return true;
|
|
2692
|
+
}
|
|
2693
|
+
return isRdfTerm(value);
|
|
2694
|
+
}
|
|
2695
|
+
//# sourceMappingURL=Rdf3xIndex.js.map
|