@undefineds.co/xpod 0.3.6 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/cli.json +1 -1
- package/config/cloud.json +54 -22
- package/config/local.json +56 -12
- package/config/resolver.json +10 -2
- package/config/xpod.base.json +50 -0
- package/config/xpod.json +8 -8
- package/dist/agents/config/resolve.js +10 -10
- package/dist/agents/config/resolve.js.map +1 -1
- package/dist/api/chatkit/index.d.ts +1 -1
- package/dist/api/chatkit/index.js.map +1 -1
- package/dist/api/chatkit/pod-store.d.ts +14 -11
- package/dist/api/chatkit/pod-store.js +114 -78
- package/dist/api/chatkit/pod-store.js.map +1 -1
- package/dist/api/chatkit/runtime/AcpAgentRuntime.js +1 -1
- package/dist/api/chatkit/runtime/AcpAgentRuntime.js.map +1 -1
- package/dist/api/chatkit/service.js +1 -1
- package/dist/api/chatkit/service.js.map +1 -1
- package/dist/api/chatkit/types.d.ts +11 -11
- package/dist/api/chatkit/types.js +3 -3
- package/dist/api/chatkit/types.js.map +1 -1
- package/dist/api/container/cloud.js +0 -8
- package/dist/api/container/cloud.js.map +1 -1
- package/dist/api/container/index.js +2 -1
- package/dist/api/container/index.js.map +1 -1
- package/dist/api/container/local.js +0 -7
- package/dist/api/container/local.js.map +1 -1
- package/dist/api/container/routes.js +3 -17
- package/dist/api/container/routes.js.map +1 -1
- package/dist/api/container/types.d.ts +0 -2
- package/dist/api/container/types.js.map +1 -1
- package/dist/api/handlers/PodManagementHandler.d.ts +3 -0
- package/dist/api/handlers/PodManagementHandler.js +71 -1
- package/dist/api/handlers/PodManagementHandler.js.map +1 -1
- package/dist/api/handlers/RunHandler.js +5 -5
- package/dist/api/handlers/RunHandler.js.map +1 -1
- package/dist/api/runs/AgentRuntimeTypes.d.ts +7 -8
- package/dist/api/runs/AgentRuntimeTypes.js.map +1 -1
- package/dist/api/runs/InngestRunExecutionBackend.d.ts +2 -2
- package/dist/api/runs/ManagedRunWorker.d.ts +1 -1
- package/dist/api/runs/ManagedRunWorker.js +6 -6
- package/dist/api/runs/ManagedRunWorker.js.map +1 -1
- package/dist/api/runs/PiAgentRuntimeDriver.d.ts +16 -1
- package/dist/api/runs/PiAgentRuntimeDriver.js +182 -23
- package/dist/api/runs/PiAgentRuntimeDriver.js.map +1 -1
- package/dist/api/runs/RunStateCenter.d.ts +3 -3
- package/dist/api/runs/RunStateCenter.js +13 -13
- package/dist/api/runs/RunStateCenter.js.map +1 -1
- package/dist/api/runs/store.d.ts +4 -4
- package/dist/api/runs/store.js +2 -2
- package/dist/api/runs/store.js.map +1 -1
- package/dist/api/service/VectorStoreService.d.ts +1 -1
- package/dist/api/service/VectorStoreService.js +16 -16
- package/dist/api/service/VectorStoreService.js.map +1 -1
- package/dist/api/tasks/InngestTaskScheduler.d.ts +4 -4
- package/dist/api/tasks/TaskMaterializer.d.ts +3 -3
- package/dist/api/tasks/TaskMaterializer.js +11 -11
- package/dist/api/tasks/TaskMaterializer.js.map +1 -1
- package/dist/api/tasks/TaskService.d.ts +3 -3
- package/dist/api/tasks/TaskService.js +11 -7
- package/dist/api/tasks/TaskService.js.map +1 -1
- package/dist/api/tasks/store.d.ts +10 -4
- package/dist/api/tasks/store.js +14 -4
- package/dist/api/tasks/store.js.map +1 -1
- package/dist/api/workspace/types.d.ts +3 -3
- package/dist/api/workspace/types.js +6 -6
- package/dist/api/workspace/types.js.map +1 -1
- package/dist/cli/commands/config.js +2 -2
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/start.js +9 -3
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/components/components.jsonld +8 -2
- package/dist/components/context.jsonld +308 -51
- package/dist/http/search/SearchHttpHandler.js +8 -8
- package/dist/http/search/SearchHttpHandler.js.map +1 -1
- package/dist/identity/drizzle/PodLookupRepository.d.ts +11 -1
- package/dist/identity/drizzle/PodLookupRepository.js +95 -4
- package/dist/identity/drizzle/PodLookupRepository.js.map +1 -1
- package/dist/identity/drizzle/db.js +4 -43
- package/dist/identity/drizzle/db.js.map +1 -1
- package/dist/identity/drizzle/schema.pg.d.ts +0 -5
- package/dist/identity/drizzle/schema.pg.js +2 -16
- package/dist/identity/drizzle/schema.pg.js.map +1 -1
- package/dist/identity/drizzle/schema.sqlite.d.ts +19 -176
- package/dist/identity/drizzle/schema.sqlite.js +2 -16
- package/dist/identity/drizzle/schema.sqlite.js.map +1 -1
- package/dist/identity/oidc/AutoDetectIdentityProviderHandler.d.ts +4 -4
- package/dist/identity/oidc/AutoDetectIdentityProviderHandler.js +7 -7
- package/dist/identity/oidc/AutoDetectIdentityProviderHandler.js.map +1 -1
- package/dist/identity/oidc/AutoDetectIdentityProviderHandler.jsonld +6 -6
- package/dist/identity/oidc/AutoDetectOidcHandler.d.ts +4 -4
- package/dist/identity/oidc/AutoDetectOidcHandler.js +6 -6
- package/dist/identity/oidc/AutoDetectOidcHandler.js.map +1 -1
- package/dist/identity/oidc/AutoDetectOidcHandler.jsonld +6 -6
- package/dist/identity/oidc/ScopedPickWebIdHandler.d.ts +37 -0
- package/dist/identity/oidc/ScopedPickWebIdHandler.js +211 -0
- package/dist/identity/oidc/ScopedPickWebIdHandler.js.map +1 -0
- package/dist/identity/oidc/ScopedPickWebIdHandler.jsonld +158 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/main.js +8 -2
- package/dist/main.js.map +1 -1
- package/dist/provision/ProvisionPodCreator.d.ts +3 -4
- package/dist/provision/ProvisionPodCreator.js +8 -13
- package/dist/provision/ProvisionPodCreator.js.map +1 -1
- package/dist/provision/ProvisionPodCreator.jsonld +7 -7
- package/dist/runtime/Proxy.d.ts +0 -1
- package/dist/runtime/Proxy.js +0 -9
- package/dist/runtime/Proxy.js.map +1 -1
- package/dist/runtime/bootstrap.d.ts +1 -0
- package/dist/runtime/bootstrap.js +5 -2
- package/dist/runtime/bootstrap.js.map +1 -1
- package/dist/runtime/css-process.d.ts +12 -4
- package/dist/runtime/css-process.js +61 -14
- package/dist/runtime/css-process.js.map +1 -1
- package/dist/runtime/oidc-issuer.d.ts +3 -2
- package/dist/runtime/oidc-issuer.js +3 -2
- package/dist/runtime/oidc-issuer.js.map +1 -1
- package/dist/runtime/runtime-types.d.ts +1 -0
- package/dist/runtime/runtime-types.js.map +1 -1
- package/dist/solidfs/LocalFirstRdfRepresentationResolver.d.ts +21 -0
- package/dist/solidfs/LocalFirstRdfRepresentationResolver.js +38 -0
- package/dist/solidfs/LocalFirstRdfRepresentationResolver.js.map +1 -0
- package/dist/solidfs/LocalSolidFS.d.ts +18 -0
- package/dist/solidfs/LocalSolidFS.js +539 -0
- package/dist/solidfs/LocalSolidFS.js.map +1 -0
- package/dist/solidfs/PodSolidFsHttpClient.d.ts +16 -0
- package/dist/solidfs/PodSolidFsHttpClient.js +93 -0
- package/dist/solidfs/PodSolidFsHttpClient.js.map +1 -0
- package/dist/solidfs/PodSolidFsHydrator.d.ts +27 -0
- package/dist/solidfs/PodSolidFsHydrator.js +127 -0
- package/dist/solidfs/PodSolidFsHydrator.js.map +1 -0
- package/dist/solidfs/PodSolidFsSyncer.d.ts +21 -0
- package/dist/solidfs/PodSolidFsSyncer.js +78 -0
- package/dist/solidfs/PodSolidFsSyncer.js.map +1 -0
- package/dist/solidfs/RdfIndexSolidFsSyncer.d.ts +22 -0
- package/dist/solidfs/RdfIndexSolidFsSyncer.js +131 -0
- package/dist/solidfs/RdfIndexSolidFsSyncer.js.map +1 -0
- package/dist/solidfs/index.d.ts +7 -0
- package/dist/solidfs/index.js +24 -0
- package/dist/solidfs/index.js.map +1 -0
- package/dist/solidfs/types.d.ts +131 -0
- package/dist/solidfs/types.js +19 -0
- package/dist/solidfs/types.js.map +1 -0
- package/dist/storage/RepresentationPartialConvertingStore.js +6 -13
- package/dist/storage/RepresentationPartialConvertingStore.js.map +1 -1
- package/dist/storage/SparqlUpdateResourceStore.d.ts +4 -0
- package/dist/storage/SparqlUpdateResourceStore.js +13 -0
- package/dist/storage/SparqlUpdateResourceStore.js.map +1 -1
- package/dist/storage/SparqlUpdateResourceStore.jsonld +26 -0
- package/dist/storage/accessors/MixDataAccessor.d.ts +85 -4
- package/dist/storage/accessors/MixDataAccessor.js +511 -16
- package/dist/storage/accessors/MixDataAccessor.js.map +1 -1
- package/dist/storage/accessors/MixDataAccessor.jsonld +176 -1
- package/dist/storage/accessors/QuintStoreSparqlDataAccessor.d.ts +7 -0
- package/dist/storage/accessors/QuintStoreSparqlDataAccessor.js +72 -4
- package/dist/storage/accessors/QuintStoreSparqlDataAccessor.js.map +1 -1
- package/dist/storage/accessors/QuintStoreSparqlDataAccessor.jsonld +24 -0
- package/dist/storage/quint/BaseQuintStore.d.ts +3 -0
- package/dist/storage/quint/BaseQuintStore.js +51 -27
- package/dist/storage/quint/BaseQuintStore.js.map +1 -1
- package/dist/storage/quint/PgQuintStore.d.ts +1 -0
- package/dist/storage/quint/PgQuintStore.js +50 -32
- package/dist/storage/quint/PgQuintStore.js.map +1 -1
- package/dist/storage/quint/PgQuintStore.jsonld +4 -3
- package/dist/storage/quint/SqliteQuintStore.d.ts +5 -0
- package/dist/storage/quint/SqliteQuintStore.js +100 -0
- package/dist/storage/quint/SqliteQuintStore.js.map +1 -1
- package/dist/storage/quint/SqliteQuintStore.jsonld +20 -0
- package/dist/storage/quint/types.d.ts +16 -0
- package/dist/storage/quint/types.js.map +1 -1
- package/dist/storage/rdf/Rdf3xTripleIndex.d.ts +55 -0
- package/dist/storage/rdf/Rdf3xTripleIndex.js +1235 -0
- package/dist/storage/rdf/Rdf3xTripleIndex.js.map +1 -0
- package/dist/storage/rdf/RdfContentTypes.d.ts +9 -0
- package/dist/storage/rdf/RdfContentTypes.js +79 -0
- package/dist/storage/rdf/RdfContentTypes.js.map +1 -0
- package/dist/storage/rdf/RdfLocalQueryEngine.d.ts +79 -0
- package/dist/storage/rdf/RdfLocalQueryEngine.js +2705 -0
- package/dist/storage/rdf/RdfLocalQueryEngine.js.map +1 -0
- package/dist/storage/rdf/RdfQuadIndex.d.ts +98 -0
- package/dist/storage/rdf/RdfQuadIndex.js +1840 -0
- package/dist/storage/rdf/RdfQuadIndex.js.map +1 -0
- package/dist/storage/rdf/RdfQuadIndex.jsonld +416 -0
- package/dist/storage/rdf/RdfShadowComparator.d.ts +12 -0
- package/dist/storage/rdf/RdfShadowComparator.js +47 -0
- package/dist/storage/rdf/RdfShadowComparator.js.map +1 -0
- package/dist/storage/rdf/RdfSparqlAdapter.d.ts +147 -0
- package/dist/storage/rdf/RdfSparqlAdapter.js +2420 -0
- package/dist/storage/rdf/RdfSparqlAdapter.js.map +1 -0
- package/dist/storage/rdf/RdfSparqlAdapter.jsonld +414 -0
- package/dist/storage/rdf/RdfTermDictionary.d.ts +27 -0
- package/dist/storage/rdf/RdfTermDictionary.js +352 -0
- package/dist/storage/rdf/RdfTermDictionary.js.map +1 -0
- package/dist/storage/rdf/RdfTermDictionary.jsonld +114 -0
- package/dist/storage/rdf/RdfTermSemantics.d.ts +6 -0
- package/dist/storage/rdf/RdfTermSemantics.js +40 -0
- package/dist/storage/rdf/RdfTermSemantics.js.map +1 -0
- package/dist/storage/rdf/RdfTextIndex.d.ts +23 -0
- package/dist/storage/rdf/RdfTextIndex.js +569 -0
- package/dist/storage/rdf/RdfTextIndex.js.map +1 -0
- package/dist/storage/rdf/RdfVectorIndex.d.ts +22 -0
- package/dist/storage/rdf/RdfVectorIndex.js +631 -0
- package/dist/storage/rdf/RdfVectorIndex.js.map +1 -0
- package/dist/storage/rdf/RdfXmlSerializer.d.ts +2 -0
- package/dist/storage/rdf/RdfXmlSerializer.js +123 -0
- package/dist/storage/rdf/RdfXmlSerializer.js.map +1 -0
- package/dist/storage/rdf/ShadowRdfQuintStore.d.ts +58 -0
- package/dist/storage/rdf/ShadowRdfQuintStore.js +202 -0
- package/dist/storage/rdf/ShadowRdfQuintStore.js.map +1 -0
- package/dist/storage/rdf/ShadowRdfQuintStore.jsonld +308 -0
- package/dist/storage/rdf/SolidRdfEngine.d.ts +56 -0
- package/dist/storage/rdf/SolidRdfEngine.js +292 -0
- package/dist/storage/rdf/SolidRdfEngine.js.map +1 -0
- package/dist/storage/rdf/SolidRdfEngine.jsonld +372 -0
- package/dist/storage/rdf/SolidRdfSparqlEngine.d.ts +92 -0
- package/dist/storage/rdf/SolidRdfSparqlEngine.js +477 -0
- package/dist/storage/rdf/SolidRdfSparqlEngine.js.map +1 -0
- package/dist/storage/rdf/SolidRdfSparqlEngine.jsonld +257 -0
- package/dist/storage/rdf/index.d.ts +15 -0
- package/dist/storage/rdf/index.js +61 -0
- package/dist/storage/rdf/index.js.map +1 -0
- package/dist/storage/rdf/models-benchmark.d.ts +260 -0
- package/dist/storage/rdf/models-benchmark.js +1405 -0
- package/dist/storage/rdf/models-benchmark.js.map +1 -0
- package/dist/storage/rdf/types.d.ts +726 -0
- package/dist/storage/rdf/types.js +3 -0
- package/dist/storage/rdf/types.js.map +1 -0
- package/dist/storage/rdf/types.jsonld +316 -0
- package/dist/storage/vector/VectorIndexingListener.d.ts +5 -5
- package/dist/storage/vector/VectorIndexingListener.js +19 -19
- package/dist/storage/vector/VectorIndexingListener.js.map +1 -1
- package/package.json +3 -2
- package/templates/pod/acp/.acr.hbs +39 -0
- package/templates/pod/acp/README.acr +18 -0
- package/templates/pod/acp/profile/card.acr +22 -0
- package/templates/pod/base/README$.md.hbs +27 -0
- package/templates/pod/base/profile/card$.ttl.hbs +13 -0
- package/templates/pod/wac/.acl.hbs +26 -0
- package/templates/pod/wac/README.acl.hbs +14 -0
- package/templates/pod/wac/profile/card.acl.hbs +19 -0
- package/dist/api/handlers/WebIdProfileHandler.d.ts +0 -16
- package/dist/api/handlers/WebIdProfileHandler.js +0 -423
- package/dist/api/handlers/WebIdProfileHandler.js.map +0 -1
- package/dist/identity/drizzle/WebIdProfileRepository.d.ts +0 -63
- package/dist/identity/drizzle/WebIdProfileRepository.js +0 -168
- package/dist/identity/drizzle/WebIdProfileRepository.js.map +0 -1
- package/dist/identity/drizzle/WebIdProfileRepository.jsonld +0 -112
- package/dist/storage/quint/BaseQuintStore.jsonld +0 -257
|
@@ -0,0 +1,1840 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RdfQuadIndex = 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 types_1 = require("../quint/types");
|
|
9
|
+
const RdfTermDictionary_1 = require("./RdfTermDictionary");
|
|
10
|
+
const RdfTermSemantics_1 = require("./RdfTermSemantics");
|
|
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 TERM_COLUMN = {
|
|
15
|
+
graph: 'graph_id',
|
|
16
|
+
subject: 'subject_id',
|
|
17
|
+
predicate: 'predicate_id',
|
|
18
|
+
object: 'object_id',
|
|
19
|
+
};
|
|
20
|
+
const TERM_KEYS = ['graph', 'subject', 'predicate', 'object'];
|
|
21
|
+
const TERM_IN_JOIN_THRESHOLD = 64;
|
|
22
|
+
class RdfQuadIndex {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.options = options;
|
|
25
|
+
this.sqliteRuntime = (0, SqliteRuntime_1.createSqliteRuntime)();
|
|
26
|
+
this.db = null;
|
|
27
|
+
this.dictionary = null;
|
|
28
|
+
this.cardinalityCache = new Map();
|
|
29
|
+
}
|
|
30
|
+
open() {
|
|
31
|
+
if (this.db) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (this.options.path !== ':memory:') {
|
|
35
|
+
const dir = (0, node_path_1.dirname)(this.options.path);
|
|
36
|
+
if (!(0, node_fs_1.existsSync)(dir)) {
|
|
37
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this.db = this.sqliteRuntime.openDatabase(this.options.path);
|
|
41
|
+
this.dictionary = new RdfTermDictionary_1.RdfTermDictionary(this.db);
|
|
42
|
+
this.dictionary.initialize();
|
|
43
|
+
this.initializeSchema();
|
|
44
|
+
}
|
|
45
|
+
close() {
|
|
46
|
+
this.db?.close();
|
|
47
|
+
this.db = null;
|
|
48
|
+
this.dictionary = null;
|
|
49
|
+
}
|
|
50
|
+
clear() {
|
|
51
|
+
const db = this.requireDb();
|
|
52
|
+
db.exec('DELETE FROM rdf_quads; DELETE FROM rdf_sources; DELETE FROM rdf_terms;');
|
|
53
|
+
this.cardinalityCache.clear();
|
|
54
|
+
}
|
|
55
|
+
put(quad, options) {
|
|
56
|
+
this.multiPut([quad], options);
|
|
57
|
+
}
|
|
58
|
+
replaceSource(quads, source) {
|
|
59
|
+
const db = this.requireDb();
|
|
60
|
+
db.transaction(() => {
|
|
61
|
+
this.deleteSource(source.source);
|
|
62
|
+
if (quads.length > 0) {
|
|
63
|
+
this.insertQuads(quads, { source });
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.upsertSource(source);
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
this.cardinalityCache.clear();
|
|
70
|
+
}
|
|
71
|
+
deleteSource(source) {
|
|
72
|
+
const db = this.requireDb();
|
|
73
|
+
const row = db
|
|
74
|
+
.prepare('SELECT id FROM rdf_sources WHERE source = ?')
|
|
75
|
+
.get(source);
|
|
76
|
+
if (!row) {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
const result = db.prepare('DELETE FROM rdf_quads WHERE source_file_id = ?').run(row.id);
|
|
80
|
+
db.prepare('DELETE FROM rdf_sources WHERE id = ?').run(row.id);
|
|
81
|
+
if (result.changes > 0) {
|
|
82
|
+
this.cardinalityCache.clear();
|
|
83
|
+
}
|
|
84
|
+
return result.changes;
|
|
85
|
+
}
|
|
86
|
+
multiPut(quads, options) {
|
|
87
|
+
if (quads.length === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const db = this.requireDb();
|
|
91
|
+
db.transaction(() => {
|
|
92
|
+
this.insertQuads(quads, options);
|
|
93
|
+
})();
|
|
94
|
+
this.cardinalityCache.clear();
|
|
95
|
+
}
|
|
96
|
+
insertQuads(quads, options) {
|
|
97
|
+
const db = this.requireDb();
|
|
98
|
+
const dictionary = this.requireDictionary();
|
|
99
|
+
const sourceId = options?.source ? this.upsertSource(options.source) : null;
|
|
100
|
+
const insert = db.prepare(`
|
|
101
|
+
INSERT INTO rdf_quads (
|
|
102
|
+
graph_id,
|
|
103
|
+
subject_id,
|
|
104
|
+
predicate_id,
|
|
105
|
+
object_id,
|
|
106
|
+
source_file_id,
|
|
107
|
+
source_line_no
|
|
108
|
+
)
|
|
109
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
110
|
+
ON CONFLICT (graph_id, subject_id, predicate_id, object_id)
|
|
111
|
+
DO UPDATE SET
|
|
112
|
+
source_file_id = excluded.source_file_id,
|
|
113
|
+
source_line_no = excluded.source_line_no
|
|
114
|
+
`);
|
|
115
|
+
for (const quad of quads) {
|
|
116
|
+
insert.run(dictionary.getOrCreate(quad.graph), dictionary.getOrCreate(quad.subject), dictionary.getOrCreate(quad.predicate), dictionary.getOrCreate(quad.object), sourceId, options?.sourceLineNo ?? null);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
delete(pattern) {
|
|
120
|
+
const db = this.requireDb();
|
|
121
|
+
const { joins, whereClause, params } = this.buildWhereClause(pattern, false);
|
|
122
|
+
if (!whereClause) {
|
|
123
|
+
const result = db.prepare('DELETE FROM rdf_quads').run();
|
|
124
|
+
this.cardinalityCache.clear();
|
|
125
|
+
return result.changes;
|
|
126
|
+
}
|
|
127
|
+
const sql = joins
|
|
128
|
+
? `DELETE FROM rdf_quads WHERE rowid IN (SELECT rdf_quads.rowid FROM rdf_quads${joins}${whereClause})`
|
|
129
|
+
: `DELETE FROM rdf_quads${whereClause}`;
|
|
130
|
+
const changes = db.prepare(sql).run(...params).changes;
|
|
131
|
+
if (changes > 0) {
|
|
132
|
+
this.cardinalityCache.clear();
|
|
133
|
+
}
|
|
134
|
+
return changes;
|
|
135
|
+
}
|
|
136
|
+
scan(pattern, options) {
|
|
137
|
+
return this.scanInternal(pattern, options);
|
|
138
|
+
}
|
|
139
|
+
scanWithTupleConstraints(pattern, tupleSource, options) {
|
|
140
|
+
return this.scanInternal(pattern, options, tupleSource);
|
|
141
|
+
}
|
|
142
|
+
joinPatterns(patterns, options) {
|
|
143
|
+
const start = Date.now();
|
|
144
|
+
if (patterns.length === 0) {
|
|
145
|
+
return {
|
|
146
|
+
bindings: [],
|
|
147
|
+
metrics: this.metrics('none', 0, 0, start, ['JoinBGP(empty)']),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const compiled = this.compileJoinPatterns(patterns, options);
|
|
151
|
+
if (compiled.unresolved) {
|
|
152
|
+
return {
|
|
153
|
+
bindings: [],
|
|
154
|
+
metrics: this.metrics('none', 0, 0, start, [...compiled.queryPlan, `unresolved ${compiled.unresolved}`]),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const rows = this.requireDb().prepare(compiled.sql).all(...compiled.params);
|
|
158
|
+
const matchedRows = compiled.countSql
|
|
159
|
+
? this.requireDb().prepare(compiled.countSql).get(...compiled.countParams)?.count ?? 0
|
|
160
|
+
: rows.length;
|
|
161
|
+
return {
|
|
162
|
+
bindings: this.joinRowsToBindings(rows, compiled.variableAliases),
|
|
163
|
+
metrics: this.metrics(compiled.indexChoice, matchedRows, rows.length, start, [...compiled.queryPlan, compiled.sql]),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
countJoinPatterns(patterns, options) {
|
|
167
|
+
return this.aggregateJoinPatternsInternal(patterns, options, 'JoinCount');
|
|
168
|
+
}
|
|
169
|
+
aggregateJoinPatterns(patterns, options) {
|
|
170
|
+
return this.aggregateJoinPatternsInternal(patterns, options, 'JoinAggregate');
|
|
171
|
+
}
|
|
172
|
+
aggregateJoinPatternsInternal(patterns, options, label) {
|
|
173
|
+
const start = Date.now();
|
|
174
|
+
if (patterns.length === 0) {
|
|
175
|
+
return {
|
|
176
|
+
bindings: [],
|
|
177
|
+
metrics: this.metrics('none', 0, 0, start, [`${label}(empty)`]),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const compiled = this.compileJoinPatterns(patterns);
|
|
181
|
+
if (compiled.unresolved) {
|
|
182
|
+
return {
|
|
183
|
+
bindings: [],
|
|
184
|
+
metrics: this.metrics('none', 0, 0, start, [...compiled.queryPlan, `unresolved ${compiled.unresolved}`]),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const aggregateAliases = new Map();
|
|
188
|
+
const aggregateTypes = new Map();
|
|
189
|
+
const numericJoins = new Map();
|
|
190
|
+
const numericJoinSql = [];
|
|
191
|
+
const projection = options.aggregates.map((aggregate, index) => {
|
|
192
|
+
const alias = `a${index}`;
|
|
193
|
+
aggregateAliases.set(aggregate.as, alias);
|
|
194
|
+
return this.buildJoinAggregateColumn(aggregate, alias, compiled.variableColumns, aggregateTypes, numericJoins, numericJoinSql, 'RDF BGP', this.joinRowKeyExpression(patterns));
|
|
195
|
+
}).join(', ');
|
|
196
|
+
const aggregateJoins = numericJoinSql.join('');
|
|
197
|
+
const sql = `SELECT ${projection} FROM ${compiled.from}${compiled.joins}${aggregateJoins}${compiled.whereClause}`;
|
|
198
|
+
const rows = this.requireDb().prepare(sql).all(...compiled.params);
|
|
199
|
+
const matchedRows = this.requireDb()
|
|
200
|
+
.prepare(`SELECT COUNT(*) AS count FROM ${compiled.from}${compiled.joins}${aggregateJoins}${compiled.whereClause}`)
|
|
201
|
+
.get(...compiled.params)?.count ?? 0;
|
|
202
|
+
return {
|
|
203
|
+
bindings: this.joinRowsToBindings(rows, compiled.variableAliases, aggregateAliases, aggregateTypes),
|
|
204
|
+
metrics: this.metrics(compiled.indexChoice, matchedRows, rows.length, start, [
|
|
205
|
+
...compiled.queryPlan,
|
|
206
|
+
...(numericJoinSql.length > 0 ? [`JoinAggregateNumeric(${[...numericJoins.keys()].map((variableName) => `?${variableName}`).join(',')})`] : []),
|
|
207
|
+
`${label}(${options.aggregates.map((aggregate) => (`${aggregate.type}${aggregate.distinct ? ':DISTINCT' : ''}(${aggregate.variable ? `?${aggregate.variable}` : '*'})`)).join(',')})`,
|
|
208
|
+
sql,
|
|
209
|
+
]),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
groupCountJoinPatterns(patterns, options) {
|
|
213
|
+
return this.groupAggregateJoinPatternsInternal(patterns, options, 'JoinGroupCount');
|
|
214
|
+
}
|
|
215
|
+
groupAggregateJoinPatterns(patterns, options) {
|
|
216
|
+
return this.groupAggregateJoinPatternsInternal(patterns, options, 'JoinGroupAggregate');
|
|
217
|
+
}
|
|
218
|
+
groupAggregateJoinPatternsInternal(patterns, options, label) {
|
|
219
|
+
const start = Date.now();
|
|
220
|
+
if (patterns.length === 0) {
|
|
221
|
+
return {
|
|
222
|
+
bindings: [],
|
|
223
|
+
metrics: this.metrics('none', 0, 0, start, [`${label}(empty)`]),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const compiled = this.compileJoinPatterns(patterns);
|
|
227
|
+
if (compiled.unresolved) {
|
|
228
|
+
return {
|
|
229
|
+
bindings: [],
|
|
230
|
+
metrics: this.metrics('none', 0, 0, start, [...compiled.queryPlan, `unresolved ${compiled.unresolved}`]),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const aggregateAliases = new Map();
|
|
234
|
+
const aggregateSqlAliases = new Map();
|
|
235
|
+
const aggregateTypes = new Map();
|
|
236
|
+
const numericJoins = new Map();
|
|
237
|
+
const numericJoinSql = [];
|
|
238
|
+
const groupColumns = options.groupBy.map((variableName) => {
|
|
239
|
+
const column = compiled.variableColumns.get(variableName);
|
|
240
|
+
if (!column) {
|
|
241
|
+
throw new Error(`RDF BGP group aggregate cannot group by unbound variable: ${variableName}`);
|
|
242
|
+
}
|
|
243
|
+
return column;
|
|
244
|
+
});
|
|
245
|
+
const aggregateColumns = options.aggregates.map((aggregate, index) => {
|
|
246
|
+
const alias = `a${index}`;
|
|
247
|
+
aggregateAliases.set(aggregate.as, alias);
|
|
248
|
+
aggregateSqlAliases.set(aggregate.as, alias);
|
|
249
|
+
return this.buildJoinAggregateColumn(aggregate, alias, compiled.variableColumns, aggregateTypes, numericJoins, numericJoinSql, 'RDF BGP group aggregate', '__row_key');
|
|
250
|
+
});
|
|
251
|
+
const projection = [
|
|
252
|
+
...options.groupBy.map((variableName) => {
|
|
253
|
+
const alias = compiled.variableAliases.get(variableName);
|
|
254
|
+
const column = compiled.variableColumns.get(variableName);
|
|
255
|
+
if (!alias || !column) {
|
|
256
|
+
throw new Error(`RDF BGP group aggregate cannot project unbound group variable: ${variableName}`);
|
|
257
|
+
}
|
|
258
|
+
return `${column} AS ${alias}`;
|
|
259
|
+
}),
|
|
260
|
+
...aggregateColumns,
|
|
261
|
+
].join(', ');
|
|
262
|
+
const groupBy = groupColumns.join(', ');
|
|
263
|
+
const rowKeyExpression = this.joinRowKeyExpression(patterns);
|
|
264
|
+
const aggregateJoins = numericJoinSql.join('');
|
|
265
|
+
const havingClause = this.buildGroupAggregateHavingClause(options.having, aggregateSqlAliases);
|
|
266
|
+
const orderScope = this.buildGroupAggregateOrderScope(options, compiled.variableColumns, aggregateSqlAliases);
|
|
267
|
+
const fromSql = `${compiled.from}${compiled.joins}${aggregateJoins}${compiled.whereClause}`;
|
|
268
|
+
const sourceFromSql = `${compiled.from}${compiled.joins}${aggregateJoins}${orderScope.joins}${compiled.whereClause}`;
|
|
269
|
+
const sourceSql = aggregateColumns.some((entry) => entry.includes('__row_key'))
|
|
270
|
+
? `SELECT ${projection.replace(/__row_key/g, rowKeyExpression)} FROM ${sourceFromSql} GROUP BY ${groupBy}${havingClause.sql}`
|
|
271
|
+
: `SELECT ${projection} FROM ${sourceFromSql} GROUP BY ${groupBy}${havingClause.sql}`;
|
|
272
|
+
const orderClause = orderScope.orderBy;
|
|
273
|
+
let sql = `${sourceSql}${orderClause}`;
|
|
274
|
+
const params = [...compiled.params, ...havingClause.params];
|
|
275
|
+
const paginated = options.limit !== undefined || options.offset !== undefined;
|
|
276
|
+
if (options.limit !== undefined) {
|
|
277
|
+
sql += ' LIMIT ?';
|
|
278
|
+
params.push(options.limit);
|
|
279
|
+
}
|
|
280
|
+
if (options.offset !== undefined) {
|
|
281
|
+
if (options.limit === undefined) {
|
|
282
|
+
sql += ' LIMIT -1';
|
|
283
|
+
}
|
|
284
|
+
sql += ' OFFSET ?';
|
|
285
|
+
params.push(options.offset);
|
|
286
|
+
}
|
|
287
|
+
const rows = this.requireDb().prepare(sql).all(...params);
|
|
288
|
+
const matchedRows = this.requireDb()
|
|
289
|
+
.prepare(`SELECT COUNT(*) AS count FROM ${fromSql}`)
|
|
290
|
+
.get(...compiled.params)?.count ?? 0;
|
|
291
|
+
return {
|
|
292
|
+
bindings: this.joinRowsToBindings(rows, compiled.variableAliases, aggregateAliases, aggregateTypes),
|
|
293
|
+
metrics: this.metrics(compiled.indexChoice, matchedRows, rows.length, start, [
|
|
294
|
+
...compiled.queryPlan,
|
|
295
|
+
...(numericJoinSql.length > 0 ? [`JoinGroupAggregateNumeric(${[...numericJoins.keys()].map((variableName) => `?${variableName}`).join(',')})`] : []),
|
|
296
|
+
`${label}(${options.groupBy.map((variableName) => `?${variableName}`).join(',')})`,
|
|
297
|
+
...(havingClause.sql ? [`${label}Having(${(options.having ?? []).map((entry) => `${entry.aggregate}${entry.operator}`).join(',')})`] : []),
|
|
298
|
+
...(orderClause ? [`${label}Order(${(options.orderBy ?? []).map((entry) => `${entry.direction ?? 'asc'}:${entry.variable}`).join(',')})`] : []),
|
|
299
|
+
...(paginated ? [`${label}Limit`] : []),
|
|
300
|
+
sql,
|
|
301
|
+
]),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
scanInternal(pattern, options, tupleSource) {
|
|
305
|
+
const start = Date.now();
|
|
306
|
+
const { joins, whereClause, params, indexHint, queryPlan, unresolved } = this.buildWhereClause(pattern, true);
|
|
307
|
+
if (unresolved) {
|
|
308
|
+
return {
|
|
309
|
+
quads: [],
|
|
310
|
+
metrics: this.metrics(indexHint, 0, 0, start, [...queryPlan, `unresolved ${unresolved}`]),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const orderClause = this.buildOrderClause(options);
|
|
314
|
+
const tupleJoin = tupleSource ? this.buildTupleConstraintJoin(tupleSource) : undefined;
|
|
315
|
+
let sql = `SELECT rdf_quads.graph_id, rdf_quads.subject_id, rdf_quads.predicate_id, rdf_quads.object_id, rdf_quads.source_file_id, rdf_quads.source_line_no FROM rdf_quads${joins}${tupleJoin?.join ?? ''}${orderClause.joins}`;
|
|
316
|
+
if (whereClause) {
|
|
317
|
+
sql += whereClause;
|
|
318
|
+
}
|
|
319
|
+
if (orderClause.orderBy) {
|
|
320
|
+
sql += orderClause.orderBy;
|
|
321
|
+
}
|
|
322
|
+
if (options?.limit !== undefined) {
|
|
323
|
+
sql += ' LIMIT ?';
|
|
324
|
+
params.push(options.limit);
|
|
325
|
+
}
|
|
326
|
+
if (options?.offset !== undefined) {
|
|
327
|
+
if (options.limit === undefined) {
|
|
328
|
+
sql += ' LIMIT -1';
|
|
329
|
+
}
|
|
330
|
+
sql += ' OFFSET ?';
|
|
331
|
+
params.push(options.offset);
|
|
332
|
+
}
|
|
333
|
+
const countSql = `SELECT COUNT(*) AS count FROM rdf_quads${joins}${tupleJoin?.join ?? ''}${orderClause.joins}${whereClause}`;
|
|
334
|
+
const countParams = [...params.slice(0, params.length - this.paginationParamCount(options))];
|
|
335
|
+
const countRow = this.requireDb().prepare(countSql).get(...countParams);
|
|
336
|
+
const matchedRows = countRow?.count ?? 0;
|
|
337
|
+
const rows = this.requireDb().prepare(sql).all(...params);
|
|
338
|
+
return {
|
|
339
|
+
quads: this.rowsToQuads(rows),
|
|
340
|
+
metrics: this.metrics(indexHint, matchedRows, rows.length, start, [
|
|
341
|
+
...queryPlan,
|
|
342
|
+
...(tupleJoin ? [`TupleValuesJoin(${tupleSource?.columns.join(',')})`] : []),
|
|
343
|
+
sql,
|
|
344
|
+
]),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
compileJoinPatterns(patterns, options) {
|
|
348
|
+
const from = 'rdf_quads q0';
|
|
349
|
+
const joins = [];
|
|
350
|
+
const conditions = [];
|
|
351
|
+
const params = [];
|
|
352
|
+
const queryPlan = [`JoinBGP(${patterns.length})`];
|
|
353
|
+
const variableColumns = new Map();
|
|
354
|
+
const variableAliases = new Map();
|
|
355
|
+
const indexChoices = [];
|
|
356
|
+
for (const [patternIndex, entry] of patterns.entries()) {
|
|
357
|
+
const alias = `q${patternIndex}`;
|
|
358
|
+
if (patternIndex > 0) {
|
|
359
|
+
joins.push(` JOIN rdf_quads ${alias} ON 1 = 1`);
|
|
360
|
+
}
|
|
361
|
+
const equalityColumns = new Set();
|
|
362
|
+
for (const key of TERM_KEYS) {
|
|
363
|
+
const match = entry.pattern[key];
|
|
364
|
+
const column = `${alias}.${TERM_COLUMN[key]}`;
|
|
365
|
+
const variableName = entry.variables[key];
|
|
366
|
+
if (variableName) {
|
|
367
|
+
const existingColumn = variableColumns.get(variableName);
|
|
368
|
+
if (existingColumn) {
|
|
369
|
+
conditions.push(`${existingColumn} = ${column}`);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
variableColumns.set(variableName, column);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (!match) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const condition = this.matchToJoinCondition(key, alias, TERM_COLUMN[key], match, true);
|
|
379
|
+
if (condition.unresolved) {
|
|
380
|
+
return {
|
|
381
|
+
from,
|
|
382
|
+
joins: joins.join(''),
|
|
383
|
+
whereClause: '',
|
|
384
|
+
sql: '',
|
|
385
|
+
params: [],
|
|
386
|
+
countParams: [],
|
|
387
|
+
indexChoice: 'none',
|
|
388
|
+
queryPlan,
|
|
389
|
+
variableColumns,
|
|
390
|
+
variableAliases,
|
|
391
|
+
unresolved: key,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
joins.push(...condition.joins);
|
|
395
|
+
if (condition.sql) {
|
|
396
|
+
conditions.push(condition.sql);
|
|
397
|
+
params.push(...condition.params);
|
|
398
|
+
}
|
|
399
|
+
queryPlan.push(...condition.queryPlan);
|
|
400
|
+
if (condition.equality) {
|
|
401
|
+
equalityColumns.add(TERM_COLUMN[key]);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
indexChoices.push(this.chooseIndex(equalityColumns));
|
|
405
|
+
}
|
|
406
|
+
const projectVariables = options?.project ?? [...variableColumns.keys()];
|
|
407
|
+
const projectionColumns = projectVariables.map((variableName) => {
|
|
408
|
+
const column = variableColumns.get(variableName);
|
|
409
|
+
if (!column) {
|
|
410
|
+
throw new Error(`RDF BGP join cannot project unbound variable: ${variableName}`);
|
|
411
|
+
}
|
|
412
|
+
const columnAlias = `v${variableAliases.size}`;
|
|
413
|
+
variableAliases.set(variableName, columnAlias);
|
|
414
|
+
return `${column} AS ${columnAlias}`;
|
|
415
|
+
});
|
|
416
|
+
const projection = projectionColumns.length > 0
|
|
417
|
+
? `${options?.distinct ? 'DISTINCT ' : ''}${projectionColumns.join(', ')}`
|
|
418
|
+
: `${options?.distinct ? 'DISTINCT ' : ''}1 AS __empty`;
|
|
419
|
+
const orderClause = this.buildJoinOrderClause(options, variableColumns);
|
|
420
|
+
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
421
|
+
let sql = `SELECT ${projection} FROM ${from}${joins.join('')}${orderClause.joins}${whereClause}${orderClause.orderBy}`;
|
|
422
|
+
const sqlParams = [...params];
|
|
423
|
+
const paginated = options?.limit !== undefined || options?.offset !== undefined;
|
|
424
|
+
const countMatchedRows = options?.countMatchedRows ?? true;
|
|
425
|
+
if (options?.limit !== undefined) {
|
|
426
|
+
sql += ' LIMIT ?';
|
|
427
|
+
sqlParams.push(options.limit);
|
|
428
|
+
}
|
|
429
|
+
if (options?.offset !== undefined) {
|
|
430
|
+
if (options.limit === undefined) {
|
|
431
|
+
sql += ' LIMIT -1';
|
|
432
|
+
}
|
|
433
|
+
sql += ' OFFSET ?';
|
|
434
|
+
sqlParams.push(options.offset);
|
|
435
|
+
}
|
|
436
|
+
if (orderClause.orderBy) {
|
|
437
|
+
queryPlan.push(`JoinOrder(${(options?.orderBy ?? []).map((entry) => `${entry.direction ?? 'asc'}:${entry.variable}`).join(',')})`);
|
|
438
|
+
}
|
|
439
|
+
if (options?.distinct) {
|
|
440
|
+
queryPlan.push(`JoinDistinct(${projectVariables.map((variableName) => `?${variableName}`).join(',')})`);
|
|
441
|
+
}
|
|
442
|
+
if (paginated) {
|
|
443
|
+
queryPlan.push('JoinLimit');
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
from,
|
|
447
|
+
joins: joins.join(''),
|
|
448
|
+
whereClause,
|
|
449
|
+
sql,
|
|
450
|
+
params: sqlParams,
|
|
451
|
+
countSql: paginated && countMatchedRows ? `SELECT COUNT(*) AS count FROM ${from}${joins.join('')}${whereClause}` : undefined,
|
|
452
|
+
countParams: params,
|
|
453
|
+
indexChoice: `JoinBGP(${indexChoices.join('>')})`,
|
|
454
|
+
queryPlan,
|
|
455
|
+
variableColumns,
|
|
456
|
+
variableAliases,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
buildJoinOrderClause(options, variableColumns) {
|
|
460
|
+
if (!options?.orderBy || options.orderBy.length === 0) {
|
|
461
|
+
return { joins: '', orderBy: '' };
|
|
462
|
+
}
|
|
463
|
+
const joins = options.orderBy.map((entry, index) => {
|
|
464
|
+
const column = variableColumns.get(entry.variable);
|
|
465
|
+
if (!column) {
|
|
466
|
+
throw new Error(`RDF BGP join cannot order by unbound variable: ${entry.variable}`);
|
|
467
|
+
}
|
|
468
|
+
const alias = `join_order_t${index}`;
|
|
469
|
+
return {
|
|
470
|
+
join: ` JOIN rdf_terms ${alias} ON ${alias}.id = ${column}`,
|
|
471
|
+
order: `${alias}.value${entry.direction === 'desc' ? ' DESC' : ''}`,
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
joins: joins.map((entry) => entry.join).join(''),
|
|
476
|
+
orderBy: ` ORDER BY ${joins.map((entry) => entry.order).join(', ')}`,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
joinRowKeyExpression(patterns) {
|
|
480
|
+
return patterns.map((_, index) => `q${index}.rowid`).join(` || ':' || `);
|
|
481
|
+
}
|
|
482
|
+
buildJoinAggregateColumn(aggregate, alias, variableColumns, aggregateTypes, numericJoins, numericJoinSql, errorPrefix, rowKeyExpression) {
|
|
483
|
+
if (aggregate.type === 'count' && !aggregate.variable) {
|
|
484
|
+
aggregateTypes.set(aggregate.as, 'integer');
|
|
485
|
+
return `${aggregate.distinct ? `COUNT(DISTINCT ${rowKeyExpression})` : 'COUNT(*)'} AS ${alias}`;
|
|
486
|
+
}
|
|
487
|
+
if (!aggregate.variable) {
|
|
488
|
+
throw new Error(`${errorPrefix} ${aggregate.type} aggregate requires a bound variable`);
|
|
489
|
+
}
|
|
490
|
+
const column = variableColumns.get(aggregate.variable);
|
|
491
|
+
if (!column) {
|
|
492
|
+
throw new Error(`${errorPrefix} aggregate cannot read unbound variable: ${aggregate.variable}`);
|
|
493
|
+
}
|
|
494
|
+
if (aggregate.type === 'count') {
|
|
495
|
+
aggregateTypes.set(aggregate.as, 'integer');
|
|
496
|
+
return `COUNT(${aggregate.distinct ? 'DISTINCT ' : ''}${column}) AS ${alias}`;
|
|
497
|
+
}
|
|
498
|
+
if (aggregate.distinct) {
|
|
499
|
+
throw new Error(`${errorPrefix} ${aggregate.type} DISTINCT aggregate is not supported in SQL aggregate path`);
|
|
500
|
+
}
|
|
501
|
+
aggregateTypes.set(aggregate.as, 'decimal');
|
|
502
|
+
const termAlias = numericJoins.get(aggregate.variable) ?? `agg_numeric_t${numericJoins.size}`;
|
|
503
|
+
if (!numericJoins.has(aggregate.variable)) {
|
|
504
|
+
numericJoins.set(aggregate.variable, termAlias);
|
|
505
|
+
numericJoinSql.push(` JOIN rdf_terms ${termAlias} ON ${termAlias}.id = ${column} AND ${termAlias}.kind = 'literal' AND ${termAlias}.numeric_value IS NOT NULL`);
|
|
506
|
+
}
|
|
507
|
+
switch (aggregate.type) {
|
|
508
|
+
case 'sum':
|
|
509
|
+
return `COALESCE(SUM(${termAlias}.numeric_value), 0) AS ${alias}`;
|
|
510
|
+
case 'avg':
|
|
511
|
+
return `AVG(${termAlias}.numeric_value) AS ${alias}`;
|
|
512
|
+
case 'min':
|
|
513
|
+
return `MIN(${termAlias}.numeric_value) AS ${alias}`;
|
|
514
|
+
case 'max':
|
|
515
|
+
return `MAX(${termAlias}.numeric_value) AS ${alias}`;
|
|
516
|
+
default: {
|
|
517
|
+
const exhaustive = aggregate.type;
|
|
518
|
+
throw new Error(`Unsupported RDF BGP aggregate type: ${exhaustive}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
buildGroupCountHavingClause(having, aggregateAliases) {
|
|
523
|
+
return this.buildGroupAggregateHavingClause(having, aggregateAliases);
|
|
524
|
+
}
|
|
525
|
+
buildGroupAggregateHavingClause(having, aggregateAliases) {
|
|
526
|
+
if (!having || having.length === 0) {
|
|
527
|
+
return { sql: '', params: [] };
|
|
528
|
+
}
|
|
529
|
+
const conditions = [];
|
|
530
|
+
const params = [];
|
|
531
|
+
for (const entry of having) {
|
|
532
|
+
const alias = aggregateAliases.get(entry.aggregate);
|
|
533
|
+
if (!alias) {
|
|
534
|
+
throw new Error(`RDF BGP group count cannot HAVING on unknown aggregate: ${entry.aggregate}`);
|
|
535
|
+
}
|
|
536
|
+
conditions.push(`${alias} ${this.havingSqlOperator(entry.operator)} ?`);
|
|
537
|
+
params.push(entry.value);
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
sql: ` HAVING ${conditions.join(' AND ')}`,
|
|
541
|
+
params,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
havingSqlOperator(operator) {
|
|
545
|
+
switch (operator) {
|
|
546
|
+
case '$eq':
|
|
547
|
+
return '=';
|
|
548
|
+
case '$ne':
|
|
549
|
+
return '!=';
|
|
550
|
+
case '$gt':
|
|
551
|
+
return '>';
|
|
552
|
+
case '$gte':
|
|
553
|
+
return '>=';
|
|
554
|
+
case '$lt':
|
|
555
|
+
return '<';
|
|
556
|
+
case '$lte':
|
|
557
|
+
return '<=';
|
|
558
|
+
default: {
|
|
559
|
+
const exhaustive = operator;
|
|
560
|
+
throw new Error(`Unsupported RDF BGP group count HAVING operator: ${exhaustive}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
buildGroupCountOrderScope(options, variableColumns, aggregateAliases) {
|
|
565
|
+
return this.buildGroupAggregateOrderScope(options, variableColumns, aggregateAliases);
|
|
566
|
+
}
|
|
567
|
+
buildGroupAggregateOrderScope(options, variableColumns, aggregateAliases) {
|
|
568
|
+
if (!options.orderBy || options.orderBy.length === 0) {
|
|
569
|
+
return { joins: '', orderBy: '' };
|
|
570
|
+
}
|
|
571
|
+
const joins = [];
|
|
572
|
+
const orders = options.orderBy.map((entry, index) => {
|
|
573
|
+
const aggregateAlias = aggregateAliases.get(entry.variable);
|
|
574
|
+
if (aggregateAlias) {
|
|
575
|
+
return `${aggregateAlias}${entry.direction === 'desc' ? ' DESC' : ''}`;
|
|
576
|
+
}
|
|
577
|
+
const column = variableColumns.get(entry.variable);
|
|
578
|
+
if (!column) {
|
|
579
|
+
throw new Error(`RDF BGP group count cannot order by unbound variable: ${entry.variable}`);
|
|
580
|
+
}
|
|
581
|
+
const alias = `group_order_t${index}`;
|
|
582
|
+
joins.push(` JOIN rdf_terms ${alias} ON ${alias}.id = ${column}`);
|
|
583
|
+
return `${alias}.value${entry.direction === 'desc' ? ' DESC' : ''}`;
|
|
584
|
+
});
|
|
585
|
+
return {
|
|
586
|
+
joins: joins.join(''),
|
|
587
|
+
orderBy: ` ORDER BY ${orders.join(', ')}`,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
matchToJoinCondition(key, alias, columnName, match, allowUnresolved) {
|
|
591
|
+
return this.matchToCondition(key, columnName, match, allowUnresolved, {
|
|
592
|
+
quadAlias: alias,
|
|
593
|
+
aliasPrefix: alias,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
joinRowsToBindings(rows, variableAliases, aggregateAliases, aggregateTypes) {
|
|
597
|
+
const aliases = [...variableAliases.entries()];
|
|
598
|
+
const termMap = this.requireDictionary().rowsForIds(rows.flatMap((row) => (aliases
|
|
599
|
+
.map(([, alias]) => row[alias])
|
|
600
|
+
.filter((id) => typeof id === 'number'))));
|
|
601
|
+
return rows.map((row) => {
|
|
602
|
+
const binding = {};
|
|
603
|
+
for (const [variableName, alias] of aliases) {
|
|
604
|
+
const id = row[alias];
|
|
605
|
+
if (typeof id !== 'number') {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
binding[variableName] = this.requiredTerm(termMap, id);
|
|
609
|
+
}
|
|
610
|
+
for (const [variableName, alias] of aggregateAliases ?? []) {
|
|
611
|
+
const value = row[alias];
|
|
612
|
+
if (typeof value === 'number') {
|
|
613
|
+
const datatype = aggregateTypes?.get(variableName) === 'decimal' ? XSD_DECIMAL : XSD_INTEGER;
|
|
614
|
+
binding[variableName] = n3_1.DataFactory.literal(String(value), n3_1.DataFactory.namedNode(datatype));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return binding;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
count(pattern) {
|
|
621
|
+
const { joins, whereClause, params, unresolved } = this.buildWhereClause(pattern, true);
|
|
622
|
+
if (unresolved) {
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
625
|
+
const row = this.requireDb()
|
|
626
|
+
.prepare(`SELECT COUNT(*) AS count FROM rdf_quads${joins}${whereClause}`)
|
|
627
|
+
.get(...params);
|
|
628
|
+
return row?.count ?? 0;
|
|
629
|
+
}
|
|
630
|
+
estimateCardinality(pattern) {
|
|
631
|
+
const exact = this.exactTermPattern(pattern);
|
|
632
|
+
if (!exact) {
|
|
633
|
+
return {
|
|
634
|
+
rows: this.count(pattern),
|
|
635
|
+
source: 'exact-count',
|
|
636
|
+
indexChoice: this.buildWhereClause(pattern, true).indexHint,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
const cacheKey = exact.cacheKey;
|
|
640
|
+
const cached = this.cardinalityCache.get(cacheKey);
|
|
641
|
+
if (cached) {
|
|
642
|
+
return {
|
|
643
|
+
...cached,
|
|
644
|
+
source: 'cached-exact-count',
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const estimate = this.countExactTermPattern(exact.ids, exact.indexChoice);
|
|
648
|
+
this.cardinalityCache.set(cacheKey, estimate);
|
|
649
|
+
return estimate;
|
|
650
|
+
}
|
|
651
|
+
countDistinct(pattern, distinctKey) {
|
|
652
|
+
const exact = this.exactTermPattern(pattern);
|
|
653
|
+
if (!exact) {
|
|
654
|
+
const count = this.countDistinctPattern(pattern, distinctKey);
|
|
655
|
+
return {
|
|
656
|
+
rows: count,
|
|
657
|
+
source: 'exact-distinct-count',
|
|
658
|
+
indexChoice: this.buildWhereClause(pattern, true).indexHint,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const cacheKey = `distinct:${distinctKey}|${exact.cacheKey}`;
|
|
662
|
+
const cached = this.cardinalityCache.get(cacheKey);
|
|
663
|
+
if (cached) {
|
|
664
|
+
return {
|
|
665
|
+
...cached,
|
|
666
|
+
source: 'cached-exact-distinct-count',
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
const estimate = this.countExactDistinctTermPattern(exact.ids, exact.indexChoice, distinctKey);
|
|
670
|
+
this.cardinalityCache.set(cacheKey, estimate);
|
|
671
|
+
return estimate;
|
|
672
|
+
}
|
|
673
|
+
countDistinctTuple(pattern, distinctKeys) {
|
|
674
|
+
const keys = uniquePatternKeys(distinctKeys);
|
|
675
|
+
if (keys.length === 0) {
|
|
676
|
+
return this.estimateCardinality(pattern);
|
|
677
|
+
}
|
|
678
|
+
if (keys.length === 1) {
|
|
679
|
+
return this.countDistinct(pattern, keys[0]);
|
|
680
|
+
}
|
|
681
|
+
const exact = this.exactTermPattern(pattern);
|
|
682
|
+
if (!exact) {
|
|
683
|
+
const count = this.countDistinctTuplePattern(pattern, keys);
|
|
684
|
+
return {
|
|
685
|
+
rows: count,
|
|
686
|
+
source: 'exact-distinct-tuple-count',
|
|
687
|
+
indexChoice: this.buildWhereClause(pattern, true).indexHint,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const cacheKey = `distinct-tuple:${keys.join(',')}|${exact.cacheKey}`;
|
|
691
|
+
const cached = this.cardinalityCache.get(cacheKey);
|
|
692
|
+
if (cached) {
|
|
693
|
+
return {
|
|
694
|
+
...cached,
|
|
695
|
+
source: 'cached-exact-distinct-tuple-count',
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
const estimate = this.countExactDistinctTupleTermPattern(exact.ids, exact.indexChoice, keys);
|
|
699
|
+
this.cardinalityCache.set(cacheKey, estimate);
|
|
700
|
+
return estimate;
|
|
701
|
+
}
|
|
702
|
+
stats() {
|
|
703
|
+
const db = this.requireDb();
|
|
704
|
+
const spaceObjects = this.collectSpaceObjects();
|
|
705
|
+
const databaseBytes = this.estimateDatabaseBytes();
|
|
706
|
+
const accountedBytes = spaceObjects.reduce((sum, object) => sum + object.bytes, 0);
|
|
707
|
+
return {
|
|
708
|
+
termCount: this.requireDictionary().count(),
|
|
709
|
+
quadCount: db.prepare('SELECT COUNT(*) AS count FROM rdf_quads').get()?.count ?? 0,
|
|
710
|
+
sourceCount: db.prepare('SELECT COUNT(*) AS count FROM rdf_sources').get()?.count ?? 0,
|
|
711
|
+
graphCount: db.prepare('SELECT COUNT(DISTINCT graph_id) AS count FROM rdf_quads').get()?.count ?? 0,
|
|
712
|
+
databaseBytes: accountedBytes || databaseBytes,
|
|
713
|
+
tableBytes: sumSpaceObjects(spaceObjects, 'table'),
|
|
714
|
+
indexBytes: sumSpaceObjects(spaceObjects, 'index'),
|
|
715
|
+
spaceObjects,
|
|
716
|
+
serializedTermTextBytes: this.estimateSerializedTextBytes(),
|
|
717
|
+
literalDatatypeDistribution: this.literalDatatypeDistribution(),
|
|
718
|
+
cardinalityDistributions: this.cardinalityDistributions(),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
cardinalityDistributions(limit = 100) {
|
|
722
|
+
return {
|
|
723
|
+
graphs: this.graphCardinalityDistribution(limit),
|
|
724
|
+
predicates: this.predicateCardinalityDistribution(limit),
|
|
725
|
+
predicateObjects: this.predicateObjectCardinalityDistribution(limit),
|
|
726
|
+
subjectPredicates: this.subjectPredicateCardinalityDistribution(limit),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
literalDatatypeDistribution() {
|
|
730
|
+
const rows = this.requireDb().prepare(`
|
|
731
|
+
SELECT
|
|
732
|
+
COALESCE(datatype.value, 'http://www.w3.org/2001/XMLSchema#string') AS datatype,
|
|
733
|
+
COUNT(DISTINCT literal.id) AS term_count,
|
|
734
|
+
COUNT(quad.object_id) AS object_quad_count
|
|
735
|
+
FROM rdf_terms literal
|
|
736
|
+
LEFT JOIN rdf_terms datatype ON datatype.id = literal.datatype_id
|
|
737
|
+
LEFT JOIN rdf_quads quad ON quad.object_id = literal.id
|
|
738
|
+
WHERE literal.kind = 'literal'
|
|
739
|
+
GROUP BY datatype
|
|
740
|
+
ORDER BY object_quad_count DESC, term_count DESC, datatype ASC
|
|
741
|
+
`).all();
|
|
742
|
+
return rows.map((row) => ({
|
|
743
|
+
datatype: row.datatype ?? 'http://www.w3.org/2001/XMLSchema#string',
|
|
744
|
+
termCount: row.term_count,
|
|
745
|
+
objectQuadCount: row.object_quad_count ?? 0,
|
|
746
|
+
}));
|
|
747
|
+
}
|
|
748
|
+
graphCardinalityDistribution(limit) {
|
|
749
|
+
const rows = this.requireDb().prepare(`
|
|
750
|
+
SELECT
|
|
751
|
+
graph_id,
|
|
752
|
+
COUNT(*) AS quad_count,
|
|
753
|
+
COUNT(DISTINCT subject_id) AS distinct_subjects,
|
|
754
|
+
COUNT(DISTINCT predicate_id) AS distinct_predicates,
|
|
755
|
+
COUNT(DISTINCT object_id) AS distinct_objects
|
|
756
|
+
FROM rdf_quads
|
|
757
|
+
GROUP BY graph_id
|
|
758
|
+
ORDER BY quad_count DESC, graph_id ASC
|
|
759
|
+
LIMIT ?
|
|
760
|
+
`).all(limit);
|
|
761
|
+
const termMap = this.requireDictionary().rowsForIds(rows.map((row) => row.graph_id));
|
|
762
|
+
return rows.map((row) => ({
|
|
763
|
+
graph: this.cardinalityTerm(termMap, row.graph_id),
|
|
764
|
+
quadCount: row.quad_count,
|
|
765
|
+
distinctSubjects: row.distinct_subjects,
|
|
766
|
+
distinctPredicates: row.distinct_predicates,
|
|
767
|
+
distinctObjects: row.distinct_objects,
|
|
768
|
+
}));
|
|
769
|
+
}
|
|
770
|
+
predicateCardinalityDistribution(limit) {
|
|
771
|
+
const rows = this.requireDb().prepare(`
|
|
772
|
+
SELECT
|
|
773
|
+
predicate_id,
|
|
774
|
+
COUNT(*) AS quad_count,
|
|
775
|
+
COUNT(DISTINCT graph_id) AS graph_count,
|
|
776
|
+
COUNT(DISTINCT subject_id) AS distinct_subjects,
|
|
777
|
+
COUNT(DISTINCT object_id) AS distinct_objects
|
|
778
|
+
FROM rdf_quads
|
|
779
|
+
GROUP BY predicate_id
|
|
780
|
+
ORDER BY quad_count DESC, predicate_id ASC
|
|
781
|
+
LIMIT ?
|
|
782
|
+
`).all(limit);
|
|
783
|
+
const termMap = this.requireDictionary().rowsForIds(rows.map((row) => row.predicate_id));
|
|
784
|
+
return rows.map((row) => ({
|
|
785
|
+
predicate: this.cardinalityTerm(termMap, row.predicate_id),
|
|
786
|
+
quadCount: row.quad_count,
|
|
787
|
+
graphCount: row.graph_count,
|
|
788
|
+
distinctSubjects: row.distinct_subjects,
|
|
789
|
+
distinctObjects: row.distinct_objects,
|
|
790
|
+
}));
|
|
791
|
+
}
|
|
792
|
+
predicateObjectCardinalityDistribution(limit) {
|
|
793
|
+
const rows = this.requireDb().prepare(`
|
|
794
|
+
SELECT
|
|
795
|
+
predicate_id,
|
|
796
|
+
object_id,
|
|
797
|
+
COUNT(*) AS quad_count,
|
|
798
|
+
COUNT(DISTINCT graph_id) AS graph_count,
|
|
799
|
+
COUNT(DISTINCT subject_id) AS distinct_subjects
|
|
800
|
+
FROM rdf_quads
|
|
801
|
+
GROUP BY predicate_id, object_id
|
|
802
|
+
ORDER BY quad_count DESC, predicate_id ASC, object_id ASC
|
|
803
|
+
LIMIT ?
|
|
804
|
+
`).all(limit);
|
|
805
|
+
const termMap = this.requireDictionary().rowsForIds(rows.flatMap((row) => [row.predicate_id, row.object_id]));
|
|
806
|
+
return rows.map((row) => ({
|
|
807
|
+
predicate: this.cardinalityTerm(termMap, row.predicate_id),
|
|
808
|
+
object: this.cardinalityTerm(termMap, row.object_id),
|
|
809
|
+
quadCount: row.quad_count,
|
|
810
|
+
graphCount: row.graph_count,
|
|
811
|
+
distinctSubjects: row.distinct_subjects,
|
|
812
|
+
}));
|
|
813
|
+
}
|
|
814
|
+
subjectPredicateCardinalityDistribution(limit) {
|
|
815
|
+
const rows = this.requireDb().prepare(`
|
|
816
|
+
SELECT
|
|
817
|
+
subject_id,
|
|
818
|
+
predicate_id,
|
|
819
|
+
COUNT(*) AS quad_count,
|
|
820
|
+
COUNT(DISTINCT graph_id) AS graph_count,
|
|
821
|
+
COUNT(DISTINCT object_id) AS distinct_objects
|
|
822
|
+
FROM rdf_quads
|
|
823
|
+
GROUP BY subject_id, predicate_id
|
|
824
|
+
ORDER BY quad_count DESC, subject_id ASC, predicate_id ASC
|
|
825
|
+
LIMIT ?
|
|
826
|
+
`).all(limit);
|
|
827
|
+
const termMap = this.requireDictionary().rowsForIds(rows.flatMap((row) => [row.subject_id, row.predicate_id]));
|
|
828
|
+
return rows.map((row) => ({
|
|
829
|
+
subject: this.cardinalityTerm(termMap, row.subject_id),
|
|
830
|
+
predicate: this.cardinalityTerm(termMap, row.predicate_id),
|
|
831
|
+
quadCount: row.quad_count,
|
|
832
|
+
graphCount: row.graph_count,
|
|
833
|
+
distinctObjects: row.distinct_objects,
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
836
|
+
cardinalityTerm(termMap, id) {
|
|
837
|
+
const term = this.requiredTerm(termMap, id);
|
|
838
|
+
const result = {
|
|
839
|
+
value: term.value,
|
|
840
|
+
kind: rdfTermKind(term),
|
|
841
|
+
};
|
|
842
|
+
if (term.termType === 'Literal') {
|
|
843
|
+
if (term.datatype.value) {
|
|
844
|
+
result.datatype = term.datatype.value;
|
|
845
|
+
}
|
|
846
|
+
if (term.language) {
|
|
847
|
+
result.language = term.language;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return result;
|
|
851
|
+
}
|
|
852
|
+
estimateSerializedTextBytes() {
|
|
853
|
+
const db = this.requireDb();
|
|
854
|
+
const row = db.prepare(`
|
|
855
|
+
SELECT
|
|
856
|
+
COALESCE(SUM(length(value)), 0) +
|
|
857
|
+
COALESCE(SUM(CASE WHEN lang IS NULL THEN 0 ELSE length(lang) END), 0) AS bytes
|
|
858
|
+
FROM rdf_terms
|
|
859
|
+
`).get();
|
|
860
|
+
return row?.bytes ?? 0;
|
|
861
|
+
}
|
|
862
|
+
estimateDatabaseBytes() {
|
|
863
|
+
const db = this.requireDb();
|
|
864
|
+
try {
|
|
865
|
+
const pageCount = db.prepare('PRAGMA page_count').get()?.page_count ?? 0;
|
|
866
|
+
const pageSize = db.prepare('PRAGMA page_size').get()?.page_size ?? 0;
|
|
867
|
+
return pageCount * pageSize;
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
return 0;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
collectSpaceObjects() {
|
|
874
|
+
const db = this.requireDb();
|
|
875
|
+
try {
|
|
876
|
+
const schemaRows = db.prepare(`
|
|
877
|
+
SELECT name, type, tbl_name
|
|
878
|
+
FROM sqlite_schema
|
|
879
|
+
WHERE type IN ('table', 'index')
|
|
880
|
+
AND (
|
|
881
|
+
name IN ('rdf_terms', 'rdf_sources', 'rdf_quads')
|
|
882
|
+
OR tbl_name IN ('rdf_terms', 'rdf_sources', 'rdf_quads')
|
|
883
|
+
)
|
|
884
|
+
`).all();
|
|
885
|
+
const schema = new Map(schemaRows.map((row) => [row.name, row]));
|
|
886
|
+
try {
|
|
887
|
+
const rows = db.prepare(`
|
|
888
|
+
SELECT name, COUNT(*) AS pages, SUM(pgsize) AS bytes
|
|
889
|
+
FROM dbstat
|
|
890
|
+
WHERE name IN (
|
|
891
|
+
SELECT name
|
|
892
|
+
FROM sqlite_schema
|
|
893
|
+
WHERE type IN ('table', 'index')
|
|
894
|
+
AND (
|
|
895
|
+
name IN ('rdf_terms', 'rdf_sources', 'rdf_quads')
|
|
896
|
+
OR tbl_name IN ('rdf_terms', 'rdf_sources', 'rdf_quads')
|
|
897
|
+
)
|
|
898
|
+
)
|
|
899
|
+
GROUP BY name
|
|
900
|
+
ORDER BY name
|
|
901
|
+
`).all();
|
|
902
|
+
if (rows.length > 0) {
|
|
903
|
+
return rows.map((row) => {
|
|
904
|
+
const object = schema.get(row.name);
|
|
905
|
+
const kind = rdfSpaceObjectKind(row.name, object?.type, object?.tbl_name);
|
|
906
|
+
return {
|
|
907
|
+
name: row.name,
|
|
908
|
+
kind,
|
|
909
|
+
...(object?.tbl_name && object.tbl_name !== row.name ? { tableName: object.tbl_name } : {}),
|
|
910
|
+
pages: row.pages,
|
|
911
|
+
bytes: row.bytes ?? 0,
|
|
912
|
+
};
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
// Some SQLite builds do not expose dbstat for in-memory databases.
|
|
918
|
+
}
|
|
919
|
+
return this.estimateSpaceObjectsFromSchema(schemaRows);
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
return [];
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
estimateSpaceObjectsFromSchema(schemaRows) {
|
|
926
|
+
const pageSize = this.estimatePageSize();
|
|
927
|
+
return schemaRows.map((object) => ({
|
|
928
|
+
name: object.name,
|
|
929
|
+
kind: rdfSpaceObjectKind(object.name, object.type, object.tbl_name),
|
|
930
|
+
...(object.tbl_name && object.tbl_name !== object.name ? { tableName: object.tbl_name } : {}),
|
|
931
|
+
pages: 1,
|
|
932
|
+
bytes: pageSize,
|
|
933
|
+
estimated: true,
|
|
934
|
+
}));
|
|
935
|
+
}
|
|
936
|
+
estimatePageSize() {
|
|
937
|
+
try {
|
|
938
|
+
return this.requireDb().prepare('PRAGMA page_size').get()?.page_size ?? 4096;
|
|
939
|
+
}
|
|
940
|
+
catch {
|
|
941
|
+
return 4096;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
initializeSchema() {
|
|
945
|
+
this.requireDb().exec(`
|
|
946
|
+
CREATE TABLE IF NOT EXISTS rdf_sources (
|
|
947
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
948
|
+
source TEXT NOT NULL UNIQUE,
|
|
949
|
+
workspace TEXT NOT NULL,
|
|
950
|
+
local_path TEXT,
|
|
951
|
+
content_type TEXT,
|
|
952
|
+
last_indexed_at TEXT,
|
|
953
|
+
source_version TEXT
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
CREATE TABLE IF NOT EXISTS rdf_quads (
|
|
957
|
+
graph_id INTEGER NOT NULL,
|
|
958
|
+
subject_id INTEGER NOT NULL,
|
|
959
|
+
predicate_id INTEGER NOT NULL,
|
|
960
|
+
object_id INTEGER NOT NULL,
|
|
961
|
+
source_file_id INTEGER,
|
|
962
|
+
source_line_no INTEGER,
|
|
963
|
+
PRIMARY KEY (graph_id, subject_id, predicate_id, object_id),
|
|
964
|
+
FOREIGN KEY (graph_id) REFERENCES rdf_terms(id),
|
|
965
|
+
FOREIGN KEY (subject_id) REFERENCES rdf_terms(id),
|
|
966
|
+
FOREIGN KEY (predicate_id) REFERENCES rdf_terms(id),
|
|
967
|
+
FOREIGN KEY (object_id) REFERENCES rdf_terms(id),
|
|
968
|
+
FOREIGN KEY (source_file_id) REFERENCES rdf_sources(id)
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
CREATE INDEX IF NOT EXISTS rdf_quads_spog ON rdf_quads(subject_id, predicate_id, object_id, graph_id);
|
|
972
|
+
CREATE INDEX IF NOT EXISTS rdf_quads_sopg ON rdf_quads(subject_id, object_id, predicate_id, graph_id);
|
|
973
|
+
CREATE INDEX IF NOT EXISTS rdf_quads_psog ON rdf_quads(predicate_id, subject_id, object_id, graph_id);
|
|
974
|
+
CREATE INDEX IF NOT EXISTS rdf_quads_posg ON rdf_quads(predicate_id, object_id, subject_id, graph_id);
|
|
975
|
+
CREATE INDEX IF NOT EXISTS rdf_quads_ospg ON rdf_quads(object_id, subject_id, predicate_id, graph_id);
|
|
976
|
+
CREATE INDEX IF NOT EXISTS rdf_quads_opsg ON rdf_quads(object_id, predicate_id, subject_id, graph_id);
|
|
977
|
+
CREATE INDEX IF NOT EXISTS rdf_quads_gspo ON rdf_quads(graph_id, subject_id, predicate_id, object_id);
|
|
978
|
+
CREATE INDEX IF NOT EXISTS rdf_quads_gpos ON rdf_quads(graph_id, predicate_id, object_id, subject_id);
|
|
979
|
+
CREATE INDEX IF NOT EXISTS rdf_quads_source ON rdf_quads(source_file_id);
|
|
980
|
+
`);
|
|
981
|
+
}
|
|
982
|
+
upsertSource(source) {
|
|
983
|
+
const db = this.requireDb();
|
|
984
|
+
db.prepare(`
|
|
985
|
+
INSERT INTO rdf_sources (
|
|
986
|
+
source,
|
|
987
|
+
workspace,
|
|
988
|
+
local_path,
|
|
989
|
+
content_type,
|
|
990
|
+
last_indexed_at,
|
|
991
|
+
source_version
|
|
992
|
+
)
|
|
993
|
+
VALUES (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), ?)
|
|
994
|
+
ON CONFLICT (source)
|
|
995
|
+
DO UPDATE SET
|
|
996
|
+
workspace = excluded.workspace,
|
|
997
|
+
local_path = excluded.local_path,
|
|
998
|
+
content_type = excluded.content_type,
|
|
999
|
+
last_indexed_at = excluded.last_indexed_at,
|
|
1000
|
+
source_version = excluded.source_version
|
|
1001
|
+
`).run(source.source, source.workspace, source.localPath ?? null, source.contentType ?? null, source.sourceVersion ?? null);
|
|
1002
|
+
const row = db.prepare('SELECT * FROM rdf_sources WHERE source = ?').get(source.source);
|
|
1003
|
+
if (!row) {
|
|
1004
|
+
throw new Error(`Failed to upsert RDF source: ${source.source}`);
|
|
1005
|
+
}
|
|
1006
|
+
return row.id;
|
|
1007
|
+
}
|
|
1008
|
+
buildWhereClause(pattern, allowUnresolved) {
|
|
1009
|
+
const conditions = [];
|
|
1010
|
+
const joins = [];
|
|
1011
|
+
const params = [];
|
|
1012
|
+
const queryPlan = [];
|
|
1013
|
+
const equalityColumns = new Set();
|
|
1014
|
+
for (const key of TERM_KEYS) {
|
|
1015
|
+
const match = pattern[key];
|
|
1016
|
+
if (!match) {
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
const column = TERM_COLUMN[key];
|
|
1020
|
+
const condition = this.matchToCondition(key, column, match, allowUnresolved);
|
|
1021
|
+
if (condition.unresolved) {
|
|
1022
|
+
return { joins: '', whereClause: '', params: [], indexHint: 'none', queryPlan, unresolved: key };
|
|
1023
|
+
}
|
|
1024
|
+
joins.push(...condition.joins);
|
|
1025
|
+
if (condition.sql) {
|
|
1026
|
+
conditions.push(condition.sql);
|
|
1027
|
+
params.push(...condition.params);
|
|
1028
|
+
}
|
|
1029
|
+
queryPlan.push(...condition.queryPlan);
|
|
1030
|
+
if (condition.equality) {
|
|
1031
|
+
equalityColumns.add(column);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
joins: joins.join(''),
|
|
1036
|
+
whereClause: conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '',
|
|
1037
|
+
params,
|
|
1038
|
+
indexHint: this.chooseIndex(equalityColumns),
|
|
1039
|
+
queryPlan,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
matchToCondition(key, column, match, allowUnresolved, scope) {
|
|
1043
|
+
const columnRef = this.scopedQuadColumn(column, scope);
|
|
1044
|
+
if ((0, types_1.isTerm)(match)) {
|
|
1045
|
+
const id = this.requireDictionary().find(match);
|
|
1046
|
+
if (id === undefined) {
|
|
1047
|
+
if (allowUnresolved) {
|
|
1048
|
+
return { joins: [], params: [], queryPlan: [], unresolved: true };
|
|
1049
|
+
}
|
|
1050
|
+
return { joins: [], sql: `${columnRef} = ?`, params: [-1], equality: true, queryPlan: [] };
|
|
1051
|
+
}
|
|
1052
|
+
return { joins: [], sql: `${columnRef} = ?`, params: [id], equality: true, queryPlan: [] };
|
|
1053
|
+
}
|
|
1054
|
+
const ops = match;
|
|
1055
|
+
const fragments = [];
|
|
1056
|
+
const joins = [];
|
|
1057
|
+
const params = [];
|
|
1058
|
+
const queryPlan = [];
|
|
1059
|
+
let equality = false;
|
|
1060
|
+
if (ops.$eq !== undefined) {
|
|
1061
|
+
const id = this.termOperatorValueId(ops.$eq, allowUnresolved);
|
|
1062
|
+
if (id === undefined)
|
|
1063
|
+
return { joins, params: [], queryPlan, unresolved: true };
|
|
1064
|
+
fragments.push(`${columnRef} = ?`);
|
|
1065
|
+
params.push(id);
|
|
1066
|
+
equality = true;
|
|
1067
|
+
}
|
|
1068
|
+
if (ops.$in !== undefined) {
|
|
1069
|
+
const ids = uniqueNumbers(ops.$in
|
|
1070
|
+
.map((value) => this.termOperatorValueId(value, allowUnresolved))
|
|
1071
|
+
.filter((value) => value !== undefined));
|
|
1072
|
+
if (ids.length === 0) {
|
|
1073
|
+
return { joins, params: [], queryPlan, unresolved: true };
|
|
1074
|
+
}
|
|
1075
|
+
if (ids.length > TERM_IN_JOIN_THRESHOLD) {
|
|
1076
|
+
const candidateTable = this.populateTermInCandidateTable(column, 'in', ids, scope);
|
|
1077
|
+
const candidateAlias = this.scopedSqlName(`${column}_in_candidates`, scope);
|
|
1078
|
+
joins.push(` JOIN ${candidateTable} ${candidateAlias} ON ${candidateAlias}.id = ${columnRef}`);
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
fragments.push(`${columnRef} IN (${ids.map(() => '?').join(', ')})`);
|
|
1082
|
+
params.push(...ids);
|
|
1083
|
+
}
|
|
1084
|
+
queryPlan.push(`TermIn(${key})`);
|
|
1085
|
+
equality = true;
|
|
1086
|
+
}
|
|
1087
|
+
if (ops.$ne !== undefined) {
|
|
1088
|
+
const id = this.termOperatorValueId(ops.$ne, allowUnresolved);
|
|
1089
|
+
if (id !== undefined) {
|
|
1090
|
+
fragments.push(`${columnRef} != ?`);
|
|
1091
|
+
params.push(id);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
for (const operator of ['$gt', '$gte', '$lt', '$lte']) {
|
|
1095
|
+
if (ops[operator] !== undefined) {
|
|
1096
|
+
const range = this.termRangeCondition(key, column, operator, ops[operator], allowUnresolved, scope);
|
|
1097
|
+
if (range.unresolved) {
|
|
1098
|
+
return { joins, params: [], queryPlan, unresolved: true };
|
|
1099
|
+
}
|
|
1100
|
+
joins.push(...(range.joins ?? []));
|
|
1101
|
+
fragments.push(range.sql);
|
|
1102
|
+
params.push(...range.params);
|
|
1103
|
+
queryPlan.push(...(range.queryPlan ?? []));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (ops.$notIn !== undefined) {
|
|
1107
|
+
const ids = uniqueNumbers(ops.$notIn
|
|
1108
|
+
.map((value) => this.termOperatorValueId(value, allowUnresolved))
|
|
1109
|
+
.filter((value) => value !== undefined));
|
|
1110
|
+
if (ids.length > 0) {
|
|
1111
|
+
if (ids.length > TERM_IN_JOIN_THRESHOLD) {
|
|
1112
|
+
const candidateTable = this.populateTermInCandidateTable(column, 'not_in', ids, scope);
|
|
1113
|
+
const candidateAlias = this.scopedSqlName(`${column}_not_in_candidates`, scope);
|
|
1114
|
+
joins.push(` LEFT JOIN ${candidateTable} ${candidateAlias} ON ${candidateAlias}.id = ${columnRef}`);
|
|
1115
|
+
fragments.push(`${candidateAlias}.id IS NULL`);
|
|
1116
|
+
}
|
|
1117
|
+
else {
|
|
1118
|
+
fragments.push(`${columnRef} NOT IN (${ids.map(() => '?').join(', ')})`);
|
|
1119
|
+
params.push(...ids);
|
|
1120
|
+
}
|
|
1121
|
+
queryPlan.push(`TermNotIn(${key})`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (ops.$termType !== undefined) {
|
|
1125
|
+
const condition = this.termTypeConditionJoin(key, column, ops.$termType, scope);
|
|
1126
|
+
joins.push(condition.join);
|
|
1127
|
+
fragments.push(condition.sql);
|
|
1128
|
+
params.push(...condition.params);
|
|
1129
|
+
queryPlan.push(condition.queryPlan);
|
|
1130
|
+
}
|
|
1131
|
+
if (ops.$language !== undefined) {
|
|
1132
|
+
const condition = this.languageConditionJoin(key, column, '$language', ops.$language, scope);
|
|
1133
|
+
joins.push(condition.join);
|
|
1134
|
+
fragments.push(condition.sql);
|
|
1135
|
+
params.push(...condition.params);
|
|
1136
|
+
queryPlan.push(condition.queryPlan);
|
|
1137
|
+
}
|
|
1138
|
+
if (ops.$notLanguage !== undefined) {
|
|
1139
|
+
const condition = this.languageConditionJoin(key, column, '$notLanguage', ops.$notLanguage, scope);
|
|
1140
|
+
joins.push(condition.join);
|
|
1141
|
+
fragments.push(condition.sql);
|
|
1142
|
+
params.push(...condition.params);
|
|
1143
|
+
queryPlan.push(condition.queryPlan);
|
|
1144
|
+
}
|
|
1145
|
+
if (ops.$langMatches !== undefined) {
|
|
1146
|
+
const condition = this.languageConditionJoin(key, column, '$langMatches', ops.$langMatches, scope);
|
|
1147
|
+
joins.push(condition.join);
|
|
1148
|
+
fragments.push(condition.sql);
|
|
1149
|
+
params.push(...condition.params);
|
|
1150
|
+
queryPlan.push(condition.queryPlan);
|
|
1151
|
+
}
|
|
1152
|
+
if (ops.$datatype !== undefined) {
|
|
1153
|
+
const condition = this.datatypeConditionJoin(key, column, '$datatype', ops.$datatype, allowUnresolved, scope);
|
|
1154
|
+
if (condition.unresolved) {
|
|
1155
|
+
return { joins, params: [], queryPlan, unresolved: true };
|
|
1156
|
+
}
|
|
1157
|
+
joins.push(condition.join);
|
|
1158
|
+
fragments.push(condition.sql);
|
|
1159
|
+
params.push(...condition.params);
|
|
1160
|
+
queryPlan.push(condition.queryPlan);
|
|
1161
|
+
}
|
|
1162
|
+
if (ops.$notDatatype !== undefined) {
|
|
1163
|
+
const condition = this.datatypeConditionJoin(key, column, '$notDatatype', ops.$notDatatype, allowUnresolved, scope);
|
|
1164
|
+
if (condition.unresolved) {
|
|
1165
|
+
return { joins, params: [], queryPlan, unresolved: true };
|
|
1166
|
+
}
|
|
1167
|
+
joins.push(condition.join);
|
|
1168
|
+
fragments.push(condition.sql);
|
|
1169
|
+
params.push(...condition.params);
|
|
1170
|
+
queryPlan.push(condition.queryPlan);
|
|
1171
|
+
}
|
|
1172
|
+
if (ops.$startsWith !== undefined) {
|
|
1173
|
+
const condition = this.prefixSearchConditionJoin(key, column, ops.$startsWith, scope);
|
|
1174
|
+
joins.push(condition.join);
|
|
1175
|
+
fragments.push(condition.sql);
|
|
1176
|
+
params.push(...condition.params);
|
|
1177
|
+
queryPlan.push(`PrefixRange(${key})`);
|
|
1178
|
+
equality = true;
|
|
1179
|
+
}
|
|
1180
|
+
equality = this.addTextSearchCondition(joins, fragments, params, queryPlan, key, column, '$contains', ops.$contains, scope) || equality;
|
|
1181
|
+
equality = this.addTextSearchCondition(joins, fragments, params, queryPlan, key, column, '$endsWith', ops.$endsWith, scope) || equality;
|
|
1182
|
+
equality = this.addTextSearchCondition(joins, fragments, params, queryPlan, key, column, '$regex', ops.$regex, scope) || equality;
|
|
1183
|
+
return {
|
|
1184
|
+
joins,
|
|
1185
|
+
sql: fragments.length > 0 ? fragments.join(' AND ') : undefined,
|
|
1186
|
+
params,
|
|
1187
|
+
equality,
|
|
1188
|
+
queryPlan,
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
termTypeConditionJoin(key, column, termType, scope) {
|
|
1192
|
+
const alias = this.scopedSqlName(`${column}_term_type_${termType}`, scope);
|
|
1193
|
+
const columnRef = this.scopedQuadColumn(column, scope);
|
|
1194
|
+
const possibleKinds = this.termKindsForPatternKey(key);
|
|
1195
|
+
if (termType === 'numeric') {
|
|
1196
|
+
if (!possibleKinds.includes('literal')) {
|
|
1197
|
+
return {
|
|
1198
|
+
join: ` JOIN rdf_terms ${alias} ON ${alias}.id = ${columnRef}`,
|
|
1199
|
+
sql: '1 = 0',
|
|
1200
|
+
params: [],
|
|
1201
|
+
queryPlan: `TermType(${key}:numeric)`,
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
return {
|
|
1205
|
+
join: ` JOIN rdf_terms ${alias} ON ${alias}.id = ${columnRef}`,
|
|
1206
|
+
sql: `${alias}.kind = 'literal' AND ${alias}.numeric_value IS NOT NULL`,
|
|
1207
|
+
params: [],
|
|
1208
|
+
queryPlan: `TermType(${key}:numeric)`,
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
if (!possibleKinds.includes(termType)) {
|
|
1212
|
+
return {
|
|
1213
|
+
join: ` JOIN rdf_terms ${alias} ON ${alias}.id = ${columnRef}`,
|
|
1214
|
+
sql: '1 = 0',
|
|
1215
|
+
params: [],
|
|
1216
|
+
queryPlan: `TermType(${key}:${termType})`,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
join: ` JOIN rdf_terms ${alias} ON ${alias}.id = ${columnRef}`,
|
|
1221
|
+
sql: `${alias}.kind = ?`,
|
|
1222
|
+
params: [termType],
|
|
1223
|
+
queryPlan: `TermType(${key}:${termType})`,
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
languageConditionJoin(key, column, operator, language, scope) {
|
|
1227
|
+
const alias = this.scopedSqlName(`${column}_${operator.slice(1).toLowerCase()}`, scope);
|
|
1228
|
+
const columnRef = this.scopedQuadColumn(column, scope);
|
|
1229
|
+
if (!this.termKindsForPatternKey(key).includes('literal')) {
|
|
1230
|
+
return {
|
|
1231
|
+
join: ` JOIN rdf_terms ${alias} ON ${alias}.id = ${columnRef}`,
|
|
1232
|
+
sql: '1 = 0',
|
|
1233
|
+
params: [],
|
|
1234
|
+
queryPlan: `Language(${key}${operator})`,
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
const join = ` JOIN rdf_terms ${alias} ON ${alias}.id = ${columnRef}`;
|
|
1238
|
+
if (operator === '$language') {
|
|
1239
|
+
return {
|
|
1240
|
+
join,
|
|
1241
|
+
sql: `${alias}.kind = 'literal' AND COALESCE(${alias}.lang, '') = ?`,
|
|
1242
|
+
params: [language],
|
|
1243
|
+
queryPlan: `Language(${key}${operator})`,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
if (operator === '$notLanguage') {
|
|
1247
|
+
return {
|
|
1248
|
+
join,
|
|
1249
|
+
sql: `${alias}.kind = 'literal' AND COALESCE(${alias}.lang, '') != ?`,
|
|
1250
|
+
params: [language],
|
|
1251
|
+
queryPlan: `Language(${key}${operator})`,
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
if (language === '*') {
|
|
1255
|
+
return {
|
|
1256
|
+
join,
|
|
1257
|
+
sql: `${alias}.kind = 'literal' AND ${alias}.lang IS NOT NULL AND ${alias}.lang != ''`,
|
|
1258
|
+
params: [],
|
|
1259
|
+
queryPlan: `Language(${key}${operator})`,
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
join,
|
|
1264
|
+
sql: `${alias}.kind = 'literal'
|
|
1265
|
+
AND (lower(${alias}.lang) = lower(?) OR lower(${alias}.lang) LIKE lower(?) ESCAPE '\\')`,
|
|
1266
|
+
params: [language, `${escapeLikePattern(language)}-%`],
|
|
1267
|
+
queryPlan: `Language(${key}${operator})`,
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
datatypeConditionJoin(key, column, operator, datatype, allowUnresolved, scope) {
|
|
1271
|
+
const alias = this.scopedSqlName(`${column}_${operator.slice(1).toLowerCase()}`, scope);
|
|
1272
|
+
const columnRef = this.scopedQuadColumn(column, scope);
|
|
1273
|
+
if (datatype.termType !== 'NamedNode') {
|
|
1274
|
+
throw new Error('RdfQuadIndex datatype filters only support named node datatype terms');
|
|
1275
|
+
}
|
|
1276
|
+
if (!this.termKindsForPatternKey(key).includes('literal')) {
|
|
1277
|
+
return {
|
|
1278
|
+
join: ` JOIN rdf_terms ${alias} ON ${alias}.id = ${columnRef}`,
|
|
1279
|
+
sql: '1 = 0',
|
|
1280
|
+
params: [],
|
|
1281
|
+
queryPlan: `Datatype(${key}${operator})`,
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
const join = ` JOIN rdf_terms ${alias} ON ${alias}.id = ${columnRef}`;
|
|
1285
|
+
if (datatype.value === XSD_STRING) {
|
|
1286
|
+
return {
|
|
1287
|
+
join,
|
|
1288
|
+
sql: operator === '$datatype'
|
|
1289
|
+
? `${alias}.kind = 'literal' AND ${alias}.lang IS NULL AND ${alias}.datatype_id IS NULL`
|
|
1290
|
+
: `${alias}.kind = 'literal' AND NOT (${alias}.lang IS NULL AND ${alias}.datatype_id IS NULL)`,
|
|
1291
|
+
params: [],
|
|
1292
|
+
queryPlan: `Datatype(${key}${operator})`,
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
const datatypeId = this.requireDictionary().find(datatype);
|
|
1296
|
+
if (datatypeId === undefined) {
|
|
1297
|
+
if (operator === '$notDatatype') {
|
|
1298
|
+
return {
|
|
1299
|
+
join,
|
|
1300
|
+
sql: `${alias}.kind = 'literal'`,
|
|
1301
|
+
params: [],
|
|
1302
|
+
queryPlan: `Datatype(${key}${operator})`,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
if (allowUnresolved) {
|
|
1306
|
+
return { join, sql: '', params: [], queryPlan: `Datatype(${key}${operator})`, unresolved: true };
|
|
1307
|
+
}
|
|
1308
|
+
return {
|
|
1309
|
+
join,
|
|
1310
|
+
sql: operator === '$datatype' ? '1 = 0' : `${alias}.kind = 'literal'`,
|
|
1311
|
+
params: [],
|
|
1312
|
+
queryPlan: `Datatype(${key}${operator})`,
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
return {
|
|
1316
|
+
join,
|
|
1317
|
+
sql: operator === '$datatype'
|
|
1318
|
+
? `${alias}.kind = 'literal' AND ${alias}.datatype_id = ?`
|
|
1319
|
+
: `${alias}.kind = 'literal' AND (${alias}.datatype_id IS NULL OR ${alias}.datatype_id != ?)`,
|
|
1320
|
+
params: [datatypeId],
|
|
1321
|
+
queryPlan: `Datatype(${key}${operator})`,
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
termRangeCondition(key, column, operator, value, allowUnresolved, scope) {
|
|
1325
|
+
const kinds = this.termKindsForPatternKey(key);
|
|
1326
|
+
const lexicalValue = this.termOperatorLexicalValue(value);
|
|
1327
|
+
if (lexicalValue === undefined) {
|
|
1328
|
+
if (allowUnresolved) {
|
|
1329
|
+
return { sql: '', params: [], unresolved: true };
|
|
1330
|
+
}
|
|
1331
|
+
return { sql: '1 = 0', params: [] };
|
|
1332
|
+
}
|
|
1333
|
+
const comparator = {
|
|
1334
|
+
$gt: '>',
|
|
1335
|
+
$gte: '>=',
|
|
1336
|
+
$lt: '<',
|
|
1337
|
+
$lte: '<=',
|
|
1338
|
+
}[operator];
|
|
1339
|
+
const numericValue = this.termOperatorNumericValue(value);
|
|
1340
|
+
if (numericValue !== undefined) {
|
|
1341
|
+
if (!kinds.includes('literal')) {
|
|
1342
|
+
if (allowUnresolved) {
|
|
1343
|
+
return { sql: '', params: [], unresolved: true };
|
|
1344
|
+
}
|
|
1345
|
+
return { sql: '1 = 0', params: [] };
|
|
1346
|
+
}
|
|
1347
|
+
const termAlias = this.scopedSqlName(`${column}_numeric_range_${operator.slice(1)}`, scope);
|
|
1348
|
+
return {
|
|
1349
|
+
joins: [` JOIN rdf_terms ${termAlias} ON ${termAlias}.id = ${this.scopedQuadColumn(column, scope)}`],
|
|
1350
|
+
sql: `${termAlias}.kind = 'literal'
|
|
1351
|
+
AND ${termAlias}.numeric_value IS NOT NULL
|
|
1352
|
+
AND ${termAlias}.numeric_value ${comparator} ?`,
|
|
1353
|
+
params: [numericValue],
|
|
1354
|
+
queryPlan: [`NumericRange(${key}${operator})`],
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
const termAlias = this.scopedSqlName(`${column}_range_${operator.slice(1)}`, scope);
|
|
1358
|
+
return {
|
|
1359
|
+
joins: [` JOIN rdf_terms ${termAlias} ON ${termAlias}.id = ${this.scopedQuadColumn(column, scope)}`],
|
|
1360
|
+
sql: `${termAlias}.kind IN (${kinds.map(() => '?').join(', ')})
|
|
1361
|
+
AND ${termAlias}.value ${comparator} ?`,
|
|
1362
|
+
params: [...kinds, lexicalValue],
|
|
1363
|
+
queryPlan: [`LexicalRange(${key}${operator})`],
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
termOperatorLexicalValue(value) {
|
|
1367
|
+
if (!value || typeof value !== 'object' || !('termType' in value)) {
|
|
1368
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
1369
|
+
return String(value);
|
|
1370
|
+
}
|
|
1371
|
+
throw new Error('RdfQuadIndex range operators only support Term, string, or number values');
|
|
1372
|
+
}
|
|
1373
|
+
const term = value;
|
|
1374
|
+
switch (term.termType) {
|
|
1375
|
+
case 'NamedNode':
|
|
1376
|
+
case 'BlankNode':
|
|
1377
|
+
case 'Literal':
|
|
1378
|
+
return term.value;
|
|
1379
|
+
case 'DefaultGraph':
|
|
1380
|
+
return '';
|
|
1381
|
+
default:
|
|
1382
|
+
throw new Error(`RdfQuadIndex range operators do not support ${term.termType}`);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
termOperatorNumericValue(value) {
|
|
1386
|
+
if (typeof value === 'number') {
|
|
1387
|
+
return Number.isFinite(value) ? value : undefined;
|
|
1388
|
+
}
|
|
1389
|
+
if (!value || typeof value !== 'object' || !('termType' in value)) {
|
|
1390
|
+
return undefined;
|
|
1391
|
+
}
|
|
1392
|
+
const term = value;
|
|
1393
|
+
if (term.termType !== 'Literal' || !(0, RdfTermSemantics_1.isRdfNumericDatatype)(term.datatype.value)) {
|
|
1394
|
+
return undefined;
|
|
1395
|
+
}
|
|
1396
|
+
const parsed = (0, RdfTermSemantics_1.rdfNumericValue)(term.value);
|
|
1397
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1398
|
+
}
|
|
1399
|
+
termKindsForPatternKey(key) {
|
|
1400
|
+
switch (key) {
|
|
1401
|
+
case 'object':
|
|
1402
|
+
return ['iri', 'literal', 'blank'];
|
|
1403
|
+
case 'subject':
|
|
1404
|
+
return ['iri', 'blank'];
|
|
1405
|
+
case 'graph':
|
|
1406
|
+
return ['iri', 'default_graph'];
|
|
1407
|
+
case 'predicate':
|
|
1408
|
+
return ['iri'];
|
|
1409
|
+
default: {
|
|
1410
|
+
const exhaustive = key;
|
|
1411
|
+
throw new Error(`Unsupported RDF pattern key: ${exhaustive}`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
termOperatorValueId(value, allowUnresolved) {
|
|
1416
|
+
if (!value || typeof value !== 'object' || !('termType' in value)) {
|
|
1417
|
+
throw new Error('RdfQuadIndex only supports Term values in first-stage exact scans');
|
|
1418
|
+
}
|
|
1419
|
+
const id = this.requireDictionary().find(value);
|
|
1420
|
+
if (id === undefined && !allowUnresolved) {
|
|
1421
|
+
return -1;
|
|
1422
|
+
}
|
|
1423
|
+
return id;
|
|
1424
|
+
}
|
|
1425
|
+
addTextSearchCondition(joins, fragments, params, queryPlan, key, column, operator, value, scope) {
|
|
1426
|
+
if (value === undefined) {
|
|
1427
|
+
return false;
|
|
1428
|
+
}
|
|
1429
|
+
if (typeof value !== 'string') {
|
|
1430
|
+
throw new Error(`RdfQuadIndex text search ${operator} only supports string values`);
|
|
1431
|
+
}
|
|
1432
|
+
const kind = this.termKindsForPatternKey(key);
|
|
1433
|
+
queryPlan.push(`TextSearch(${key}${operator})`);
|
|
1434
|
+
const condition = this.textSearchConditionJoin(kind, column, operator, value, scope);
|
|
1435
|
+
joins.push(condition.join);
|
|
1436
|
+
fragments.push(condition.sql);
|
|
1437
|
+
params.push(...condition.params);
|
|
1438
|
+
return true;
|
|
1439
|
+
}
|
|
1440
|
+
prefixSearchConditionJoin(key, column, prefix, scope) {
|
|
1441
|
+
const kind = this.termKindsForPatternKey(key);
|
|
1442
|
+
const alias = this.scopedSqlName(`prefix_${column}`, scope);
|
|
1443
|
+
return {
|
|
1444
|
+
join: ` JOIN rdf_terms ${alias} ON ${alias}.id = ${this.scopedQuadColumn(column, scope)}`,
|
|
1445
|
+
sql: `${alias}.kind IN (${kind.map(() => '?').join(', ')})
|
|
1446
|
+
AND ${alias}.value >= ?
|
|
1447
|
+
AND ${alias}.value < ?`,
|
|
1448
|
+
params: [...kind, prefix, `${prefix}\uffff`],
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
textSearchConditionJoin(kind, column, operator, value, scope) {
|
|
1452
|
+
const alias = this.scopedSqlName(`text_${column}_${operator.slice(1).toLowerCase()}`, scope);
|
|
1453
|
+
if (kind.length === 0) {
|
|
1454
|
+
return {
|
|
1455
|
+
join: ` JOIN rdf_terms ${alias} ON ${alias}.id = ${this.scopedQuadColumn(column, scope)}`,
|
|
1456
|
+
sql: '1 = 0',
|
|
1457
|
+
params: [],
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
const kindPlaceholders = kind.map(() => '?').join(', ');
|
|
1461
|
+
const normalized = value.toLowerCase();
|
|
1462
|
+
const join = ` JOIN rdf_terms ${alias} ON ${alias}.id = ${this.scopedQuadColumn(column, scope)}`;
|
|
1463
|
+
switch (operator) {
|
|
1464
|
+
case '$contains':
|
|
1465
|
+
return {
|
|
1466
|
+
join,
|
|
1467
|
+
sql: `${alias}.kind IN (${kindPlaceholders})
|
|
1468
|
+
AND ${alias}.normalized_text LIKE ? ESCAPE '\\'
|
|
1469
|
+
AND instr(${alias}.value, ?) > 0`,
|
|
1470
|
+
params: [...kind, `%${escapeLikePattern(normalized)}%`, value],
|
|
1471
|
+
};
|
|
1472
|
+
case '$endsWith':
|
|
1473
|
+
return {
|
|
1474
|
+
join,
|
|
1475
|
+
sql: `${alias}.kind IN (${kindPlaceholders})
|
|
1476
|
+
AND ${alias}.normalized_text LIKE ? ESCAPE '\\'
|
|
1477
|
+
AND substr(${alias}.value, -length(?)) = ?`,
|
|
1478
|
+
params: [...kind, `%${escapeLikePattern(normalized)}`, value, value],
|
|
1479
|
+
};
|
|
1480
|
+
case '$regex':
|
|
1481
|
+
return this.regexTextSearchConditionJoin(kind, alias, column, value, scope);
|
|
1482
|
+
default:
|
|
1483
|
+
throw new Error(`Unsupported RDF text search operator: ${operator}`);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
regexTextSearchConditionJoin(kind, alias, column, pattern, scope) {
|
|
1487
|
+
const ids = this.requireDictionary().idsByNormalizedTextRegex(kind, pattern);
|
|
1488
|
+
const candidateTable = this.populateRegexCandidateTable(column, ids, scope);
|
|
1489
|
+
const candidateAlias = `${alias}_candidates`;
|
|
1490
|
+
const join = ` JOIN rdf_terms ${alias} ON ${alias}.id = ${this.scopedQuadColumn(column, scope)}
|
|
1491
|
+
JOIN ${candidateTable} ${candidateAlias} ON ${candidateAlias}.id = ${alias}.id`;
|
|
1492
|
+
if (ids.length === 0) {
|
|
1493
|
+
return { join, sql: '1 = 0', params: [] };
|
|
1494
|
+
}
|
|
1495
|
+
const kindPlaceholders = kind.map(() => '?').join(', ');
|
|
1496
|
+
return {
|
|
1497
|
+
join,
|
|
1498
|
+
sql: `${alias}.kind IN (${kindPlaceholders})`,
|
|
1499
|
+
params: kind,
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
populateRegexCandidateTable(column, ids, scope) {
|
|
1503
|
+
const tableName = this.scopedSqlName(`rdf_regex_candidates_${column}`, scope);
|
|
1504
|
+
this.populateCandidateTable(tableName, ids);
|
|
1505
|
+
return tableName;
|
|
1506
|
+
}
|
|
1507
|
+
populateTermInCandidateTable(column, operator, ids, scope) {
|
|
1508
|
+
const tableName = this.scopedSqlName(`rdf_term_${operator}_candidates_${column}`, scope);
|
|
1509
|
+
this.populateCandidateTable(tableName, ids);
|
|
1510
|
+
return tableName;
|
|
1511
|
+
}
|
|
1512
|
+
scopedQuadColumn(column, scope) {
|
|
1513
|
+
return `${scope?.quadAlias ?? 'rdf_quads'}.${column}`;
|
|
1514
|
+
}
|
|
1515
|
+
scopedSqlName(base, scope) {
|
|
1516
|
+
const name = scope?.aliasPrefix ? `${scope.aliasPrefix}_${base}` : base;
|
|
1517
|
+
return name.replace(/[^A-Za-z0-9_]/g, '_');
|
|
1518
|
+
}
|
|
1519
|
+
buildTupleConstraintJoin(source) {
|
|
1520
|
+
const columns = Array.from(new Set(source.columns));
|
|
1521
|
+
if (columns.length === 0) {
|
|
1522
|
+
return { join: '' };
|
|
1523
|
+
}
|
|
1524
|
+
const candidateColumns = columns.map((key) => TERM_COLUMN[key]);
|
|
1525
|
+
const tableName = `rdf_tuple_values_${candidateColumns.join('_')}`;
|
|
1526
|
+
this.populateTupleConstraintTable(tableName, columns, source.rows);
|
|
1527
|
+
const alias = 'tuple_values';
|
|
1528
|
+
const onClause = columns
|
|
1529
|
+
.map((key) => `${alias}.${TERM_COLUMN[key]} = rdf_quads.${TERM_COLUMN[key]}`)
|
|
1530
|
+
.join(' AND ');
|
|
1531
|
+
return {
|
|
1532
|
+
join: ` JOIN ${tableName} ${alias} ON ${onClause}`,
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
populateTupleConstraintTable(tableName, columns, rows) {
|
|
1536
|
+
const db = this.requireDb();
|
|
1537
|
+
const columnDefs = columns.map((key) => `${TERM_COLUMN[key]} INTEGER NOT NULL`).join(', ');
|
|
1538
|
+
const primaryKey = columns.map((key) => TERM_COLUMN[key]).join(', ');
|
|
1539
|
+
db.exec(`CREATE TEMP TABLE IF NOT EXISTS ${tableName} (${columnDefs}, PRIMARY KEY (${primaryKey}))`);
|
|
1540
|
+
db.prepare(`DELETE FROM ${tableName}`).run();
|
|
1541
|
+
const valueRows = rows
|
|
1542
|
+
.map((row) => columns.map((key) => this.termIdForTupleConstraint(row[key])))
|
|
1543
|
+
.filter((ids) => ids.every((id) => id !== undefined));
|
|
1544
|
+
if (valueRows.length === 0) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const insertColumns = columns.map((key) => TERM_COLUMN[key]).join(', ');
|
|
1548
|
+
const placeholders = `(${columns.map(() => '?').join(', ')})`;
|
|
1549
|
+
const insert = db.prepare(`INSERT OR IGNORE INTO ${tableName} (${insertColumns}) VALUES ${placeholders}`);
|
|
1550
|
+
for (const valueRow of valueRows) {
|
|
1551
|
+
insert.run(...valueRow);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
termIdForTupleConstraint(term) {
|
|
1555
|
+
if (!term) {
|
|
1556
|
+
return undefined;
|
|
1557
|
+
}
|
|
1558
|
+
return this.requireDictionary().find(term);
|
|
1559
|
+
}
|
|
1560
|
+
populateCandidateTable(tableName, ids) {
|
|
1561
|
+
const db = this.requireDb();
|
|
1562
|
+
db.exec(`CREATE TEMP TABLE IF NOT EXISTS ${tableName} (id INTEGER PRIMARY KEY)`);
|
|
1563
|
+
db.prepare(`DELETE FROM ${tableName}`).run();
|
|
1564
|
+
for (let offset = 0; offset < ids.length; offset += 500) {
|
|
1565
|
+
const batch = ids.slice(offset, offset + 500);
|
|
1566
|
+
db.prepare(`INSERT OR IGNORE INTO ${tableName} (id) VALUES ${batch.map(() => '(?)').join(', ')}`).run(...batch);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
chooseIndex(columns) {
|
|
1570
|
+
const has = (column) => columns.has(column);
|
|
1571
|
+
if (has('graph_id') && has('subject_id'))
|
|
1572
|
+
return 'GSPO';
|
|
1573
|
+
if (has('graph_id') && has('predicate_id'))
|
|
1574
|
+
return 'GPOS';
|
|
1575
|
+
if (has('subject_id') && has('predicate_id'))
|
|
1576
|
+
return 'SPOG';
|
|
1577
|
+
if (has('subject_id') && has('object_id'))
|
|
1578
|
+
return 'SOPG';
|
|
1579
|
+
if (has('predicate_id') && has('object_id'))
|
|
1580
|
+
return 'POSG';
|
|
1581
|
+
if (has('object_id') && has('subject_id'))
|
|
1582
|
+
return 'OSPG';
|
|
1583
|
+
if (has('subject_id'))
|
|
1584
|
+
return 'SPOG';
|
|
1585
|
+
if (has('predicate_id'))
|
|
1586
|
+
return 'POSG';
|
|
1587
|
+
if (has('object_id'))
|
|
1588
|
+
return 'OSPG';
|
|
1589
|
+
if (has('graph_id'))
|
|
1590
|
+
return 'GSPO';
|
|
1591
|
+
return 'full-scan';
|
|
1592
|
+
}
|
|
1593
|
+
exactTermPattern(pattern) {
|
|
1594
|
+
const ids = {};
|
|
1595
|
+
const columns = new Set();
|
|
1596
|
+
for (const key of TERM_KEYS) {
|
|
1597
|
+
const match = pattern[key];
|
|
1598
|
+
if (!match) {
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
if (!(0, types_1.isTerm)(match)) {
|
|
1602
|
+
return undefined;
|
|
1603
|
+
}
|
|
1604
|
+
const id = this.requireDictionary().find(match);
|
|
1605
|
+
if (id === undefined) {
|
|
1606
|
+
return {
|
|
1607
|
+
ids: { [key]: -1 },
|
|
1608
|
+
cacheKey: `${key}=-1`,
|
|
1609
|
+
indexChoice: this.chooseIndex(new Set([TERM_COLUMN[key]])),
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
ids[key] = id;
|
|
1613
|
+
columns.add(TERM_COLUMN[key]);
|
|
1614
|
+
}
|
|
1615
|
+
return {
|
|
1616
|
+
ids,
|
|
1617
|
+
cacheKey: TERM_KEYS
|
|
1618
|
+
.map((key) => `${key}:${ids[key] ?? '*'}`)
|
|
1619
|
+
.join('|'),
|
|
1620
|
+
indexChoice: this.chooseIndex(columns),
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
countExactTermPattern(ids, indexChoice) {
|
|
1624
|
+
if (Object.values(ids).some((id) => id === -1)) {
|
|
1625
|
+
return {
|
|
1626
|
+
rows: 0,
|
|
1627
|
+
source: 'exact-count',
|
|
1628
|
+
indexChoice,
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
const conditions = [];
|
|
1632
|
+
const params = [];
|
|
1633
|
+
for (const key of TERM_KEYS) {
|
|
1634
|
+
const id = ids[key];
|
|
1635
|
+
if (id === undefined) {
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
conditions.push(`${TERM_COLUMN[key]} = ?`);
|
|
1639
|
+
params.push(id);
|
|
1640
|
+
}
|
|
1641
|
+
const sql = `SELECT COUNT(*) AS count FROM rdf_quads${conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : ''}`;
|
|
1642
|
+
const row = this.requireDb().prepare(sql).get(...params);
|
|
1643
|
+
return {
|
|
1644
|
+
rows: row?.count ?? 0,
|
|
1645
|
+
source: 'exact-count',
|
|
1646
|
+
indexChoice,
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
countExactDistinctTermPattern(ids, indexChoice, distinctKey) {
|
|
1650
|
+
if (Object.values(ids).some((id) => id === -1)) {
|
|
1651
|
+
return {
|
|
1652
|
+
rows: 0,
|
|
1653
|
+
source: 'exact-distinct-count',
|
|
1654
|
+
indexChoice,
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
const conditions = [];
|
|
1658
|
+
const params = [];
|
|
1659
|
+
for (const key of TERM_KEYS) {
|
|
1660
|
+
const id = ids[key];
|
|
1661
|
+
if (id === undefined) {
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
conditions.push(`${TERM_COLUMN[key]} = ?`);
|
|
1665
|
+
params.push(id);
|
|
1666
|
+
}
|
|
1667
|
+
const sql = `SELECT COUNT(DISTINCT ${TERM_COLUMN[distinctKey]}) AS count FROM rdf_quads${conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : ''}`;
|
|
1668
|
+
const row = this.requireDb().prepare(sql).get(...params);
|
|
1669
|
+
return {
|
|
1670
|
+
rows: row?.count ?? 0,
|
|
1671
|
+
source: 'exact-distinct-count',
|
|
1672
|
+
indexChoice,
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
countExactDistinctTupleTermPattern(ids, indexChoice, distinctKeys) {
|
|
1676
|
+
if (Object.values(ids).some((id) => id === -1)) {
|
|
1677
|
+
return {
|
|
1678
|
+
rows: 0,
|
|
1679
|
+
source: 'exact-distinct-tuple-count',
|
|
1680
|
+
indexChoice,
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
const conditions = [];
|
|
1684
|
+
const params = [];
|
|
1685
|
+
for (const key of TERM_KEYS) {
|
|
1686
|
+
const id = ids[key];
|
|
1687
|
+
if (id === undefined) {
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
conditions.push(`${TERM_COLUMN[key]} = ?`);
|
|
1691
|
+
params.push(id);
|
|
1692
|
+
}
|
|
1693
|
+
const tupleProjection = distinctKeys.map((key) => TERM_COLUMN[key]).join(', ');
|
|
1694
|
+
const sql = `
|
|
1695
|
+
SELECT COUNT(*) AS count
|
|
1696
|
+
FROM (
|
|
1697
|
+
SELECT DISTINCT ${tupleProjection}
|
|
1698
|
+
FROM rdf_quads${conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : ''}
|
|
1699
|
+
) distinct_tuple
|
|
1700
|
+
`;
|
|
1701
|
+
const row = this.requireDb().prepare(sql).get(...params);
|
|
1702
|
+
return {
|
|
1703
|
+
rows: row?.count ?? 0,
|
|
1704
|
+
source: 'exact-distinct-tuple-count',
|
|
1705
|
+
indexChoice,
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
countDistinctPattern(pattern, distinctKey) {
|
|
1709
|
+
const { joins, whereClause, params, unresolved } = this.buildWhereClause(pattern, true);
|
|
1710
|
+
if (unresolved) {
|
|
1711
|
+
return 0;
|
|
1712
|
+
}
|
|
1713
|
+
const row = this.requireDb()
|
|
1714
|
+
.prepare(`SELECT COUNT(DISTINCT rdf_quads.${TERM_COLUMN[distinctKey]}) AS count FROM rdf_quads${joins}${whereClause}`)
|
|
1715
|
+
.get(...params);
|
|
1716
|
+
return row?.count ?? 0;
|
|
1717
|
+
}
|
|
1718
|
+
countDistinctTuplePattern(pattern, distinctKeys) {
|
|
1719
|
+
const { joins, whereClause, params, unresolved } = this.buildWhereClause(pattern, true);
|
|
1720
|
+
if (unresolved) {
|
|
1721
|
+
return 0;
|
|
1722
|
+
}
|
|
1723
|
+
const tupleProjection = distinctKeys.map((key) => `rdf_quads.${TERM_COLUMN[key]}`).join(', ');
|
|
1724
|
+
const row = this.requireDb()
|
|
1725
|
+
.prepare(`
|
|
1726
|
+
SELECT COUNT(*) AS count
|
|
1727
|
+
FROM (
|
|
1728
|
+
SELECT DISTINCT ${tupleProjection}
|
|
1729
|
+
FROM rdf_quads${joins}${whereClause}
|
|
1730
|
+
) distinct_tuple
|
|
1731
|
+
`)
|
|
1732
|
+
.get(...params);
|
|
1733
|
+
return row?.count ?? 0;
|
|
1734
|
+
}
|
|
1735
|
+
buildOrderClause(options) {
|
|
1736
|
+
if (!options?.order || options.order.length === 0) {
|
|
1737
|
+
return { joins: '', orderBy: '' };
|
|
1738
|
+
}
|
|
1739
|
+
const columns = options.order.map((termName) => TERM_COLUMN[termName]);
|
|
1740
|
+
if (columns.some((column) => !column)) {
|
|
1741
|
+
throw new Error(`Unsupported RDF quad order fields: ${options.order.join(', ')}`);
|
|
1742
|
+
}
|
|
1743
|
+
const joins = options.order.map((termName, index) => {
|
|
1744
|
+
const column = TERM_COLUMN[termName];
|
|
1745
|
+
const direction = options.orderDirections?.[index] ?? (options.reverse ? 'desc' : 'asc');
|
|
1746
|
+
return {
|
|
1747
|
+
join: ` JOIN rdf_terms order_t${index} ON order_t${index}.id = rdf_quads.${column}`,
|
|
1748
|
+
order: `order_t${index}.value${direction === 'desc' ? ' DESC' : ''}`,
|
|
1749
|
+
};
|
|
1750
|
+
});
|
|
1751
|
+
return {
|
|
1752
|
+
joins: joins.map((join) => join.join).join(''),
|
|
1753
|
+
orderBy: ` ORDER BY ${joins.map((join) => join.order).join(', ')}`,
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
paginationParamCount(options) {
|
|
1757
|
+
return (options?.limit !== undefined ? 1 : 0) + (options?.offset !== undefined ? 1 : 0);
|
|
1758
|
+
}
|
|
1759
|
+
rowsToQuads(rows) {
|
|
1760
|
+
const dictionary = this.requireDictionary();
|
|
1761
|
+
const termMap = dictionary.rowsForIds(rows.flatMap((row) => [
|
|
1762
|
+
row.graph_id,
|
|
1763
|
+
row.subject_id,
|
|
1764
|
+
row.predicate_id,
|
|
1765
|
+
row.object_id,
|
|
1766
|
+
]));
|
|
1767
|
+
return rows.map((row) => n3_1.DataFactory.quad(this.requiredTerm(termMap, row.subject_id), this.requiredTerm(termMap, row.predicate_id), this.requiredTerm(termMap, row.object_id), this.requiredTerm(termMap, row.graph_id)));
|
|
1768
|
+
}
|
|
1769
|
+
requiredTerm(termMap, id) {
|
|
1770
|
+
const term = termMap.get(id);
|
|
1771
|
+
if (!term) {
|
|
1772
|
+
throw new Error(`RDF term not found while reading quad index: ${id}`);
|
|
1773
|
+
}
|
|
1774
|
+
return term;
|
|
1775
|
+
}
|
|
1776
|
+
metrics(indexChoice, matchedRows, returnedRows, start, queryPlan) {
|
|
1777
|
+
return {
|
|
1778
|
+
engine: 'solid-rdf',
|
|
1779
|
+
indexChoice,
|
|
1780
|
+
matchedRows,
|
|
1781
|
+
returnedRows,
|
|
1782
|
+
durationMs: Date.now() - start,
|
|
1783
|
+
queryPlan,
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
requireDb() {
|
|
1787
|
+
if (!this.db) {
|
|
1788
|
+
throw new Error('RdfQuadIndex is not open');
|
|
1789
|
+
}
|
|
1790
|
+
return this.db;
|
|
1791
|
+
}
|
|
1792
|
+
requireDictionary() {
|
|
1793
|
+
if (!this.dictionary) {
|
|
1794
|
+
throw new Error('RdfQuadIndex is not open');
|
|
1795
|
+
}
|
|
1796
|
+
return this.dictionary;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
exports.RdfQuadIndex = RdfQuadIndex;
|
|
1800
|
+
function sumSpaceObjects(objects, kind) {
|
|
1801
|
+
return objects
|
|
1802
|
+
.filter((object) => object.kind === kind)
|
|
1803
|
+
.reduce((sum, object) => sum + object.bytes, 0);
|
|
1804
|
+
}
|
|
1805
|
+
function rdfSpaceObjectKind(name, schemaType, tableName) {
|
|
1806
|
+
if (schemaType === 'table' && name.startsWith('rdf_')) {
|
|
1807
|
+
return 'table';
|
|
1808
|
+
}
|
|
1809
|
+
if (schemaType === 'index' && (name.startsWith('rdf_') || tableName?.startsWith('rdf_'))) {
|
|
1810
|
+
return 'index';
|
|
1811
|
+
}
|
|
1812
|
+
if (name.startsWith('sqlite_')) {
|
|
1813
|
+
return 'internal';
|
|
1814
|
+
}
|
|
1815
|
+
return 'unknown';
|
|
1816
|
+
}
|
|
1817
|
+
function rdfTermKind(term) {
|
|
1818
|
+
switch (term.termType) {
|
|
1819
|
+
case 'NamedNode':
|
|
1820
|
+
return 'iri';
|
|
1821
|
+
case 'BlankNode':
|
|
1822
|
+
return 'blank';
|
|
1823
|
+
case 'Literal':
|
|
1824
|
+
return 'literal';
|
|
1825
|
+
case 'DefaultGraph':
|
|
1826
|
+
return 'default_graph';
|
|
1827
|
+
default:
|
|
1828
|
+
return 'iri';
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
function escapeLikePattern(value) {
|
|
1832
|
+
return value.replace(/[\\%_]/g, (match) => `\\${match}`);
|
|
1833
|
+
}
|
|
1834
|
+
function uniqueNumbers(values) {
|
|
1835
|
+
return [...new Set(values)];
|
|
1836
|
+
}
|
|
1837
|
+
function uniquePatternKeys(values) {
|
|
1838
|
+
return TERM_KEYS.filter((key) => values.includes(key));
|
|
1839
|
+
}
|
|
1840
|
+
//# sourceMappingURL=RdfQuadIndex.js.map
|