@undefineds.co/xpod 0.3.18 → 0.3.22

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.
Files changed (99) hide show
  1. package/config/bun.json +57 -11
  2. package/config/cloud.json +14 -12
  3. package/config/local.json +16 -14
  4. package/config/xpod.json +47 -9
  5. package/dist/api/matrix/PodMatrixStore.d.ts +4 -7
  6. package/dist/api/matrix/PodMatrixStore.js +116 -148
  7. package/dist/api/matrix/PodMatrixStore.js.map +1 -1
  8. package/dist/api/matrix/types.d.ts +2 -0
  9. package/dist/api/matrix/types.js.map +1 -1
  10. package/dist/components/components.jsonld +3 -0
  11. package/dist/components/context.jsonld +71 -32
  12. package/dist/http/SubgraphSparqlHttpHandler.d.ts +1 -0
  13. package/dist/http/SubgraphSparqlHttpHandler.js +27 -4
  14. package/dist/http/SubgraphSparqlHttpHandler.js.map +1 -1
  15. package/dist/http/SubgraphSparqlHttpHandler.jsonld +4 -0
  16. package/dist/http/vector/VectorHttpHandler.d.ts +5 -1
  17. package/dist/http/vector/VectorHttpHandler.js +5 -5
  18. package/dist/http/vector/VectorHttpHandler.js.map +1 -1
  19. package/dist/http/vector/VectorHttpHandler.jsonld +40 -28
  20. package/dist/index.d.ts +5 -2
  21. package/dist/index.js +9 -4
  22. package/dist/index.js.map +1 -1
  23. package/dist/runtime/Proxy.d.ts +3 -0
  24. package/dist/runtime/Proxy.js +31 -7
  25. package/dist/runtime/Proxy.js.map +1 -1
  26. package/dist/storage/SparqlUpdateResourceStore.js +94 -33
  27. package/dist/storage/SparqlUpdateResourceStore.js.map +1 -1
  28. package/dist/storage/accessors/MixDataAccessor.d.ts +22 -5
  29. package/dist/storage/accessors/MixDataAccessor.js +376 -61
  30. package/dist/storage/accessors/MixDataAccessor.js.map +1 -1
  31. package/dist/storage/accessors/MixDataAccessor.jsonld +73 -5
  32. package/dist/storage/accessors/QuadstoreSparqlDataAccessor.js +32 -10
  33. package/dist/storage/accessors/QuadstoreSparqlDataAccessor.js.map +1 -1
  34. package/dist/storage/accessors/QuintStoreSparqlDataAccessor.js +28 -6
  35. package/dist/storage/accessors/QuintStoreSparqlDataAccessor.js.map +1 -1
  36. package/dist/storage/accessors/SolidRdfDataAccessor.d.ts +45 -0
  37. package/dist/storage/accessors/SolidRdfDataAccessor.js +277 -0
  38. package/dist/storage/accessors/SolidRdfDataAccessor.js.map +1 -0
  39. package/dist/storage/accessors/SolidRdfDataAccessor.jsonld +161 -0
  40. package/dist/storage/rdf/Rdf3xIndex.d.ts +122 -0
  41. package/dist/storage/rdf/Rdf3xIndex.js +2695 -0
  42. package/dist/storage/rdf/Rdf3xIndex.js.map +1 -0
  43. package/dist/storage/rdf/Rdf3xIndex.jsonld +528 -0
  44. package/dist/storage/rdf/Rdf3xSchema.d.ts +20 -0
  45. package/dist/storage/rdf/Rdf3xSchema.js +65 -0
  46. package/dist/storage/rdf/Rdf3xSchema.js.map +1 -0
  47. package/dist/storage/rdf/RdfLocalQueryEngine.d.ts +10 -4
  48. package/dist/storage/rdf/RdfLocalQueryEngine.js +607 -127
  49. package/dist/storage/rdf/RdfLocalQueryEngine.js.map +1 -1
  50. package/dist/storage/rdf/RdfQuadIndex.d.ts +12 -1
  51. package/dist/storage/rdf/RdfQuadIndex.js +152 -22
  52. package/dist/storage/rdf/RdfQuadIndex.js.map +1 -1
  53. package/dist/storage/rdf/RdfQuadIndex.jsonld +36 -4
  54. package/dist/storage/rdf/RdfSparqlAdapter.d.ts +20 -2
  55. package/dist/storage/rdf/RdfSparqlAdapter.js +364 -40
  56. package/dist/storage/rdf/RdfSparqlAdapter.js.map +1 -1
  57. package/dist/storage/rdf/RdfSparqlAdapter.jsonld +60 -0
  58. package/dist/storage/rdf/RdfTermDictionary.d.ts +8 -0
  59. package/dist/storage/rdf/RdfTermDictionary.js +141 -70
  60. package/dist/storage/rdf/RdfTermDictionary.js.map +1 -1
  61. package/dist/storage/rdf/RdfTermDictionary.jsonld +24 -0
  62. package/dist/storage/rdf/RdfTextIndex.js +10 -3
  63. package/dist/storage/rdf/RdfTextIndex.js.map +1 -1
  64. package/dist/storage/rdf/SolidRdfEngine.d.ts +15 -6
  65. package/dist/storage/rdf/SolidRdfEngine.js +218 -25
  66. package/dist/storage/rdf/SolidRdfEngine.js.map +1 -1
  67. package/dist/storage/rdf/SolidRdfEngine.jsonld +70 -7
  68. package/dist/storage/rdf/SolidRdfSparqlEngine.d.ts +11 -7
  69. package/dist/storage/rdf/SolidRdfSparqlEngine.js +60 -47
  70. package/dist/storage/rdf/SolidRdfSparqlEngine.js.map +1 -1
  71. package/dist/storage/rdf/SolidRdfSparqlEngine.jsonld +9 -5
  72. package/dist/storage/rdf/index.d.ts +2 -2
  73. package/dist/storage/rdf/index.js +3 -3
  74. package/dist/storage/rdf/index.js.map +1 -1
  75. package/dist/storage/rdf/models-benchmark.d.ts +12 -1
  76. package/dist/storage/rdf/models-benchmark.js +549 -32
  77. package/dist/storage/rdf/models-benchmark.js.map +1 -1
  78. package/dist/storage/rdf/types.d.ts +81 -7
  79. package/dist/storage/rdf/types.js.map +1 -1
  80. package/dist/storage/sparql/CompatibilitySparqlEngine.d.ts +36 -0
  81. package/dist/storage/sparql/CompatibilitySparqlEngine.js +96 -0
  82. package/dist/storage/sparql/CompatibilitySparqlEngine.js.map +1 -0
  83. package/dist/storage/sparql/CompatibilitySparqlEngine.jsonld +123 -0
  84. package/dist/storage/sparql/CompatibilitySparqlEngineImpl.d.ts +35 -0
  85. package/dist/storage/sparql/CompatibilitySparqlEngineImpl.js +112 -0
  86. package/dist/storage/sparql/CompatibilitySparqlEngineImpl.js.map +1 -0
  87. package/dist/storage/sparql/SubgraphQueryEngine.d.ts +1 -36
  88. package/dist/storage/sparql/SubgraphQueryEngine.js +2 -115
  89. package/dist/storage/sparql/SubgraphQueryEngine.js.map +1 -1
  90. package/dist/storage/sparql/SubgraphQueryEngine.jsonld +1 -124
  91. package/dist/terminal/AclPermissionService.d.ts +2 -1
  92. package/dist/terminal/AclPermissionService.js +26 -3
  93. package/dist/terminal/AclPermissionService.js.map +1 -1
  94. package/dist/terminal/TerminalSessionManager.js +25 -3
  95. package/dist/terminal/TerminalSessionManager.js.map +1 -1
  96. package/package.json +1 -1
  97. package/dist/storage/rdf/Rdf3xTripleIndex.d.ts +0 -55
  98. package/dist/storage/rdf/Rdf3xTripleIndex.js +0 -1235
  99. 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