@undefineds.co/xpod 0.3.25 → 0.3.27
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/cloud.json +6 -5
- package/config/main.json +0 -3
- package/config/xpod.base.json +0 -32
- package/dist/api/container/index.js +3 -0
- package/dist/api/container/index.js.map +1 -1
- package/dist/api/container/routes.js +2 -0
- package/dist/api/container/routes.js.map +1 -1
- package/dist/api/container/types.d.ts +5 -0
- package/dist/api/container/types.js.map +1 -1
- package/dist/authorization/AuthMode.d.ts +8 -0
- package/dist/authorization/AuthMode.js +51 -0
- package/dist/authorization/AuthMode.js.map +1 -0
- package/dist/authorization/PodAuthorizationResources.d.ts +18 -0
- package/dist/authorization/PodAuthorizationResources.js +108 -0
- package/dist/authorization/PodAuthorizationResources.js.map +1 -0
- package/dist/cli/commands/start.js +11 -2
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/components/components.jsonld +3 -2
- package/dist/components/context.jsonld +115 -14
- package/dist/index.d.ts +5 -3
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/main.js +11 -2
- package/dist/main.js.map +1 -1
- package/dist/provision/LocalPodProvisioningService.d.ts +6 -2
- package/dist/provision/LocalPodProvisioningService.js +36 -33
- package/dist/provision/LocalPodProvisioningService.js.map +1 -1
- package/dist/provision/LocalPodProvisioningService.jsonld +65 -8
- package/dist/runtime/XpodRuntime.js +0 -1
- package/dist/runtime/XpodRuntime.js.map +1 -1
- package/dist/runtime/bootstrap.d.ts +4 -2
- package/dist/runtime/bootstrap.js +43 -11
- package/dist/runtime/bootstrap.js.map +1 -1
- package/dist/runtime/css-process.d.ts +6 -1
- package/dist/runtime/css-process.js +18 -6
- package/dist/runtime/css-process.js.map +1 -1
- package/dist/runtime/lifecycle.d.ts +2 -3
- package/dist/runtime/lifecycle.js +2 -2
- package/dist/runtime/lifecycle.js.map +1 -1
- package/dist/runtime/runtime-types.d.ts +2 -1
- package/dist/runtime/runtime-types.js.map +1 -1
- package/dist/storage/accessors/SolidRdfDataAccessor.d.ts +2 -3
- package/dist/storage/accessors/SolidRdfDataAccessor.js +48 -42
- package/dist/storage/accessors/SolidRdfDataAccessor.js.map +1 -1
- package/dist/storage/accessors/SolidRdfDataAccessor.jsonld +1 -1
- package/dist/storage/keyvalue/BaseKeyValueStorage.d.ts +33 -0
- package/dist/storage/keyvalue/BaseKeyValueStorage.js +106 -0
- package/dist/storage/keyvalue/BaseKeyValueStorage.js.map +1 -0
- package/dist/storage/keyvalue/BaseKeyValueStorage.jsonld +177 -0
- package/dist/storage/keyvalue/PostgresKeyValueStorage.d.ts +9 -18
- package/dist/storage/keyvalue/PostgresKeyValueStorage.js +24 -96
- package/dist/storage/keyvalue/PostgresKeyValueStorage.js.map +1 -1
- package/dist/storage/keyvalue/PostgresKeyValueStorage.jsonld +15 -58
- package/dist/storage/keyvalue/SqliteKeyValueStorage.d.ts +9 -15
- package/dist/storage/keyvalue/SqliteKeyValueStorage.js +36 -104
- package/dist/storage/keyvalue/SqliteKeyValueStorage.js.map +1 -1
- package/dist/storage/keyvalue/SqliteKeyValueStorage.jsonld +21 -52
- package/dist/storage/quint/BaseQuintStore.d.ts +4 -1
- package/dist/storage/quint/BaseQuintStore.js +41 -52
- package/dist/storage/quint/BaseQuintStore.js.map +1 -1
- package/dist/storage/quint/PgQuintStore.d.ts +4 -3
- package/dist/storage/quint/PgQuintStore.js.map +1 -1
- package/dist/storage/quint/SqliteQuintStore.d.ts +43 -54
- package/dist/storage/quint/SqliteQuintStore.js +197 -520
- package/dist/storage/quint/SqliteQuintStore.js.map +1 -1
- package/dist/storage/quint/SqliteQuintStore.jsonld +38 -86
- package/dist/storage/rdf/PostgresRdfEngine.d.ts +118 -0
- package/dist/storage/rdf/PostgresRdfEngine.js +2609 -0
- package/dist/storage/rdf/PostgresRdfEngine.js.map +1 -0
- package/dist/storage/rdf/PostgresRdfEngine.jsonld +657 -0
- package/dist/storage/rdf/SolidRdfEngine.d.ts +2 -2
- package/dist/storage/rdf/SolidRdfEngine.js.map +1 -1
- package/dist/storage/rdf/SolidRdfEngine.jsonld +3 -0
- package/dist/storage/rdf/SolidRdfSparqlEngine.d.ts +3 -3
- package/dist/storage/rdf/SolidRdfSparqlEngine.js +20 -20
- package/dist/storage/rdf/SolidRdfSparqlEngine.js.map +1 -1
- package/dist/storage/rdf/SolidRdfSparqlEngine.jsonld +1 -1
- package/dist/storage/rdf/index.d.ts +2 -1
- package/dist/storage/rdf/index.js +3 -1
- package/dist/storage/rdf/index.js.map +1 -1
- package/dist/storage/rdf/types.d.ts +19 -0
- package/dist/storage/rdf/types.js.map +1 -1
- package/dist/storage/rdf/types.jsonld +115 -0
- package/package.json +2 -2
- package/config/runtime-open.json +0 -22
- package/dist/authorization/AuthModeSelector.d.ts +0 -10
- package/dist/authorization/AuthModeSelector.js +0 -27
- package/dist/authorization/AuthModeSelector.js.map +0 -1
- package/dist/authorization/AuthModeSelector.jsonld +0 -81
|
@@ -0,0 +1,2609 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PostgresRdfEngine = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const promises_1 = require("node:fs/promises");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const node_os_1 = require("node:os");
|
|
9
|
+
const n3_1 = require("n3");
|
|
10
|
+
const pglite_1 = require("@electric-sql/pglite");
|
|
11
|
+
const PostgresPoolManager_1 = require("../database/PostgresPoolManager");
|
|
12
|
+
const RdfTermDictionary_1 = require("./RdfTermDictionary");
|
|
13
|
+
const RdfTermSemantics_1 = require("./RdfTermSemantics");
|
|
14
|
+
const SolidRdfEngine_1 = require("./SolidRdfEngine");
|
|
15
|
+
const Rdf3xSchema_1 = require("./Rdf3xSchema");
|
|
16
|
+
const types_1 = require("../quint/types");
|
|
17
|
+
const { namedNode, quad } = n3_1.DataFactory;
|
|
18
|
+
const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
|
|
19
|
+
const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
|
|
20
|
+
const XSD_DECIMAL = 'http://www.w3.org/2001/XMLSchema#decimal';
|
|
21
|
+
const RDF_LANG_STRING = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString';
|
|
22
|
+
const POSTGRES_RDF_SCHEMA_VERSION = 1;
|
|
23
|
+
const POSTGRES_RDF3X_SCHEMA_VERSION = 1;
|
|
24
|
+
const PG_STRING_ESCAPE = '\u001f';
|
|
25
|
+
const TERM_KEYS = ['subject', 'predicate', 'object'];
|
|
26
|
+
const PATTERN_KEYS = ['graph', 'subject', 'predicate', 'object'];
|
|
27
|
+
const RDF_FACTS_TABLE = 'rdf_quads';
|
|
28
|
+
const TERM_COLUMN = {
|
|
29
|
+
graph: 'graph_id',
|
|
30
|
+
subject: 'subject_id',
|
|
31
|
+
predicate: 'predicate_id',
|
|
32
|
+
object: 'object_id',
|
|
33
|
+
};
|
|
34
|
+
const PERMUTATIONS = [
|
|
35
|
+
{ name: 'SPO', indexName: 'rdf_quads_spog', columns: ['subject_id', 'predicate_id', 'object_id', 'graph_id'] },
|
|
36
|
+
{ name: 'SOP', indexName: 'rdf_quads_sopg', columns: ['subject_id', 'object_id', 'predicate_id', 'graph_id'] },
|
|
37
|
+
{ name: 'PSO', indexName: 'rdf_quads_psog', columns: ['predicate_id', 'subject_id', 'object_id', 'graph_id'] },
|
|
38
|
+
{ name: 'POS', indexName: 'rdf_quads_posg', columns: ['predicate_id', 'object_id', 'subject_id', 'graph_id'] },
|
|
39
|
+
{ name: 'OSP', indexName: 'rdf_quads_ospg', columns: ['object_id', 'subject_id', 'predicate_id', 'graph_id'] },
|
|
40
|
+
{ name: 'OPS', indexName: 'rdf_quads_opsg', columns: ['object_id', 'predicate_id', 'subject_id', 'graph_id'] },
|
|
41
|
+
];
|
|
42
|
+
const PAIR_PROJECTIONS = [
|
|
43
|
+
{ name: 'SP', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.SP, columns: ['subject_id', 'predicate_id'], remainder: 'object_id' },
|
|
44
|
+
{ name: 'SO', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.SO, columns: ['subject_id', 'object_id'], remainder: 'predicate_id' },
|
|
45
|
+
{ name: 'PS', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.PS, columns: ['predicate_id', 'subject_id'], remainder: 'object_id' },
|
|
46
|
+
{ name: 'PO', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.PO, columns: ['predicate_id', 'object_id'], remainder: 'subject_id' },
|
|
47
|
+
{ name: 'OS', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.OS, columns: ['object_id', 'subject_id'], remainder: 'predicate_id' },
|
|
48
|
+
{ name: 'OP', table: Rdf3xSchema_1.RDF3X_PAIR_PROJECTION_TABLE_BY_NAME.OP, columns: ['object_id', 'predicate_id'], remainder: 'subject_id' },
|
|
49
|
+
];
|
|
50
|
+
const TERM_PROJECTIONS = [
|
|
51
|
+
{ name: 'S', table: Rdf3xSchema_1.RDF3X_TERM_PROJECTION_TABLE_BY_NAME.S, column: 'subject_id' },
|
|
52
|
+
{ name: 'P', table: Rdf3xSchema_1.RDF3X_TERM_PROJECTION_TABLE_BY_NAME.P, column: 'predicate_id' },
|
|
53
|
+
{ name: 'O', table: Rdf3xSchema_1.RDF3X_TERM_PROJECTION_TABLE_BY_NAME.O, column: 'object_id' },
|
|
54
|
+
];
|
|
55
|
+
const OBJECT_RANGE_KINDS = ['iri', 'literal', 'blank'];
|
|
56
|
+
function toPgSafe(value) {
|
|
57
|
+
return value
|
|
58
|
+
.replaceAll(PG_STRING_ESCAPE, `${PG_STRING_ESCAPE}${PG_STRING_ESCAPE}`)
|
|
59
|
+
.replaceAll('\u0000', `${PG_STRING_ESCAPE}0`);
|
|
60
|
+
}
|
|
61
|
+
function fromPgSafe(value) {
|
|
62
|
+
let result = '';
|
|
63
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
64
|
+
const char = value[i];
|
|
65
|
+
if (char !== PG_STRING_ESCAPE) {
|
|
66
|
+
result += char;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const next = value[i + 1];
|
|
70
|
+
if (next === '0') {
|
|
71
|
+
result += '\u0000';
|
|
72
|
+
i += 1;
|
|
73
|
+
}
|
|
74
|
+
else if (next === PG_STRING_ESCAPE) {
|
|
75
|
+
result += PG_STRING_ESCAPE;
|
|
76
|
+
i += 1;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Legacy rows from the earlier direct replacement format used a bare
|
|
80
|
+
// separator as a null-byte placeholder.
|
|
81
|
+
result += '\u0000';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function normalizePgValue(value) {
|
|
87
|
+
if (typeof value === 'string') {
|
|
88
|
+
return toPgSafe(value);
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
return value.map((item) => normalizePgValue(item));
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
class PgliteExecutor {
|
|
96
|
+
constructor(db) {
|
|
97
|
+
this.db = db;
|
|
98
|
+
}
|
|
99
|
+
async query(sql, params = []) {
|
|
100
|
+
const result = await this.db.query(sql, params.map((value) => normalizePgValue(value)));
|
|
101
|
+
return result.rows.map((row) => restoreRow(row));
|
|
102
|
+
}
|
|
103
|
+
async exec(sql, params = []) {
|
|
104
|
+
await this.db.query(sql, params.map((value) => normalizePgValue(value)));
|
|
105
|
+
}
|
|
106
|
+
async transaction(fn) {
|
|
107
|
+
await this.db.query('BEGIN');
|
|
108
|
+
try {
|
|
109
|
+
const result = await fn(this);
|
|
110
|
+
await this.db.query('COMMIT');
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
await this.db.query('ROLLBACK');
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async close() {
|
|
119
|
+
await this.db.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
class PgPoolExecutor {
|
|
123
|
+
constructor(pool, client) {
|
|
124
|
+
this.pool = pool;
|
|
125
|
+
this.client = client;
|
|
126
|
+
}
|
|
127
|
+
async query(sql, params = []) {
|
|
128
|
+
const result = this.client
|
|
129
|
+
? await this.client.query(sql, params.map((value) => normalizePgValue(value)))
|
|
130
|
+
: await this.pool.query(sql, params.map((value) => normalizePgValue(value)));
|
|
131
|
+
return result.rows.map((row) => restoreRow(row));
|
|
132
|
+
}
|
|
133
|
+
async exec(sql, params = []) {
|
|
134
|
+
if (this.client) {
|
|
135
|
+
await this.client.query(sql, params.map((value) => normalizePgValue(value)));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
await this.pool.query(sql, params.map((value) => normalizePgValue(value)));
|
|
139
|
+
}
|
|
140
|
+
async transaction(fn) {
|
|
141
|
+
if (this.client) {
|
|
142
|
+
const result = await fn(this);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
const client = await this.pool.connect();
|
|
146
|
+
const tx = new PgPoolExecutor(this.pool, client);
|
|
147
|
+
try {
|
|
148
|
+
await client.query('BEGIN');
|
|
149
|
+
const result = await fn(tx);
|
|
150
|
+
await client.query('COMMIT');
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
try {
|
|
155
|
+
await client.query('ROLLBACK');
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
client.release();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async close() {
|
|
166
|
+
if (this.client) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await this.pool.end();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
class PgSqlBuilder {
|
|
173
|
+
constructor(initialParams = []) {
|
|
174
|
+
this.params = [];
|
|
175
|
+
this.params = [...initialParams];
|
|
176
|
+
}
|
|
177
|
+
add(value) {
|
|
178
|
+
this.params.push(value);
|
|
179
|
+
return `$${this.params.length}`;
|
|
180
|
+
}
|
|
181
|
+
addAll(values) {
|
|
182
|
+
return values.map((value) => this.add(value)).join(', ');
|
|
183
|
+
}
|
|
184
|
+
snapshot() {
|
|
185
|
+
return [...this.params];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
class PostgresRdfTermDictionary {
|
|
189
|
+
constructor(executor) {
|
|
190
|
+
this.executor = executor;
|
|
191
|
+
this.termCache = new Map();
|
|
192
|
+
this.idCache = new Map();
|
|
193
|
+
}
|
|
194
|
+
async initialize() {
|
|
195
|
+
await this.executor.exec(`
|
|
196
|
+
CREATE TABLE IF NOT EXISTS rdf_terms (
|
|
197
|
+
id BIGSERIAL PRIMARY KEY,
|
|
198
|
+
kind TEXT NOT NULL,
|
|
199
|
+
value TEXT NOT NULL,
|
|
200
|
+
value_head TEXT NOT NULL,
|
|
201
|
+
datatype_id BIGINT,
|
|
202
|
+
lang TEXT,
|
|
203
|
+
hash TEXT NOT NULL UNIQUE,
|
|
204
|
+
normalized_text TEXT,
|
|
205
|
+
numeric_value DOUBLE PRECISION,
|
|
206
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
207
|
+
)
|
|
208
|
+
`);
|
|
209
|
+
await this.executor.exec(`
|
|
210
|
+
CREATE UNIQUE INDEX IF NOT EXISTS rdf_terms_identity_hash ON rdf_terms (hash)
|
|
211
|
+
`);
|
|
212
|
+
await this.executor.exec('CREATE INDEX IF NOT EXISTS rdf_terms_kind_value_head ON rdf_terms (kind, value_head)');
|
|
213
|
+
await this.executor.exec('CREATE INDEX IF NOT EXISTS rdf_terms_kind_datatype ON rdf_terms (kind, datatype_id)');
|
|
214
|
+
await this.executor.exec('CREATE INDEX IF NOT EXISTS rdf_terms_kind_lang ON rdf_terms (kind, lang)');
|
|
215
|
+
await this.executor.exec('CREATE INDEX IF NOT EXISTS rdf_terms_kind_numeric_value ON rdf_terms (kind, numeric_value)');
|
|
216
|
+
}
|
|
217
|
+
async getOrCreate(term) {
|
|
218
|
+
const identity = await this.toIdentity(term);
|
|
219
|
+
const cacheKey = this.identityCacheKey(identity);
|
|
220
|
+
const cached = this.termCache.get(cacheKey);
|
|
221
|
+
if (cached !== undefined) {
|
|
222
|
+
return cached;
|
|
223
|
+
}
|
|
224
|
+
const row = await this.executor.query(`
|
|
225
|
+
INSERT INTO rdf_terms (
|
|
226
|
+
kind,
|
|
227
|
+
value,
|
|
228
|
+
value_head,
|
|
229
|
+
datatype_id,
|
|
230
|
+
lang,
|
|
231
|
+
hash,
|
|
232
|
+
normalized_text,
|
|
233
|
+
numeric_value
|
|
234
|
+
)
|
|
235
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
236
|
+
ON CONFLICT (hash) DO UPDATE
|
|
237
|
+
SET hash = EXCLUDED.hash
|
|
238
|
+
RETURNING id
|
|
239
|
+
`, [
|
|
240
|
+
identity.kind,
|
|
241
|
+
identity.value,
|
|
242
|
+
identity.valueHead,
|
|
243
|
+
identity.datatypeId,
|
|
244
|
+
identity.lang,
|
|
245
|
+
identity.hash,
|
|
246
|
+
identity.normalizedText,
|
|
247
|
+
identity.numericValue,
|
|
248
|
+
]);
|
|
249
|
+
const id = row[0]?.id;
|
|
250
|
+
if (id === undefined) {
|
|
251
|
+
throw new Error('Failed to insert RDF term');
|
|
252
|
+
}
|
|
253
|
+
this.termCache.set(cacheKey, id);
|
|
254
|
+
this.idCache.set(id, term);
|
|
255
|
+
return id;
|
|
256
|
+
}
|
|
257
|
+
async find(term) {
|
|
258
|
+
const identity = await this.toIdentity(term);
|
|
259
|
+
const cacheKey = this.identityCacheKey(identity);
|
|
260
|
+
const cached = this.termCache.get(cacheKey);
|
|
261
|
+
if (cached !== undefined) {
|
|
262
|
+
return cached;
|
|
263
|
+
}
|
|
264
|
+
const rows = await this.executor.query('SELECT * FROM rdf_terms WHERE hash = $1', [identity.hash]);
|
|
265
|
+
const id = rows.find((row) => this.rowMatchesIdentity(row, identity))?.id;
|
|
266
|
+
if (id !== undefined) {
|
|
267
|
+
this.termCache.set(cacheKey, id);
|
|
268
|
+
}
|
|
269
|
+
return id;
|
|
270
|
+
}
|
|
271
|
+
async termForId(id) {
|
|
272
|
+
const cached = this.idCache.get(id);
|
|
273
|
+
if (cached) {
|
|
274
|
+
return cached;
|
|
275
|
+
}
|
|
276
|
+
const row = await this.executor.query('SELECT * FROM rdf_terms WHERE id = $1', [id]);
|
|
277
|
+
const termRow = row[0];
|
|
278
|
+
if (!termRow) {
|
|
279
|
+
throw new Error(`RDF term not found: ${id}`);
|
|
280
|
+
}
|
|
281
|
+
const term = await this.rowToTerm(termRow);
|
|
282
|
+
this.idCache.set(id, term);
|
|
283
|
+
return term;
|
|
284
|
+
}
|
|
285
|
+
async rowsForIds(ids) {
|
|
286
|
+
const result = new Map();
|
|
287
|
+
const uniqueIds = Array.from(new Set(ids));
|
|
288
|
+
if (uniqueIds.length === 0) {
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
const rows = await this.executor.query('SELECT * FROM rdf_terms WHERE id = ANY($1::bigint[])', [uniqueIds]);
|
|
292
|
+
for (const row of rows) {
|
|
293
|
+
const term = await this.rowToTerm(row);
|
|
294
|
+
this.idCache.set(row.id, term);
|
|
295
|
+
result.set(row.id, term);
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
async rowsByNormalizedTextRegex(kinds, pattern) {
|
|
300
|
+
if (kinds.length === 0) {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
const rows = await this.executor.query(`
|
|
304
|
+
SELECT id, value
|
|
305
|
+
FROM rdf_terms
|
|
306
|
+
WHERE kind = ANY($1::text[])
|
|
307
|
+
AND normalized_text IS NOT NULL
|
|
308
|
+
`, [kinds]);
|
|
309
|
+
const regex = new RegExp(pattern);
|
|
310
|
+
return rows
|
|
311
|
+
.filter((row) => regex.test(row.value.toLowerCase()))
|
|
312
|
+
.map((row) => row.id);
|
|
313
|
+
}
|
|
314
|
+
async count() {
|
|
315
|
+
const row = await this.executor.query('SELECT COUNT(*) AS count FROM rdf_terms');
|
|
316
|
+
return row[0]?.count ?? 0;
|
|
317
|
+
}
|
|
318
|
+
async toIdentity(term) {
|
|
319
|
+
switch (term.termType) {
|
|
320
|
+
case 'NamedNode':
|
|
321
|
+
return this.identity('iri', term.value, null, null, term.value, null);
|
|
322
|
+
case 'BlankNode':
|
|
323
|
+
return this.identity('blank', term.value, null, null, term.value, null);
|
|
324
|
+
case 'DefaultGraph':
|
|
325
|
+
return this.identity('default_graph', '', null, null, null, null);
|
|
326
|
+
case 'Literal': {
|
|
327
|
+
const datatypeValue = term.datatype?.value || XSD_STRING;
|
|
328
|
+
const datatypeId = datatypeValue === XSD_STRING && !term.language
|
|
329
|
+
? null
|
|
330
|
+
: await this.getOrCreate(namedNode(datatypeValue));
|
|
331
|
+
return this.identity('literal', term.value, datatypeId, term.language || null, term.value, this.numericValueForLiteral(term.value, datatypeValue));
|
|
332
|
+
}
|
|
333
|
+
case 'Variable':
|
|
334
|
+
throw new Error(`Variables cannot be indexed as RDF terms: ${term.value}`);
|
|
335
|
+
case 'Quad':
|
|
336
|
+
throw new Error('Nested RDF-star quads are not supported by the first PostgresRdfEngine index');
|
|
337
|
+
default: {
|
|
338
|
+
const exhaustive = term;
|
|
339
|
+
throw new Error(`Unsupported RDF term: ${String(exhaustive)}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
identity(kind, value, datatypeId, lang, normalizedText, numericValue) {
|
|
344
|
+
const hash = (0, node_crypto_1.createHash)('sha256')
|
|
345
|
+
.update(kind)
|
|
346
|
+
.update('\0')
|
|
347
|
+
.update(value)
|
|
348
|
+
.update('\0')
|
|
349
|
+
.update(String(datatypeId ?? ''))
|
|
350
|
+
.update('\0')
|
|
351
|
+
.update(lang ?? '')
|
|
352
|
+
.digest('hex');
|
|
353
|
+
return {
|
|
354
|
+
kind,
|
|
355
|
+
value,
|
|
356
|
+
valueHead: (0, RdfTermDictionary_1.rdfTermValueHead)(value),
|
|
357
|
+
datatypeId,
|
|
358
|
+
lang,
|
|
359
|
+
normalizedText: normalizedText ? normalizedText.toLowerCase() : null,
|
|
360
|
+
numericValue,
|
|
361
|
+
hash,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
numericValueForLiteral(value, datatypeValue) {
|
|
365
|
+
if (!(0, RdfTermSemantics_1.isRdfNumericDatatype)(datatypeValue)) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const numeric = (0, RdfTermSemantics_1.rdfNumericValue)(value);
|
|
369
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
370
|
+
}
|
|
371
|
+
identityCacheKey(identity) {
|
|
372
|
+
return [
|
|
373
|
+
identity.kind,
|
|
374
|
+
identity.value,
|
|
375
|
+
identity.datatypeId ?? '',
|
|
376
|
+
identity.lang ?? '',
|
|
377
|
+
].join('\u001f');
|
|
378
|
+
}
|
|
379
|
+
rowMatchesIdentity(row, identity) {
|
|
380
|
+
return row.kind === identity.kind
|
|
381
|
+
&& row.value === identity.value
|
|
382
|
+
&& row.datatype_id === identity.datatypeId
|
|
383
|
+
&& row.lang === identity.lang;
|
|
384
|
+
}
|
|
385
|
+
async rowToTerm(row) {
|
|
386
|
+
switch (row.kind) {
|
|
387
|
+
case 'iri':
|
|
388
|
+
return n3_1.DataFactory.namedNode(row.value);
|
|
389
|
+
case 'blank':
|
|
390
|
+
return n3_1.DataFactory.blankNode(row.value);
|
|
391
|
+
case 'default_graph':
|
|
392
|
+
return n3_1.DataFactory.defaultGraph();
|
|
393
|
+
case 'literal': {
|
|
394
|
+
const value = row.value;
|
|
395
|
+
if (row.lang) {
|
|
396
|
+
return n3_1.DataFactory.literal(value, row.lang);
|
|
397
|
+
}
|
|
398
|
+
if (row.datatype_id) {
|
|
399
|
+
const datatype = await this.termForId(row.datatype_id);
|
|
400
|
+
if (datatype.termType === 'NamedNode' && datatype.value !== XSD_STRING && datatype.value !== RDF_LANG_STRING) {
|
|
401
|
+
return n3_1.DataFactory.literal(value, datatype);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return n3_1.DataFactory.literal(value);
|
|
405
|
+
}
|
|
406
|
+
default:
|
|
407
|
+
throw new Error(`Unsupported RDF term kind in row: ${row.kind}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
class PostgresRdfEngine {
|
|
412
|
+
constructor(options) {
|
|
413
|
+
this.executor = null;
|
|
414
|
+
this.termDictionary = null;
|
|
415
|
+
this.cacheEngine = null;
|
|
416
|
+
this.cacheDir = null;
|
|
417
|
+
this.cachePath = null;
|
|
418
|
+
this.factsDataVersion = 0;
|
|
419
|
+
this.cacheDirty = true;
|
|
420
|
+
this.initialized = false;
|
|
421
|
+
this.initializing = null;
|
|
422
|
+
this.sharedPoolConfig = null;
|
|
423
|
+
this.pglite = null;
|
|
424
|
+
this.pgPool = null;
|
|
425
|
+
this.rdf3xDataVersion = 0;
|
|
426
|
+
this.rdf3xDirty = true;
|
|
427
|
+
this.pgOptions = {
|
|
428
|
+
...options,
|
|
429
|
+
driver: options.driver ?? (options.connectionString || options.pool ? 'pg' : 'pglite'),
|
|
430
|
+
};
|
|
431
|
+
if (options.autoOpen) {
|
|
432
|
+
void this.open();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async open() {
|
|
436
|
+
if (this.initialized) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
this.initializing ??= Promise.resolve()
|
|
440
|
+
.then(async () => {
|
|
441
|
+
await this.openExecutor();
|
|
442
|
+
this.termDictionary = new PostgresRdfTermDictionary(this.requireExecutor());
|
|
443
|
+
await this.termDictionary.initialize();
|
|
444
|
+
await this.initializeSchema();
|
|
445
|
+
this.factsDataVersion = await this.readFactsDataVersion();
|
|
446
|
+
this.rdf3xDataVersion = await this.readRdf3xFactsDataVersion();
|
|
447
|
+
this.rdf3xDirty = this.rdf3xDataVersion !== this.factsDataVersion;
|
|
448
|
+
this.initialized = true;
|
|
449
|
+
})
|
|
450
|
+
.finally(() => {
|
|
451
|
+
this.initializing = null;
|
|
452
|
+
});
|
|
453
|
+
await this.initializing;
|
|
454
|
+
}
|
|
455
|
+
async close() {
|
|
456
|
+
if (this.initializing) {
|
|
457
|
+
await this.initializing.catch(() => { });
|
|
458
|
+
}
|
|
459
|
+
if (this.cacheEngine) {
|
|
460
|
+
await this.cacheEngine.close();
|
|
461
|
+
this.cacheEngine = null;
|
|
462
|
+
}
|
|
463
|
+
if (this.cacheDir) {
|
|
464
|
+
await (0, promises_1.rm)(this.cacheDir, { recursive: true, force: true });
|
|
465
|
+
this.cacheDir = null;
|
|
466
|
+
this.cachePath = null;
|
|
467
|
+
}
|
|
468
|
+
this.executor = null;
|
|
469
|
+
if (this.pglite) {
|
|
470
|
+
await this.pglite.close();
|
|
471
|
+
this.pglite = null;
|
|
472
|
+
}
|
|
473
|
+
if (this.pgPool) {
|
|
474
|
+
if (this.sharedPoolConfig) {
|
|
475
|
+
(0, PostgresPoolManager_1.releaseSharedPool)(this.sharedPoolConfig);
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
await this.pgPool.end();
|
|
479
|
+
}
|
|
480
|
+
this.pgPool = null;
|
|
481
|
+
this.sharedPoolConfig = null;
|
|
482
|
+
}
|
|
483
|
+
this.termDictionary = null;
|
|
484
|
+
this.initialized = false;
|
|
485
|
+
this.cacheDirty = true;
|
|
486
|
+
this.factsDataVersion = 0;
|
|
487
|
+
this.rdf3xDataVersion = 0;
|
|
488
|
+
this.rdf3xDirty = true;
|
|
489
|
+
}
|
|
490
|
+
async put(quads, options) {
|
|
491
|
+
await this.ensureReady();
|
|
492
|
+
const quadList = Array.isArray(quads) ? quads : [quads];
|
|
493
|
+
const executor = this.requireExecutor();
|
|
494
|
+
try {
|
|
495
|
+
await executor.transaction(async (tx) => {
|
|
496
|
+
const scopedDictionary = new PostgresRdfTermDictionary(tx);
|
|
497
|
+
await scopedDictionary.initialize();
|
|
498
|
+
const sourceId = options?.source ? await this.upsertSource(options.source, tx) : null;
|
|
499
|
+
await this.insertQuads(tx, scopedDictionary, quadList, sourceId, options?.sourceLineNo ?? null);
|
|
500
|
+
await this.bumpFactsDataVersion(tx);
|
|
501
|
+
});
|
|
502
|
+
this.factsDataVersion = await this.readFactsDataVersion();
|
|
503
|
+
this.markDerivedDirty();
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
this.cacheDirty = true;
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async replaceSource(quads, source) {
|
|
511
|
+
await this.ensureReady();
|
|
512
|
+
const executor = this.requireExecutor();
|
|
513
|
+
try {
|
|
514
|
+
await executor.transaction(async (tx) => {
|
|
515
|
+
const scopedDictionary = new PostgresRdfTermDictionary(tx);
|
|
516
|
+
await scopedDictionary.initialize();
|
|
517
|
+
const sourceId = await this.upsertSource(source, tx);
|
|
518
|
+
await tx.exec('DELETE FROM rdf_quads WHERE source_file_id = $1', [sourceId]);
|
|
519
|
+
await this.insertQuads(tx, scopedDictionary, quads, sourceId, null);
|
|
520
|
+
await this.bumpFactsDataVersion(tx);
|
|
521
|
+
});
|
|
522
|
+
this.factsDataVersion = await this.readFactsDataVersion();
|
|
523
|
+
this.markDerivedDirty();
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
this.cacheDirty = true;
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async deleteSource(source) {
|
|
531
|
+
await this.ensureReady();
|
|
532
|
+
const executor = this.requireExecutor();
|
|
533
|
+
try {
|
|
534
|
+
const sourceRow = await this.findSourceRow(source, executor);
|
|
535
|
+
if (!sourceRow) {
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
const result = await executor.transaction(async (tx) => {
|
|
539
|
+
const deleteResult = await tx.query('DELETE FROM rdf_quads WHERE source_file_id = $1 RETURNING 1', [sourceRow.id]);
|
|
540
|
+
await tx.exec('DELETE FROM rdf_sources WHERE id = $1', [sourceRow.id]);
|
|
541
|
+
await this.bumpFactsDataVersion(tx);
|
|
542
|
+
return deleteResult.length;
|
|
543
|
+
});
|
|
544
|
+
this.factsDataVersion = await this.readFactsDataVersion();
|
|
545
|
+
this.markDerivedDirty();
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
this.cacheDirty = true;
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async delete(pattern) {
|
|
554
|
+
await this.ensureReady();
|
|
555
|
+
const scan = await this.scan({ pattern });
|
|
556
|
+
if (scan.quads.length === 0) {
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
const executor = this.requireExecutor();
|
|
560
|
+
try {
|
|
561
|
+
await executor.transaction(async (tx) => {
|
|
562
|
+
const scopedDictionary = new PostgresRdfTermDictionary(tx);
|
|
563
|
+
await scopedDictionary.initialize();
|
|
564
|
+
for (const value of scan.quads) {
|
|
565
|
+
await this.deleteExactQuad(tx, scopedDictionary, value);
|
|
566
|
+
}
|
|
567
|
+
await this.bumpFactsDataVersion(tx);
|
|
568
|
+
});
|
|
569
|
+
this.factsDataVersion = await this.readFactsDataVersion();
|
|
570
|
+
this.markDerivedDirty();
|
|
571
|
+
return scan.quads.length;
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
this.cacheDirty = true;
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async applyDelta(deletes, inserts, options) {
|
|
579
|
+
await this.ensureReady();
|
|
580
|
+
if (deletes.length === 0 && inserts.length === 0) {
|
|
581
|
+
return { deletedRows: 0, insertedRows: 0 };
|
|
582
|
+
}
|
|
583
|
+
const deleteQuads = [];
|
|
584
|
+
for (const pattern of deletes) {
|
|
585
|
+
deleteQuads.push(...(await this.scan({ pattern })).quads);
|
|
586
|
+
}
|
|
587
|
+
const uniqueDeleteQuads = uniqueQuads(deleteQuads);
|
|
588
|
+
const executor = this.requireExecutor();
|
|
589
|
+
let deletedRows = 0;
|
|
590
|
+
try {
|
|
591
|
+
deletedRows = await executor.transaction(async (tx) => {
|
|
592
|
+
const scopedDictionary = new PostgresRdfTermDictionary(tx);
|
|
593
|
+
await scopedDictionary.initialize();
|
|
594
|
+
let deletedRows = 0;
|
|
595
|
+
for (const value of uniqueDeleteQuads) {
|
|
596
|
+
const deleted = await this.deleteExactQuad(tx, scopedDictionary, value);
|
|
597
|
+
deletedRows += deleted;
|
|
598
|
+
}
|
|
599
|
+
const sourceId = options?.source ? await this.upsertSource(options.source, tx) : null;
|
|
600
|
+
await this.insertQuads(tx, scopedDictionary, inserts, sourceId, options?.sourceLineNo ?? null);
|
|
601
|
+
if (deletedRows > 0 || inserts.length > 0) {
|
|
602
|
+
await this.bumpFactsDataVersion(tx);
|
|
603
|
+
}
|
|
604
|
+
return deletedRows;
|
|
605
|
+
});
|
|
606
|
+
this.factsDataVersion = await this.readFactsDataVersion();
|
|
607
|
+
this.markDerivedDirty();
|
|
608
|
+
return {
|
|
609
|
+
deletedRows,
|
|
610
|
+
insertedRows: inserts.length,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
this.cacheDirty = true;
|
|
615
|
+
throw error;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async scan(query) {
|
|
619
|
+
await this.ensureReady();
|
|
620
|
+
if (!isPgRdf3xCompatiblePattern(query.pattern)) {
|
|
621
|
+
await this.ensureCacheReady();
|
|
622
|
+
const fallback = await this.requireCache().scan(query);
|
|
623
|
+
fallback.metrics.queryPlan = ['PostgresRdf3xScanFallback', ...(fallback.metrics.queryPlan ?? [])];
|
|
624
|
+
return fallback;
|
|
625
|
+
}
|
|
626
|
+
return this.scanNative(query.pattern, query.options);
|
|
627
|
+
}
|
|
628
|
+
async query(query) {
|
|
629
|
+
await this.ensureReady();
|
|
630
|
+
const native = await this.queryNative(query);
|
|
631
|
+
if (native.result) {
|
|
632
|
+
return native.result;
|
|
633
|
+
}
|
|
634
|
+
await this.ensureCacheReady();
|
|
635
|
+
const fallback = await this.requireCache().query(query);
|
|
636
|
+
fallback.metrics.plan.unshift('PostgresRdf3xFallback');
|
|
637
|
+
return fallback;
|
|
638
|
+
}
|
|
639
|
+
async refreshDerivedIndexes() {
|
|
640
|
+
await this.ensureReady();
|
|
641
|
+
const factsDataVersion = await this.readFactsDataVersion();
|
|
642
|
+
const previousFactsDataVersion = await this.readRdf3xFactsDataVersion();
|
|
643
|
+
if (previousFactsDataVersion === factsDataVersion && !this.rdf3xDirty) {
|
|
644
|
+
return {
|
|
645
|
+
derivedIndexProfile: 'rdf3x',
|
|
646
|
+
factsDataVersion,
|
|
647
|
+
rdf3x: {
|
|
648
|
+
refreshed: false,
|
|
649
|
+
previousFactsDataVersion,
|
|
650
|
+
factsDataVersion,
|
|
651
|
+
syncedWithFacts: true,
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const rebuild = await this.rebuildRdf3xDerivedIndexes(factsDataVersion);
|
|
656
|
+
this.rdf3xDataVersion = factsDataVersion;
|
|
657
|
+
this.rdf3xDirty = false;
|
|
658
|
+
return {
|
|
659
|
+
derivedIndexProfile: 'rdf3x',
|
|
660
|
+
factsDataVersion,
|
|
661
|
+
rdf3x: {
|
|
662
|
+
refreshed: previousFactsDataVersion !== factsDataVersion,
|
|
663
|
+
previousFactsDataVersion,
|
|
664
|
+
factsDataVersion,
|
|
665
|
+
syncedWithFacts: true,
|
|
666
|
+
rebuild,
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
async storageStats() {
|
|
671
|
+
await this.ensureReady();
|
|
672
|
+
const facts = await this.factsStats();
|
|
673
|
+
const rdf3x = await this.rdf3xStats();
|
|
674
|
+
const factsBytes = facts.databaseBytes;
|
|
675
|
+
const derivedBytes = rdf3x.databaseBytes;
|
|
676
|
+
const totalBytes = factsBytes + derivedBytes;
|
|
677
|
+
return {
|
|
678
|
+
derivedIndexProfile: 'rdf3x',
|
|
679
|
+
facts,
|
|
680
|
+
rdf3x: {
|
|
681
|
+
stats: rdf3x,
|
|
682
|
+
syncedWithFacts: rdf3x.factsDataVersion === await this.readFactsDataVersion(),
|
|
683
|
+
},
|
|
684
|
+
factsBytes,
|
|
685
|
+
derivedBytes,
|
|
686
|
+
totalBytes,
|
|
687
|
+
derivedToFactsRatio: factsBytes === 0 ? 0 : derivedBytes / factsBytes,
|
|
688
|
+
totalToFactsRatio: factsBytes === 0 ? 0 : totalBytes / factsBytes,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
async initializeSchema() {
|
|
692
|
+
const executor = this.requireExecutor();
|
|
693
|
+
await this.ensureCompatibleSchemaVersion(executor);
|
|
694
|
+
await executor.exec(`
|
|
695
|
+
CREATE TABLE IF NOT EXISTS rdf_sources (
|
|
696
|
+
id BIGSERIAL PRIMARY KEY,
|
|
697
|
+
source TEXT NOT NULL UNIQUE,
|
|
698
|
+
workspace TEXT NOT NULL,
|
|
699
|
+
local_path TEXT,
|
|
700
|
+
content_type TEXT,
|
|
701
|
+
last_indexed_at TIMESTAMPTZ,
|
|
702
|
+
source_version TEXT
|
|
703
|
+
)
|
|
704
|
+
`);
|
|
705
|
+
await executor.exec(`
|
|
706
|
+
CREATE TABLE IF NOT EXISTS rdf_quads (
|
|
707
|
+
graph_id BIGINT NOT NULL,
|
|
708
|
+
subject_id BIGINT NOT NULL,
|
|
709
|
+
predicate_id BIGINT NOT NULL,
|
|
710
|
+
object_id BIGINT NOT NULL,
|
|
711
|
+
source_file_id BIGINT,
|
|
712
|
+
source_line_no BIGINT,
|
|
713
|
+
PRIMARY KEY (graph_id, subject_id, predicate_id, object_id)
|
|
714
|
+
)
|
|
715
|
+
`);
|
|
716
|
+
await executor.exec('CREATE INDEX IF NOT EXISTS rdf_quads_spog ON rdf_quads (subject_id, predicate_id, object_id, graph_id)');
|
|
717
|
+
await executor.exec('CREATE INDEX IF NOT EXISTS rdf_quads_sopg ON rdf_quads (subject_id, object_id, predicate_id, graph_id)');
|
|
718
|
+
await executor.exec('CREATE INDEX IF NOT EXISTS rdf_quads_psog ON rdf_quads (predicate_id, subject_id, object_id, graph_id)');
|
|
719
|
+
await executor.exec('CREATE INDEX IF NOT EXISTS rdf_quads_posg ON rdf_quads (predicate_id, object_id, subject_id, graph_id)');
|
|
720
|
+
await executor.exec('CREATE INDEX IF NOT EXISTS rdf_quads_ospg ON rdf_quads (object_id, subject_id, predicate_id, graph_id)');
|
|
721
|
+
await executor.exec('CREATE INDEX IF NOT EXISTS rdf_quads_opsg ON rdf_quads (object_id, predicate_id, subject_id, graph_id)');
|
|
722
|
+
await executor.exec('CREATE INDEX IF NOT EXISTS rdf_quads_gspo ON rdf_quads (graph_id, subject_id, predicate_id, object_id)');
|
|
723
|
+
await executor.exec('CREATE INDEX IF NOT EXISTS rdf_quads_gpos ON rdf_quads (graph_id, predicate_id, object_id, subject_id)');
|
|
724
|
+
await executor.exec('CREATE INDEX IF NOT EXISTS rdf_quads_source ON rdf_quads (source_file_id, source_line_no)');
|
|
725
|
+
await executor.exec(`
|
|
726
|
+
CREATE TABLE IF NOT EXISTS rdf_index_metadata (
|
|
727
|
+
key TEXT PRIMARY KEY,
|
|
728
|
+
value TEXT NOT NULL
|
|
729
|
+
)
|
|
730
|
+
`);
|
|
731
|
+
await executor.exec(`
|
|
732
|
+
INSERT INTO rdf_index_metadata (key, value)
|
|
733
|
+
VALUES ('schema_version', '${POSTGRES_RDF_SCHEMA_VERSION}')
|
|
734
|
+
ON CONFLICT (key) DO NOTHING
|
|
735
|
+
`);
|
|
736
|
+
await executor.exec(`
|
|
737
|
+
INSERT INTO rdf_index_metadata (key, value)
|
|
738
|
+
VALUES ('data_version', '0')
|
|
739
|
+
ON CONFLICT (key) DO NOTHING
|
|
740
|
+
`);
|
|
741
|
+
await this.initializeRdf3xSchema(executor);
|
|
742
|
+
}
|
|
743
|
+
async ensureCompatibleSchemaVersion(executor) {
|
|
744
|
+
await executor.exec(`
|
|
745
|
+
CREATE TABLE IF NOT EXISTS rdf_index_metadata (
|
|
746
|
+
key TEXT PRIMARY KEY,
|
|
747
|
+
value TEXT NOT NULL
|
|
748
|
+
)
|
|
749
|
+
`);
|
|
750
|
+
const row = await executor.query("SELECT value FROM rdf_index_metadata WHERE key = 'schema_version'");
|
|
751
|
+
const version = row[0]?.value;
|
|
752
|
+
if (version === undefined || version === String(POSTGRES_RDF_SCHEMA_VERSION)) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
await executor.transaction(async (tx) => {
|
|
756
|
+
await tx.exec('DROP TABLE IF EXISTS rdf_quads');
|
|
757
|
+
await tx.exec('DROP TABLE IF EXISTS rdf_sources');
|
|
758
|
+
await tx.exec('DROP TABLE IF EXISTS rdf_terms');
|
|
759
|
+
await tx.exec('DELETE FROM rdf_index_metadata');
|
|
760
|
+
await tx.exec("INSERT INTO rdf_index_metadata (key, value) VALUES ('schema_version', $1)", [String(POSTGRES_RDF_SCHEMA_VERSION)]);
|
|
761
|
+
await tx.exec("INSERT INTO rdf_index_metadata (key, value) VALUES ('data_version', '0')");
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
async initializeRdf3xSchema(executor) {
|
|
765
|
+
await executor.exec(`
|
|
766
|
+
CREATE TABLE IF NOT EXISTS rdf3x_metadata (
|
|
767
|
+
key TEXT PRIMARY KEY,
|
|
768
|
+
value TEXT NOT NULL
|
|
769
|
+
)
|
|
770
|
+
`);
|
|
771
|
+
const row = await executor.query("SELECT value FROM rdf3x_metadata WHERE key = 'schema_version'");
|
|
772
|
+
const version = row[0]?.value;
|
|
773
|
+
if (version !== undefined && version !== String(POSTGRES_RDF3X_SCHEMA_VERSION)) {
|
|
774
|
+
await this.dropRdf3xDerivedSchema(executor);
|
|
775
|
+
}
|
|
776
|
+
await executor.exec(`
|
|
777
|
+
CREATE TABLE IF NOT EXISTS ${Rdf3xSchema_1.RDF3X_GRAPH_PROJECTION_TABLE} (
|
|
778
|
+
graph_id BIGINT PRIMARY KEY,
|
|
779
|
+
membership_count BIGINT NOT NULL
|
|
780
|
+
)
|
|
781
|
+
`);
|
|
782
|
+
for (const projection of PAIR_PROJECTIONS) {
|
|
783
|
+
await executor.exec(`
|
|
784
|
+
CREATE TABLE IF NOT EXISTS ${projection.table} (
|
|
785
|
+
${projection.columns[0]} BIGINT NOT NULL,
|
|
786
|
+
${projection.columns[1]} BIGINT NOT NULL,
|
|
787
|
+
triple_count BIGINT NOT NULL,
|
|
788
|
+
membership_count BIGINT NOT NULL,
|
|
789
|
+
min_${projection.remainder} BIGINT,
|
|
790
|
+
max_${projection.remainder} BIGINT,
|
|
791
|
+
PRIMARY KEY (${projection.columns.join(', ')})
|
|
792
|
+
)
|
|
793
|
+
`);
|
|
794
|
+
}
|
|
795
|
+
for (const projection of TERM_PROJECTIONS) {
|
|
796
|
+
await executor.exec(`
|
|
797
|
+
CREATE TABLE IF NOT EXISTS ${projection.table} (
|
|
798
|
+
${projection.column} BIGINT PRIMARY KEY,
|
|
799
|
+
triple_count BIGINT NOT NULL,
|
|
800
|
+
membership_count BIGINT NOT NULL
|
|
801
|
+
)
|
|
802
|
+
`);
|
|
803
|
+
}
|
|
804
|
+
await executor.exec(`
|
|
805
|
+
INSERT INTO rdf3x_metadata (key, value)
|
|
806
|
+
VALUES ('schema_version', $1)
|
|
807
|
+
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
|
808
|
+
`, [String(POSTGRES_RDF3X_SCHEMA_VERSION)]);
|
|
809
|
+
await executor.exec(`
|
|
810
|
+
INSERT INTO rdf3x_metadata (key, value)
|
|
811
|
+
VALUES ('facts_data_version', '0')
|
|
812
|
+
ON CONFLICT (key) DO NOTHING
|
|
813
|
+
`);
|
|
814
|
+
}
|
|
815
|
+
async dropRdf3xDerivedSchema(executor) {
|
|
816
|
+
for (const table of [
|
|
817
|
+
...PAIR_PROJECTIONS.map((projection) => projection.table),
|
|
818
|
+
...TERM_PROJECTIONS.map((projection) => projection.table),
|
|
819
|
+
Rdf3xSchema_1.RDF3X_GRAPH_PROJECTION_TABLE,
|
|
820
|
+
]) {
|
|
821
|
+
await executor.exec(`DROP TABLE IF EXISTS ${table}`);
|
|
822
|
+
}
|
|
823
|
+
await executor.exec('DELETE FROM rdf3x_metadata');
|
|
824
|
+
}
|
|
825
|
+
async clearRdf3xDerivedTables(executor) {
|
|
826
|
+
for (const projection of PAIR_PROJECTIONS) {
|
|
827
|
+
await executor.exec(`DELETE FROM ${projection.table}`);
|
|
828
|
+
}
|
|
829
|
+
for (const projection of TERM_PROJECTIONS) {
|
|
830
|
+
await executor.exec(`DELETE FROM ${projection.table}`);
|
|
831
|
+
}
|
|
832
|
+
await executor.exec(`DELETE FROM ${Rdf3xSchema_1.RDF3X_GRAPH_PROJECTION_TABLE}`);
|
|
833
|
+
}
|
|
834
|
+
async rebuildRdf3xDerivedIndexes(factsDataVersion) {
|
|
835
|
+
const start = Date.now();
|
|
836
|
+
const executor = this.requireExecutor();
|
|
837
|
+
const scannedQuads = await this.scalarCount(`SELECT COUNT(*) AS count FROM ${RDF_FACTS_TABLE}`);
|
|
838
|
+
await executor.transaction(async (tx) => {
|
|
839
|
+
await this.clearRdf3xDerivedTables(tx);
|
|
840
|
+
for (const projection of PAIR_PROJECTIONS) {
|
|
841
|
+
await tx.exec(`
|
|
842
|
+
INSERT INTO ${projection.table} (
|
|
843
|
+
${projection.columns[0]},
|
|
844
|
+
${projection.columns[1]},
|
|
845
|
+
triple_count,
|
|
846
|
+
membership_count,
|
|
847
|
+
min_${projection.remainder},
|
|
848
|
+
max_${projection.remainder}
|
|
849
|
+
)
|
|
850
|
+
SELECT
|
|
851
|
+
triple.${projection.columns[0]},
|
|
852
|
+
triple.${projection.columns[1]},
|
|
853
|
+
triple.triple_count,
|
|
854
|
+
COALESCE(member.membership_count, 0) AS membership_count,
|
|
855
|
+
triple.min_remainder,
|
|
856
|
+
triple.max_remainder
|
|
857
|
+
FROM (
|
|
858
|
+
SELECT
|
|
859
|
+
${projection.columns[0]},
|
|
860
|
+
${projection.columns[1]},
|
|
861
|
+
COUNT(DISTINCT ${projection.remainder}) AS triple_count,
|
|
862
|
+
MIN(${projection.remainder}) AS min_remainder,
|
|
863
|
+
MAX(${projection.remainder}) AS max_remainder
|
|
864
|
+
FROM ${RDF_FACTS_TABLE}
|
|
865
|
+
GROUP BY ${projection.columns[0]}, ${projection.columns[1]}
|
|
866
|
+
) triple
|
|
867
|
+
LEFT JOIN (
|
|
868
|
+
SELECT
|
|
869
|
+
${projection.columns[0]},
|
|
870
|
+
${projection.columns[1]},
|
|
871
|
+
COUNT(*) AS membership_count
|
|
872
|
+
FROM ${RDF_FACTS_TABLE}
|
|
873
|
+
GROUP BY ${projection.columns[0]}, ${projection.columns[1]}
|
|
874
|
+
) member
|
|
875
|
+
ON member.${projection.columns[0]} = triple.${projection.columns[0]}
|
|
876
|
+
AND member.${projection.columns[1]} = triple.${projection.columns[1]}
|
|
877
|
+
`);
|
|
878
|
+
}
|
|
879
|
+
for (const projection of TERM_PROJECTIONS) {
|
|
880
|
+
await tx.exec(`
|
|
881
|
+
INSERT INTO ${projection.table} (
|
|
882
|
+
${projection.column},
|
|
883
|
+
triple_count,
|
|
884
|
+
membership_count
|
|
885
|
+
)
|
|
886
|
+
SELECT
|
|
887
|
+
triple.${projection.column},
|
|
888
|
+
triple.triple_count,
|
|
889
|
+
COALESCE(member.membership_count, 0) AS membership_count
|
|
890
|
+
FROM (
|
|
891
|
+
SELECT
|
|
892
|
+
${projection.column},
|
|
893
|
+
COUNT(*) AS triple_count
|
|
894
|
+
FROM (
|
|
895
|
+
SELECT DISTINCT subject_id, predicate_id, object_id
|
|
896
|
+
FROM ${RDF_FACTS_TABLE}
|
|
897
|
+
) distinct_triples
|
|
898
|
+
GROUP BY ${projection.column}
|
|
899
|
+
) triple
|
|
900
|
+
LEFT JOIN (
|
|
901
|
+
SELECT
|
|
902
|
+
${projection.column},
|
|
903
|
+
COUNT(*) AS membership_count
|
|
904
|
+
FROM ${RDF_FACTS_TABLE}
|
|
905
|
+
GROUP BY ${projection.column}
|
|
906
|
+
) member
|
|
907
|
+
ON member.${projection.column} = triple.${projection.column}
|
|
908
|
+
`);
|
|
909
|
+
}
|
|
910
|
+
await tx.exec(`
|
|
911
|
+
INSERT INTO ${Rdf3xSchema_1.RDF3X_GRAPH_PROJECTION_TABLE} (
|
|
912
|
+
graph_id,
|
|
913
|
+
membership_count
|
|
914
|
+
)
|
|
915
|
+
SELECT graph_id, COUNT(*) AS membership_count
|
|
916
|
+
FROM ${RDF_FACTS_TABLE}
|
|
917
|
+
GROUP BY graph_id
|
|
918
|
+
`);
|
|
919
|
+
await tx.exec(`
|
|
920
|
+
INSERT INTO rdf3x_metadata (key, value)
|
|
921
|
+
VALUES ('facts_data_version', $1)
|
|
922
|
+
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
|
923
|
+
`, [String(factsDataVersion)]);
|
|
924
|
+
});
|
|
925
|
+
const stats = await this.rdf3xStats();
|
|
926
|
+
return {
|
|
927
|
+
scannedQuads,
|
|
928
|
+
uniqueTriples: stats.uniqueTriples,
|
|
929
|
+
memberships: stats.membershipCount,
|
|
930
|
+
projectionRows: pairProjectionRowTotal(stats.pairProjectionRows) + termProjectionRowTotal(stats.termProjectionRows),
|
|
931
|
+
factsDataVersion,
|
|
932
|
+
durationMs: Date.now() - start,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
async scanNative(pattern, options) {
|
|
936
|
+
const start = Date.now();
|
|
937
|
+
const resolved = await this.resolvePattern(pattern);
|
|
938
|
+
if (resolved.unresolved) {
|
|
939
|
+
return {
|
|
940
|
+
quads: [],
|
|
941
|
+
metrics: this.indexMetrics('none', 0, 0, start, [`unresolved ${resolved.unresolved}`]),
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
const compiled = this.compileScanSql(resolved, options);
|
|
945
|
+
const matchedRows = await this.scalarCount(compiled.countSql, compiled.countParams);
|
|
946
|
+
const rows = await this.requireExecutor().query(compiled.sql, compiled.params);
|
|
947
|
+
return {
|
|
948
|
+
quads: await this.rowsToQuads(rows),
|
|
949
|
+
metrics: this.indexMetrics(compiled.indexChoice, matchedRows, rows.length, start, [
|
|
950
|
+
...compiled.queryPlan,
|
|
951
|
+
compiled.sql,
|
|
952
|
+
]),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
async queryNative(query) {
|
|
956
|
+
if (!this.canTryNativeQuery(query)) {
|
|
957
|
+
return { unsupported: true };
|
|
958
|
+
}
|
|
959
|
+
const start = Date.now();
|
|
960
|
+
const aggregates = queryAggregates(query);
|
|
961
|
+
const requiredPatterns = query.patterns.length > 0 ? query.patterns : [{}];
|
|
962
|
+
const compiledPatterns = await this.compileNativeJoinPatterns(requiredPatterns, query.filters ?? []);
|
|
963
|
+
if (!compiledPatterns) {
|
|
964
|
+
return { unsupported: true };
|
|
965
|
+
}
|
|
966
|
+
if (!this.allFiltersPushed(query.filters ?? [], compiledPatterns)) {
|
|
967
|
+
return { unsupported: true };
|
|
968
|
+
}
|
|
969
|
+
if (aggregates.length > 0) {
|
|
970
|
+
if (!query.patterns.length) {
|
|
971
|
+
return { unsupported: true };
|
|
972
|
+
}
|
|
973
|
+
const aggregateResult = await this.queryNativeAggregate(query, compiledPatterns, aggregates, start);
|
|
974
|
+
return aggregateResult ? { result: aggregateResult } : { unsupported: true };
|
|
975
|
+
}
|
|
976
|
+
const visibleVariables = uniqueStrings(requiredPatterns.flatMap((pattern) => variablesInPattern(pattern)));
|
|
977
|
+
const project = query.select && query.select.length > 0 ? query.select : visibleVariables;
|
|
978
|
+
if (project.some((variableName) => !visibleVariables.includes(variableName))) {
|
|
979
|
+
return { unsupported: true };
|
|
980
|
+
}
|
|
981
|
+
if ((query.orderBy ?? []).some((entry) => !visibleVariables.includes(entry.variable))) {
|
|
982
|
+
return { unsupported: true };
|
|
983
|
+
}
|
|
984
|
+
const compiled = await this.compileJoinSql(compiledPatterns, {
|
|
985
|
+
project,
|
|
986
|
+
distinct: query.distinct,
|
|
987
|
+
orderBy: query.orderBy,
|
|
988
|
+
limit: query.limit,
|
|
989
|
+
offset: query.offset,
|
|
990
|
+
countMatchedRows: query.limit !== undefined || query.offset !== undefined,
|
|
991
|
+
});
|
|
992
|
+
if (compiled.unresolved) {
|
|
993
|
+
return {
|
|
994
|
+
result: {
|
|
995
|
+
bindings: [],
|
|
996
|
+
metrics: this.localMetrics(start, 0, 0, 0, [compiled.indexChoice], [
|
|
997
|
+
...compiled.queryPlan,
|
|
998
|
+
`unresolved ${compiled.unresolved}`,
|
|
999
|
+
], query.filters?.length ?? 0),
|
|
1000
|
+
},
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
const rows = await this.requireExecutor().query(compiled.sql, compiled.params);
|
|
1004
|
+
const matchedRows = compiled.countSql
|
|
1005
|
+
? await this.scalarCount(compiled.countSql, compiled.countParams)
|
|
1006
|
+
: rows.length;
|
|
1007
|
+
const bindings = await this.joinRowsToBindings(rows, compiled.variableAliases);
|
|
1008
|
+
const plan = [
|
|
1009
|
+
...storagePlanMarkers(compiled.queryPlan),
|
|
1010
|
+
`PostgresRdf3xJoin(${compiledPatterns.map((entry) => describePatternSource(entry)).join('|')})`,
|
|
1011
|
+
...(query.distinct ? [`PostgresRdf3xJoinDistinct(${project.map((variableName) => `?${variableName}`).join(',')})`] : []),
|
|
1012
|
+
...(query.limit !== undefined || query.offset !== undefined ? ['PostgresRdf3xJoinLimit'] : []),
|
|
1013
|
+
];
|
|
1014
|
+
return {
|
|
1015
|
+
result: {
|
|
1016
|
+
bindings,
|
|
1017
|
+
metrics: this.localMetrics(start, matchedRows, matchedRows, bindings.length, [compiled.indexChoice], plan, query.filters?.length ?? 0),
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
async queryNativeAggregate(query, patterns, aggregates, start) {
|
|
1022
|
+
if (!this.canNativeAggregate(query, aggregates)) {
|
|
1023
|
+
return undefined;
|
|
1024
|
+
}
|
|
1025
|
+
const visibleVariables = uniqueStrings(query.patterns.flatMap((pattern) => variablesInPattern(pattern)));
|
|
1026
|
+
const compiled = await this.compileJoinSql(patterns, {
|
|
1027
|
+
project: visibleVariables,
|
|
1028
|
+
countMatchedRows: false,
|
|
1029
|
+
});
|
|
1030
|
+
if (compiled.unresolved) {
|
|
1031
|
+
return {
|
|
1032
|
+
bindings: [],
|
|
1033
|
+
count: 0,
|
|
1034
|
+
metrics: this.localMetrics(start, 0, 0, 0, [compiled.indexChoice], [
|
|
1035
|
+
...compiled.queryPlan,
|
|
1036
|
+
`unresolved ${compiled.unresolved}`,
|
|
1037
|
+
], query.filters?.length ?? 0),
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
const aggregateAliases = new Map();
|
|
1041
|
+
const aggregateTypes = new Map();
|
|
1042
|
+
const aggregateColumns = aggregates.map((aggregate, index) => this.compileAggregateColumn(aggregate, index, compiled.variableAliases, aggregateAliases, aggregateTypes));
|
|
1043
|
+
const groupColumns = (query.groupBy ?? []).map((variableName) => {
|
|
1044
|
+
const alias = compiled.variableAliases.get(variableName);
|
|
1045
|
+
if (!alias) {
|
|
1046
|
+
throw new Error(`Postgres RDF-3X group aggregate cannot read unbound variable: ${variableName}`);
|
|
1047
|
+
}
|
|
1048
|
+
return { variableName, alias, column: `source.${alias}` };
|
|
1049
|
+
});
|
|
1050
|
+
const projection = [
|
|
1051
|
+
...groupColumns.map((group) => `${group.column} AS ${group.alias}`),
|
|
1052
|
+
...aggregateColumns.map((aggregate) => aggregate.sql),
|
|
1053
|
+
].filter(Boolean).join(', ');
|
|
1054
|
+
let sql = `SELECT ${projection} FROM (${compiled.sql}) source`;
|
|
1055
|
+
if (groupColumns.length > 0) {
|
|
1056
|
+
sql += ` GROUP BY ${groupColumns.map((group) => group.column).join(', ')}`;
|
|
1057
|
+
}
|
|
1058
|
+
const builder = new PgSqlBuilder(compiled.params);
|
|
1059
|
+
const havingClause = this.buildAggregateHavingClause(query.having ?? [], aggregateColumns, builder);
|
|
1060
|
+
sql += havingClause.sql;
|
|
1061
|
+
sql += this.buildAggregateOrderClause(query.orderBy ?? [], compiled.variableAliases, aggregateColumns);
|
|
1062
|
+
const pagination = this.buildPagination(query, builder);
|
|
1063
|
+
sql += pagination.sql;
|
|
1064
|
+
const params = builder.snapshot();
|
|
1065
|
+
const rows = await this.requireExecutor().query(sql, params);
|
|
1066
|
+
const matchedRows = groupColumns.length > 0
|
|
1067
|
+
? await this.scalarCount(`SELECT COUNT(*) AS count FROM (
|
|
1068
|
+
SELECT ${groupColumns.map((group) => group.column).join(', ')}
|
|
1069
|
+
FROM (${compiled.sql}) source
|
|
1070
|
+
GROUP BY ${groupColumns.map((group) => group.column).join(', ')}
|
|
1071
|
+
${havingClause.sql}
|
|
1072
|
+
) grouped_count`, builder.snapshot().slice(0, builder.snapshot().length - pagination.paramCount))
|
|
1073
|
+
: rows.length;
|
|
1074
|
+
const bindings = await this.joinRowsToBindings(rows, new Map(groupColumns.map((group) => [group.variableName, group.alias])), aggregateAliases, aggregateTypes);
|
|
1075
|
+
const firstCount = !groupColumns.length && aggregates[0] ? Number(bindings[0]?.[aggregates[0].as]?.value ?? 0) : undefined;
|
|
1076
|
+
const plan = [
|
|
1077
|
+
...storagePlanMarkers(compiled.queryPlan),
|
|
1078
|
+
groupColumns.length > 0 ? 'PostgresRdf3xGroupCount' : 'PostgresRdf3xJoinCount',
|
|
1079
|
+
aggregatePlan(aggregates, groupColumns.length > 0),
|
|
1080
|
+
...(havingClause.sql ? [`PostgresRdf3xAggregateHaving(${(query.having ?? []).map(describeFilter).join(',')})`] : []),
|
|
1081
|
+
...((query.orderBy ?? []).length > 0 ? [`PostgresRdf3xAggregateOrder(${describeQueryOrder(query.orderBy ?? [])})`] : []),
|
|
1082
|
+
...(pagination.sql ? ['PostgresRdf3xAggregateLimit'] : []),
|
|
1083
|
+
];
|
|
1084
|
+
return {
|
|
1085
|
+
bindings,
|
|
1086
|
+
...(firstCount !== undefined ? { count: firstCount } : {}),
|
|
1087
|
+
metrics: this.localMetrics(start, matchedRows, matchedRows, bindings.length, [compiled.indexChoice], plan, query.filters?.length ?? 0),
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
canNativeAggregate(query, aggregates) {
|
|
1091
|
+
if (aggregates.length === 0 || aggregates.some((aggregate) => aggregate.type !== 'count')) {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
const visibleVariables = uniqueStrings(query.patterns.flatMap((pattern) => variablesInPattern(pattern)));
|
|
1095
|
+
const aggregateVariables = new Set(aggregates.map((aggregate) => aggregate.as));
|
|
1096
|
+
if ((query.groupBy ?? []).some((variableName) => !visibleVariables.includes(variableName))) {
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
if (aggregates.some((aggregate) => (aggregate.variable !== undefined && !visibleVariables.includes(aggregate.variable)))) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
if (aggregates.some((aggregate) => (aggregate.distinctVariables !== undefined
|
|
1103
|
+
&& aggregate.distinctVariables.some((variableName) => !visibleVariables.includes(variableName))))) {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
if ((query.having ?? []).some((filter) => !this.canNativeAggregateHaving(filter, aggregateVariables))) {
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
if ((query.orderBy ?? []).some((entry) => (!visibleVariables.includes(entry.variable) && !aggregateVariables.has(entry.variable)))) {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
if ((query.groupBy ?? []).length === 0) {
|
|
1113
|
+
return !query.distinct
|
|
1114
|
+
&& (query.having ?? []).length === 0
|
|
1115
|
+
&& (query.orderBy ?? []).length === 0
|
|
1116
|
+
&& query.limit === undefined
|
|
1117
|
+
&& query.offset === undefined
|
|
1118
|
+
&& (query.select ?? []).every((variableName) => aggregateVariables.has(variableName));
|
|
1119
|
+
}
|
|
1120
|
+
return !query.distinct && (query.orderBy ?? []).every((entry) => ((query.groupBy ?? []).includes(entry.variable) || aggregateVariables.has(entry.variable))) && (query.select ?? []).every((variableName) => ((query.groupBy ?? []).includes(variableName) || aggregateVariables.has(variableName)));
|
|
1121
|
+
}
|
|
1122
|
+
canNativeAggregateHaving(filter, aggregateVariables) {
|
|
1123
|
+
return aggregateVariables.has(filter.variable)
|
|
1124
|
+
&& !filter.operand
|
|
1125
|
+
&& !filter.variable2
|
|
1126
|
+
&& filter.value !== undefined
|
|
1127
|
+
&& isNativeAggregateHavingOperator(filter.operator)
|
|
1128
|
+
&& typeof filter.value !== 'boolean'
|
|
1129
|
+
&& this.aggregateFilterValue(filter.value) !== undefined;
|
|
1130
|
+
}
|
|
1131
|
+
compileAggregateColumn(aggregate, index, variableAliases, aggregateAliases, aggregateTypes) {
|
|
1132
|
+
const alias = `a${index}`;
|
|
1133
|
+
aggregateAliases.set(aggregate.as, alias);
|
|
1134
|
+
aggregateTypes.set(aggregate.as, 'integer');
|
|
1135
|
+
if (!aggregate.variable) {
|
|
1136
|
+
const expression = aggregate.distinct
|
|
1137
|
+
? `COUNT(DISTINCT ${joinSolutionMappingKeyExpression(variableAliases, aggregate.distinctVariables)})`
|
|
1138
|
+
: 'COUNT(*)';
|
|
1139
|
+
return {
|
|
1140
|
+
variableName: aggregate.as,
|
|
1141
|
+
alias,
|
|
1142
|
+
type: 'integer',
|
|
1143
|
+
expression,
|
|
1144
|
+
sql: `${expression} AS ${alias}`,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
const variableAlias = variableAliases.get(aggregate.variable);
|
|
1148
|
+
if (!variableAlias) {
|
|
1149
|
+
throw new Error(`Postgres RDF-3X aggregate cannot read unbound variable: ${aggregate.variable}`);
|
|
1150
|
+
}
|
|
1151
|
+
const expression = `COUNT(${aggregate.distinct ? 'DISTINCT ' : ''}source.${variableAlias})`;
|
|
1152
|
+
return {
|
|
1153
|
+
variableName: aggregate.as,
|
|
1154
|
+
alias,
|
|
1155
|
+
type: 'integer',
|
|
1156
|
+
expression,
|
|
1157
|
+
sql: `${expression} AS ${alias}`,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
buildAggregateHavingClause(filters, aggregates, builder) {
|
|
1161
|
+
if (filters.length === 0) {
|
|
1162
|
+
return { sql: '' };
|
|
1163
|
+
}
|
|
1164
|
+
return {
|
|
1165
|
+
sql: ` HAVING ${filters.map((filter) => {
|
|
1166
|
+
const aggregate = aggregates.find((entry) => entry.variableName === filter.variable);
|
|
1167
|
+
if (!aggregate) {
|
|
1168
|
+
throw new Error(`Postgres RDF-3X aggregate cannot HAVING on unknown aggregate: ${filter.variable}`);
|
|
1169
|
+
}
|
|
1170
|
+
return `${aggregate.expression} ${aggregateSqlOperator(filter.operator)} ${builder.add(this.aggregateFilterValue(filter.value))}`;
|
|
1171
|
+
}).join(' AND ')}`,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
buildAggregateOrderClause(orderBy, variableAliases, aggregates) {
|
|
1175
|
+
if (orderBy.length === 0) {
|
|
1176
|
+
return '';
|
|
1177
|
+
}
|
|
1178
|
+
const order = orderBy.map((entry) => {
|
|
1179
|
+
const aggregate = aggregates.find((candidate) => candidate.variableName === entry.variable);
|
|
1180
|
+
if (aggregate) {
|
|
1181
|
+
return `${aggregate.alias} ${entry.direction === 'desc' ? 'DESC' : 'ASC'}`;
|
|
1182
|
+
}
|
|
1183
|
+
const alias = variableAliases.get(entry.variable);
|
|
1184
|
+
if (!alias) {
|
|
1185
|
+
throw new Error(`Postgres RDF-3X aggregate cannot order by unbound variable: ${entry.variable}`);
|
|
1186
|
+
}
|
|
1187
|
+
return `source.${alias} ${entry.direction === 'desc' ? 'DESC' : 'ASC'}`;
|
|
1188
|
+
});
|
|
1189
|
+
return ` ORDER BY ${order.join(', ')}`;
|
|
1190
|
+
}
|
|
1191
|
+
aggregateFilterValue(value) {
|
|
1192
|
+
if (typeof value === 'number')
|
|
1193
|
+
return Number.isFinite(value) ? value : undefined;
|
|
1194
|
+
if (typeof value === 'string') {
|
|
1195
|
+
const numeric = Number(value);
|
|
1196
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
1197
|
+
}
|
|
1198
|
+
if (value && typeof value === 'object' && 'termType' in value) {
|
|
1199
|
+
const numeric = (0, RdfTermSemantics_1.rdfNumericValue)(value.value);
|
|
1200
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
1201
|
+
}
|
|
1202
|
+
return undefined;
|
|
1203
|
+
}
|
|
1204
|
+
canTryNativeQuery(query) {
|
|
1205
|
+
return (query.values?.length ?? 0) === 0
|
|
1206
|
+
&& (query.textSearch?.length ?? 0) === 0
|
|
1207
|
+
&& (query.vectorSearch?.length ?? 0) === 0
|
|
1208
|
+
&& (query.unions?.length ?? 0) === 0
|
|
1209
|
+
&& (query.minus?.length ?? 0) === 0
|
|
1210
|
+
&& (query.exists?.length ?? 0) === 0
|
|
1211
|
+
&& (query.optional?.length ?? 0) === 0
|
|
1212
|
+
&& (query.binds?.length ?? 0) === 0;
|
|
1213
|
+
}
|
|
1214
|
+
async compileNativeJoinPatterns(patterns, filters) {
|
|
1215
|
+
const result = [];
|
|
1216
|
+
for (const pattern of patterns) {
|
|
1217
|
+
const compiled = {};
|
|
1218
|
+
const variables = {};
|
|
1219
|
+
const slotsByVariable = new Map();
|
|
1220
|
+
const pushed = new Set();
|
|
1221
|
+
for (const key of PATTERN_KEYS) {
|
|
1222
|
+
const value = pattern[key];
|
|
1223
|
+
if (!value)
|
|
1224
|
+
continue;
|
|
1225
|
+
if (isVariable(value)) {
|
|
1226
|
+
variables[key] = value.variable;
|
|
1227
|
+
slotsByVariable.set(value.variable, [...(slotsByVariable.get(value.variable) ?? []), key]);
|
|
1228
|
+
const pushdown = this.compilePushdownFilterWithIndexes(value.variable, filters);
|
|
1229
|
+
if (pushdown) {
|
|
1230
|
+
compiled[key] = pushdown.pattern;
|
|
1231
|
+
pushdown.filterIndexes.forEach((index) => pushed.add(index));
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
compiled[key] = value;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
result.push({
|
|
1239
|
+
pattern: compiled,
|
|
1240
|
+
variables,
|
|
1241
|
+
equalities: patternEqualities(slotsByVariable),
|
|
1242
|
+
pushedDownFilterIndexes: [...pushed],
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
return result;
|
|
1246
|
+
}
|
|
1247
|
+
allFiltersPushed(filters, patterns) {
|
|
1248
|
+
const pushed = new Set(patterns.flatMap((pattern) => pattern.pushedDownFilterIndexes));
|
|
1249
|
+
return filters.every((_filter, index) => pushed.has(index));
|
|
1250
|
+
}
|
|
1251
|
+
compilePushdownFilterWithIndexes(variableName, filters) {
|
|
1252
|
+
const operators = {};
|
|
1253
|
+
const filterIndexes = [];
|
|
1254
|
+
for (const [index, filter] of filters.entries()) {
|
|
1255
|
+
if (filter.variable !== variableName || filter.variable2 || !this.isPushdownFilter(filter)) {
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
switch (filter.operator) {
|
|
1259
|
+
case '$eq':
|
|
1260
|
+
case '$ne':
|
|
1261
|
+
if (filter.value === undefined || !(0, types_1.isTerm)(filter.value))
|
|
1262
|
+
return null;
|
|
1263
|
+
operators[filter.operator] = filter.value;
|
|
1264
|
+
filterIndexes.push(index);
|
|
1265
|
+
break;
|
|
1266
|
+
case '$gt':
|
|
1267
|
+
case '$gte':
|
|
1268
|
+
case '$lt':
|
|
1269
|
+
case '$lte':
|
|
1270
|
+
if (filter.value === undefined)
|
|
1271
|
+
return null;
|
|
1272
|
+
operators[filter.operator] = filter.value;
|
|
1273
|
+
filterIndexes.push(index);
|
|
1274
|
+
break;
|
|
1275
|
+
case '$in':
|
|
1276
|
+
case '$notIn':
|
|
1277
|
+
if (!filter.values?.length || filter.values.some((value) => !(0, types_1.isTerm)(value)))
|
|
1278
|
+
return null;
|
|
1279
|
+
operators[filter.operator] = filter.values;
|
|
1280
|
+
filterIndexes.push(index);
|
|
1281
|
+
break;
|
|
1282
|
+
case '$sameTerm':
|
|
1283
|
+
if (filter.value === undefined || !(0, types_1.isTerm)(filter.value))
|
|
1284
|
+
return null;
|
|
1285
|
+
operators.$eq = filter.value;
|
|
1286
|
+
filterIndexes.push(index);
|
|
1287
|
+
break;
|
|
1288
|
+
case '$termType':
|
|
1289
|
+
if (typeof filter.value !== 'string' || !['iri', 'blank', 'literal', 'numeric'].includes(filter.value))
|
|
1290
|
+
return null;
|
|
1291
|
+
operators.$termType = filter.value;
|
|
1292
|
+
filterIndexes.push(index);
|
|
1293
|
+
break;
|
|
1294
|
+
case '$lang':
|
|
1295
|
+
if (typeof filter.value !== 'string')
|
|
1296
|
+
return null;
|
|
1297
|
+
operators.$language = filter.value;
|
|
1298
|
+
filterIndexes.push(index);
|
|
1299
|
+
break;
|
|
1300
|
+
case '$notLang':
|
|
1301
|
+
if (typeof filter.value !== 'string')
|
|
1302
|
+
return null;
|
|
1303
|
+
operators.$notLanguage = filter.value;
|
|
1304
|
+
filterIndexes.push(index);
|
|
1305
|
+
break;
|
|
1306
|
+
case '$langMatches':
|
|
1307
|
+
if (typeof filter.value !== 'string')
|
|
1308
|
+
return null;
|
|
1309
|
+
operators.$langMatches = filter.value;
|
|
1310
|
+
filterIndexes.push(index);
|
|
1311
|
+
break;
|
|
1312
|
+
case '$datatype':
|
|
1313
|
+
case '$notDatatype':
|
|
1314
|
+
if (filter.value === undefined || !(0, types_1.isTerm)(filter.value) || filter.value.termType !== 'NamedNode')
|
|
1315
|
+
return null;
|
|
1316
|
+
operators[filter.operator] = filter.value;
|
|
1317
|
+
filterIndexes.push(index);
|
|
1318
|
+
break;
|
|
1319
|
+
case '$startsWith':
|
|
1320
|
+
if (typeof filter.value !== 'string')
|
|
1321
|
+
return null;
|
|
1322
|
+
operators.$startsWith = filter.value;
|
|
1323
|
+
filterIndexes.push(index);
|
|
1324
|
+
break;
|
|
1325
|
+
case '$contains':
|
|
1326
|
+
case '$endsWith':
|
|
1327
|
+
if (typeof filter.value !== 'string')
|
|
1328
|
+
return null;
|
|
1329
|
+
operators[filter.operator] = filter.value;
|
|
1330
|
+
filterIndexes.push(index);
|
|
1331
|
+
break;
|
|
1332
|
+
default:
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return Object.keys(operators).length > 0 ? { pattern: operators, filterIndexes } : null;
|
|
1337
|
+
}
|
|
1338
|
+
isPushdownFilter(filter) {
|
|
1339
|
+
if (filter.operand === 'stringLength' || filter.operand === 'lowerStringValue' || filter.operand === 'upperStringValue') {
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
if (filter.operand === 'stringValue') {
|
|
1343
|
+
return filter.operator === '$startsWith'
|
|
1344
|
+
|| filter.operator === '$contains'
|
|
1345
|
+
|| filter.operator === '$endsWith';
|
|
1346
|
+
}
|
|
1347
|
+
return filter.operator === '$eq'
|
|
1348
|
+
|| filter.operator === '$ne'
|
|
1349
|
+
|| filter.operator === '$gt'
|
|
1350
|
+
|| filter.operator === '$gte'
|
|
1351
|
+
|| filter.operator === '$lt'
|
|
1352
|
+
|| filter.operator === '$lte'
|
|
1353
|
+
|| filter.operator === '$in'
|
|
1354
|
+
|| filter.operator === '$notIn'
|
|
1355
|
+
|| filter.operator === '$startsWith'
|
|
1356
|
+
|| filter.operator === '$contains'
|
|
1357
|
+
|| filter.operator === '$endsWith'
|
|
1358
|
+
|| filter.operator === '$sameTerm'
|
|
1359
|
+
|| filter.operator === '$termType'
|
|
1360
|
+
|| filter.operator === '$lang'
|
|
1361
|
+
|| filter.operator === '$notLang'
|
|
1362
|
+
|| filter.operator === '$langMatches'
|
|
1363
|
+
|| filter.operator === '$datatype'
|
|
1364
|
+
|| filter.operator === '$notDatatype';
|
|
1365
|
+
}
|
|
1366
|
+
async resolvePattern(pattern) {
|
|
1367
|
+
const ids = {};
|
|
1368
|
+
const idSets = {};
|
|
1369
|
+
const excludedIdSets = {};
|
|
1370
|
+
const termFilters = {};
|
|
1371
|
+
let graphPrefix;
|
|
1372
|
+
let objectRange;
|
|
1373
|
+
for (const key of PATTERN_KEYS) {
|
|
1374
|
+
const value = pattern[key];
|
|
1375
|
+
if (!value) {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if ((0, types_1.isTerm)(value)) {
|
|
1379
|
+
const id = await this.requireDictionary().find(value);
|
|
1380
|
+
if (id === undefined) {
|
|
1381
|
+
return { ids, idSets, excludedIdSets, termFilters, unresolved: key };
|
|
1382
|
+
}
|
|
1383
|
+
ids[key] = id;
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
const operators = value;
|
|
1387
|
+
if (operators.$eq !== undefined) {
|
|
1388
|
+
const id = await this.termOperatorValueId(operators.$eq);
|
|
1389
|
+
if (id === undefined)
|
|
1390
|
+
return { ids, idSets, excludedIdSets, termFilters, unresolved: key };
|
|
1391
|
+
ids[key] = id;
|
|
1392
|
+
}
|
|
1393
|
+
if (operators.$in !== undefined) {
|
|
1394
|
+
const set = uniqueNumbers((await Promise.all(operators.$in.map((entry) => this.termOperatorValueId(entry))))
|
|
1395
|
+
.filter((id) => id !== undefined));
|
|
1396
|
+
if (set.length === 0)
|
|
1397
|
+
return { ids, idSets, excludedIdSets, termFilters, unresolved: key };
|
|
1398
|
+
idSets[key] = set;
|
|
1399
|
+
}
|
|
1400
|
+
if (operators.$notIn !== undefined) {
|
|
1401
|
+
const set = uniqueNumbers((await Promise.all(operators.$notIn.map((entry) => this.termOperatorValueId(entry))))
|
|
1402
|
+
.filter((id) => id !== undefined));
|
|
1403
|
+
if (set.length > 0)
|
|
1404
|
+
excludedIdSets[key] = set;
|
|
1405
|
+
}
|
|
1406
|
+
if (operators.$ne !== undefined) {
|
|
1407
|
+
const id = await this.termOperatorValueId(operators.$ne);
|
|
1408
|
+
if (id !== undefined)
|
|
1409
|
+
excludedIdSets[key] = uniqueNumbers([...(excludedIdSets[key] ?? []), id]);
|
|
1410
|
+
}
|
|
1411
|
+
if (key === 'graph' && operators.$startsWith !== undefined) {
|
|
1412
|
+
graphPrefix = operators.$startsWith;
|
|
1413
|
+
}
|
|
1414
|
+
const filter = await this.resolveTermFilter(key, operators);
|
|
1415
|
+
if (filter)
|
|
1416
|
+
termFilters[key] = filter;
|
|
1417
|
+
if (key === 'object') {
|
|
1418
|
+
objectRange = this.resolveObjectRange(operators);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return { ids, idSets, excludedIdSets, termFilters, ...(graphPrefix !== undefined ? { graphPrefix } : {}), ...(objectRange ? { objectRange } : {}) };
|
|
1422
|
+
}
|
|
1423
|
+
async resolveTermFilter(key, operators) {
|
|
1424
|
+
const filter = {};
|
|
1425
|
+
if (operators.$termType !== undefined)
|
|
1426
|
+
filter.termType = operators.$termType;
|
|
1427
|
+
if (operators.$language !== undefined)
|
|
1428
|
+
filter.language = operators.$language;
|
|
1429
|
+
if (operators.$notLanguage !== undefined)
|
|
1430
|
+
filter.notLanguage = operators.$notLanguage;
|
|
1431
|
+
if (operators.$langMatches !== undefined)
|
|
1432
|
+
filter.langMatches = operators.$langMatches;
|
|
1433
|
+
if (operators.$datatype !== undefined)
|
|
1434
|
+
filter.datatype = await this.resolveDatatypeFilter(operators.$datatype);
|
|
1435
|
+
if (operators.$notDatatype !== undefined)
|
|
1436
|
+
filter.notDatatype = await this.resolveDatatypeFilter(operators.$notDatatype);
|
|
1437
|
+
if (key === 'object') {
|
|
1438
|
+
const textSearches = [];
|
|
1439
|
+
if (operators.$contains !== undefined)
|
|
1440
|
+
textSearches.push({ operator: '$contains', value: operators.$contains });
|
|
1441
|
+
if (operators.$endsWith !== undefined)
|
|
1442
|
+
textSearches.push({ operator: '$endsWith', value: operators.$endsWith });
|
|
1443
|
+
if (textSearches.length > 0)
|
|
1444
|
+
filter.textSearches = textSearches;
|
|
1445
|
+
}
|
|
1446
|
+
return Object.keys(filter).length > 0 ? filter : undefined;
|
|
1447
|
+
}
|
|
1448
|
+
async resolveDatatypeFilter(datatype) {
|
|
1449
|
+
if (datatype.termType !== 'NamedNode') {
|
|
1450
|
+
return { kind: 'unknown' };
|
|
1451
|
+
}
|
|
1452
|
+
if (datatype.value === XSD_STRING) {
|
|
1453
|
+
return { kind: 'xsd-string' };
|
|
1454
|
+
}
|
|
1455
|
+
const id = await this.requireDictionary().find(datatype);
|
|
1456
|
+
return id === undefined ? { kind: 'unknown' } : { kind: 'id', id };
|
|
1457
|
+
}
|
|
1458
|
+
resolveObjectRange(match) {
|
|
1459
|
+
const numericRange = { mode: 'numeric' };
|
|
1460
|
+
const lexicalRange = { mode: 'lexical' };
|
|
1461
|
+
let hasRange = false;
|
|
1462
|
+
let allNumeric = true;
|
|
1463
|
+
for (const [operator, inclusive] of [
|
|
1464
|
+
['$gt', false],
|
|
1465
|
+
['$gte', true],
|
|
1466
|
+
['$lt', false],
|
|
1467
|
+
['$lte', true],
|
|
1468
|
+
]) {
|
|
1469
|
+
const value = match[operator];
|
|
1470
|
+
if (value === undefined)
|
|
1471
|
+
continue;
|
|
1472
|
+
hasRange = true;
|
|
1473
|
+
const numericValue = this.numericValueForPattern(value);
|
|
1474
|
+
const lexicalValue = this.lexicalValueForPattern(value);
|
|
1475
|
+
allNumeric = allNumeric && numericValue !== undefined;
|
|
1476
|
+
if (lexicalValue === undefined)
|
|
1477
|
+
return undefined;
|
|
1478
|
+
if (operator === '$gt' || operator === '$gte') {
|
|
1479
|
+
if (numericValue !== undefined)
|
|
1480
|
+
numericRange.min = numericValue;
|
|
1481
|
+
numericRange.minInclusive = inclusive;
|
|
1482
|
+
lexicalRange.min = lexicalValue;
|
|
1483
|
+
lexicalRange.minInclusive = inclusive;
|
|
1484
|
+
}
|
|
1485
|
+
else {
|
|
1486
|
+
if (numericValue !== undefined)
|
|
1487
|
+
numericRange.max = numericValue;
|
|
1488
|
+
numericRange.maxInclusive = inclusive;
|
|
1489
|
+
lexicalRange.max = lexicalValue;
|
|
1490
|
+
lexicalRange.maxInclusive = inclusive;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (!hasRange)
|
|
1494
|
+
return undefined;
|
|
1495
|
+
return allNumeric ? numericRange : lexicalRange;
|
|
1496
|
+
}
|
|
1497
|
+
numericValueForPattern(value) {
|
|
1498
|
+
if (typeof value === 'number')
|
|
1499
|
+
return Number.isFinite(value) ? value : undefined;
|
|
1500
|
+
if (typeof value === 'string') {
|
|
1501
|
+
const parsed = Number(value);
|
|
1502
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1503
|
+
}
|
|
1504
|
+
if (value.termType !== 'Literal' || !(0, RdfTermSemantics_1.isRdfNumericDatatype)(value.datatype.value))
|
|
1505
|
+
return undefined;
|
|
1506
|
+
const parsed = (0, RdfTermSemantics_1.rdfNumericValue)(value.value);
|
|
1507
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1508
|
+
}
|
|
1509
|
+
lexicalValueForPattern(value) {
|
|
1510
|
+
if (typeof value === 'number')
|
|
1511
|
+
return Number.isFinite(value) ? String(value) : undefined;
|
|
1512
|
+
if (typeof value === 'string')
|
|
1513
|
+
return value;
|
|
1514
|
+
return value.value;
|
|
1515
|
+
}
|
|
1516
|
+
async termOperatorValueId(value) {
|
|
1517
|
+
if (!value || typeof value !== 'object' || !('termType' in value)) {
|
|
1518
|
+
throw new Error('PostgresRdfEngine exact operators only support RDF terms');
|
|
1519
|
+
}
|
|
1520
|
+
return this.requireDictionary().find(value);
|
|
1521
|
+
}
|
|
1522
|
+
compileScanSql(resolved, options) {
|
|
1523
|
+
const builder = new PgSqlBuilder();
|
|
1524
|
+
const conditions = [];
|
|
1525
|
+
const joins = [];
|
|
1526
|
+
const queryPlan = [];
|
|
1527
|
+
const useMembershipSource = shouldUseMembershipSource(resolved);
|
|
1528
|
+
const permutation = this.choosePermutation(resolved);
|
|
1529
|
+
const alias = 'q';
|
|
1530
|
+
this.appendResolvedPatternConditions(resolved, alias, conditions, joins, builder, queryPlan);
|
|
1531
|
+
const order = this.buildOrderClause(options, alias);
|
|
1532
|
+
const pagination = this.buildPagination(options, builder);
|
|
1533
|
+
const from = `${RDF_FACTS_TABLE} ${alias}`;
|
|
1534
|
+
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
1535
|
+
return {
|
|
1536
|
+
sql: `
|
|
1537
|
+
SELECT ${alias}.graph_id, ${alias}.subject_id, ${alias}.predicate_id, ${alias}.object_id
|
|
1538
|
+
FROM ${from}${joins.join('')}
|
|
1539
|
+
${whereClause}
|
|
1540
|
+
${order || ` ORDER BY ${permutation.columns.map((column) => `${alias}.${column}`).join(', ')}`}
|
|
1541
|
+
${pagination.sql}
|
|
1542
|
+
`,
|
|
1543
|
+
params: builder.snapshot(),
|
|
1544
|
+
countSql: `SELECT COUNT(*) AS count FROM ${from}${joins.join('')}${whereClause}`,
|
|
1545
|
+
countParams: builder.snapshot().slice(0, builder.snapshot().length - pagination.paramCount),
|
|
1546
|
+
indexChoice: useMembershipSource ? 'source-membership' : permutation.name,
|
|
1547
|
+
queryPlan: [
|
|
1548
|
+
...(useMembershipSource ? ['Rdf3xMembershipScan'] : [`Rdf3xPermutationScan(${permutation.name})`]),
|
|
1549
|
+
...queryPlan,
|
|
1550
|
+
...(order ? [`Rdf3xJoinOrder(${describeScanOrder(options)})`] : []),
|
|
1551
|
+
...(pagination.sql ? ['Pagination'] : []),
|
|
1552
|
+
],
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
async compileJoinSql(patterns, options) {
|
|
1556
|
+
const resolvedSources = [];
|
|
1557
|
+
for (const [inputIndex, entry] of patterns.entries()) {
|
|
1558
|
+
const resolved = await this.resolvePattern(entry.pattern);
|
|
1559
|
+
const permutation = this.choosePermutation(resolved);
|
|
1560
|
+
const estimateRows = resolved.unresolved ? 0 : await this.estimateResolvedRows(resolved);
|
|
1561
|
+
resolvedSources.push({
|
|
1562
|
+
inputIndex,
|
|
1563
|
+
alias: `q${inputIndex}`,
|
|
1564
|
+
entry,
|
|
1565
|
+
resolved,
|
|
1566
|
+
permutation,
|
|
1567
|
+
estimateRows,
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
const orderedSources = this.orderJoinSources(resolvedSources);
|
|
1571
|
+
const builder = new PgSqlBuilder();
|
|
1572
|
+
const conditions = [];
|
|
1573
|
+
const joins = [];
|
|
1574
|
+
const queryPlan = [
|
|
1575
|
+
`Rdf3xJoinBGP(${patterns.length})`,
|
|
1576
|
+
`Rdf3xJoinOrder(${orderedSources.map((source) => `?${source.inputIndex}:${source.estimateRows}`).join('>')})`,
|
|
1577
|
+
];
|
|
1578
|
+
const variableColumns = new Map();
|
|
1579
|
+
const variableAliases = new Map();
|
|
1580
|
+
const indexChoices = [];
|
|
1581
|
+
for (const [position, source] of orderedSources.entries()) {
|
|
1582
|
+
if (source.resolved.unresolved) {
|
|
1583
|
+
return {
|
|
1584
|
+
sql: '',
|
|
1585
|
+
params: [],
|
|
1586
|
+
countParams: [],
|
|
1587
|
+
indexChoice: 'none',
|
|
1588
|
+
queryPlan,
|
|
1589
|
+
variableAliases,
|
|
1590
|
+
unresolved: source.resolved.unresolved,
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
const alias = source.alias;
|
|
1594
|
+
if (position === 0) {
|
|
1595
|
+
joins.push(`${RDF_FACTS_TABLE} ${alias}`);
|
|
1596
|
+
}
|
|
1597
|
+
else {
|
|
1598
|
+
const mergeConditions = this.mergeJoinConditions(source, variableColumns);
|
|
1599
|
+
joins.push(` JOIN ${RDF_FACTS_TABLE} ${alias} ON ${mergeConditions.length > 0 ? mergeConditions.join(' AND ') : '1 = 1'}`);
|
|
1600
|
+
if (mergeConditions.length > 0) {
|
|
1601
|
+
queryPlan.push(`Rdf3xMergeJoin(${mergeConditions.length})`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
this.appendResolvedPatternConditions(source.resolved, alias, conditions, joins, builder, queryPlan);
|
|
1605
|
+
this.appendPatternEqualityConditions(source.entry.equalities, alias, conditions, queryPlan);
|
|
1606
|
+
indexChoices.push(shouldUseMembershipSource(source.resolved) ? 'source-membership' : source.permutation.name);
|
|
1607
|
+
for (const key of PATTERN_KEYS) {
|
|
1608
|
+
const variableName = source.entry.variables[key];
|
|
1609
|
+
if (!variableName)
|
|
1610
|
+
continue;
|
|
1611
|
+
const column = `${alias}.${TERM_COLUMN[key]}`;
|
|
1612
|
+
if (!variableColumns.has(variableName)) {
|
|
1613
|
+
variableColumns.set(variableName, column);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
const projectVariables = options?.project ?? [...variableColumns.keys()];
|
|
1618
|
+
const projectionColumns = projectVariables.map((variableName) => {
|
|
1619
|
+
const column = variableColumns.get(variableName);
|
|
1620
|
+
if (!column)
|
|
1621
|
+
throw new Error(`Postgres RDF-3X join cannot project unbound variable: ${variableName}`);
|
|
1622
|
+
const alias = `v${variableAliases.size}`;
|
|
1623
|
+
variableAliases.set(variableName, alias);
|
|
1624
|
+
return `${column} AS ${alias}`;
|
|
1625
|
+
});
|
|
1626
|
+
const projection = projectionColumns.length > 0
|
|
1627
|
+
? `${options?.distinct ? 'DISTINCT ' : ''}${projectionColumns.join(', ')}`
|
|
1628
|
+
: `${options?.distinct ? 'DISTINCT ' : ''}1 AS __empty`;
|
|
1629
|
+
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
1630
|
+
const orderClause = this.buildJoinOrderClause(options?.orderBy, variableColumns);
|
|
1631
|
+
const pagination = this.buildPagination(options, builder);
|
|
1632
|
+
const from = joins.join('');
|
|
1633
|
+
const sql = `
|
|
1634
|
+
SELECT ${projection}
|
|
1635
|
+
FROM ${from}${orderClause.joins}
|
|
1636
|
+
${whereClause}
|
|
1637
|
+
${orderClause.orderBy}
|
|
1638
|
+
${pagination.sql}
|
|
1639
|
+
`;
|
|
1640
|
+
return {
|
|
1641
|
+
sql,
|
|
1642
|
+
params: builder.snapshot(),
|
|
1643
|
+
countSql: pagination.sql && options?.countMatchedRows !== false
|
|
1644
|
+
? `SELECT COUNT(*) AS count FROM ${from}${orderClause.joins}${whereClause}`
|
|
1645
|
+
: undefined,
|
|
1646
|
+
countParams: builder.snapshot().slice(0, builder.snapshot().length - pagination.paramCount),
|
|
1647
|
+
indexChoice: `Rdf3xJoinBGP(${indexChoices.join('>')})`,
|
|
1648
|
+
queryPlan: [
|
|
1649
|
+
...queryPlan,
|
|
1650
|
+
...(orderClause.orderBy ? [`Rdf3xJoinOrderBy(${(options?.orderBy ?? []).map((entry) => `${entry.direction ?? 'asc'}:${entry.variable}`).join(',')})`] : []),
|
|
1651
|
+
...(options?.distinct ? [`Rdf3xJoinDistinct(${projectVariables.map((variableName) => `?${variableName}`).join(',')})`] : []),
|
|
1652
|
+
...(pagination.sql ? ['Rdf3xJoinLimit'] : []),
|
|
1653
|
+
sql,
|
|
1654
|
+
],
|
|
1655
|
+
variableAliases,
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
appendResolvedPatternConditions(resolved, alias, conditions, joins, builder, queryPlan) {
|
|
1659
|
+
for (const key of PATTERN_KEYS) {
|
|
1660
|
+
const column = `${alias}.${TERM_COLUMN[key]}`;
|
|
1661
|
+
const id = resolved.ids[key];
|
|
1662
|
+
if (id !== undefined) {
|
|
1663
|
+
conditions.push(`${column} = ${builder.add(id)}`);
|
|
1664
|
+
if (key === 'graph')
|
|
1665
|
+
queryPlan.push('GraphMembershipFilter');
|
|
1666
|
+
}
|
|
1667
|
+
const ids = resolved.idSets[key];
|
|
1668
|
+
if (ids?.length) {
|
|
1669
|
+
conditions.push(`${column} = ANY(${builder.add(ids)}::bigint[])`);
|
|
1670
|
+
queryPlan.push(`TermIn(${key})`);
|
|
1671
|
+
}
|
|
1672
|
+
const excluded = resolved.excludedIdSets[key];
|
|
1673
|
+
if (excluded?.length) {
|
|
1674
|
+
conditions.push(`NOT (${column} = ANY(${builder.add(excluded)}::bigint[]))`);
|
|
1675
|
+
queryPlan.push(`TermNotIn(${key})`);
|
|
1676
|
+
}
|
|
1677
|
+
const filter = resolved.termFilters[key];
|
|
1678
|
+
if (filter) {
|
|
1679
|
+
const filterAlias = `${alias}_${key}_term_filter`;
|
|
1680
|
+
joins.push(` JOIN rdf_terms ${filterAlias} ON ${filterAlias}.id = ${column}`);
|
|
1681
|
+
this.appendTermFilterCondition(key, filterAlias, filter, conditions, builder, queryPlan);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if (resolved.graphPrefix !== undefined) {
|
|
1685
|
+
const graphAlias = `${alias}_graph_prefix`;
|
|
1686
|
+
joins.push(` JOIN rdf_terms ${graphAlias} ON ${graphAlias}.id = ${alias}.graph_id`);
|
|
1687
|
+
conditions.push(`${graphAlias}.kind = ${builder.add('iri')}`);
|
|
1688
|
+
conditions.push(`${graphAlias}.value_head >= ${builder.add((0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix))}`);
|
|
1689
|
+
conditions.push(`${graphAlias}.value_head < ${builder.add(`${(0, RdfTermDictionary_1.rdfTermValueHead)(resolved.graphPrefix)}\uffff`)}`);
|
|
1690
|
+
conditions.push(`${graphAlias}.value >= ${builder.add(resolved.graphPrefix)}`);
|
|
1691
|
+
conditions.push(`${graphAlias}.value < ${builder.add(`${resolved.graphPrefix}\uffff`)}`);
|
|
1692
|
+
queryPlan.push('GraphPrefixMembershipFilter');
|
|
1693
|
+
}
|
|
1694
|
+
if (resolved.objectRange) {
|
|
1695
|
+
const rangeAlias = `${alias}_object_range`;
|
|
1696
|
+
joins.push(` JOIN rdf_terms ${rangeAlias} ON ${rangeAlias}.id = ${alias}.object_id`);
|
|
1697
|
+
this.appendObjectRangeCondition(rangeAlias, resolved.objectRange, conditions, builder, queryPlan);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
appendPatternEqualityConditions(equalities, alias, conditions, queryPlan) {
|
|
1701
|
+
for (const equality of equalities) {
|
|
1702
|
+
conditions.push(`${alias}.${TERM_COLUMN[equality.left]} = ${alias}.${TERM_COLUMN[equality.right]}`);
|
|
1703
|
+
queryPlan.push(`Rdf3xPatternEquality(?${equality.variable}:${equality.left}=${equality.right})`);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
appendTermFilterCondition(key, alias, filter, conditions, builder, queryPlan) {
|
|
1707
|
+
if (filter.termType !== undefined) {
|
|
1708
|
+
this.appendTermTypeCondition(key, alias, filter.termType, conditions, builder);
|
|
1709
|
+
queryPlan.push(`TermType(${key}:${filter.termType})`);
|
|
1710
|
+
}
|
|
1711
|
+
if (filter.language !== undefined) {
|
|
1712
|
+
this.appendLanguageCondition(key, alias, '$language', filter.language, conditions, builder);
|
|
1713
|
+
queryPlan.push(`Language(${key}$language)`);
|
|
1714
|
+
}
|
|
1715
|
+
if (filter.notLanguage !== undefined) {
|
|
1716
|
+
this.appendLanguageCondition(key, alias, '$notLanguage', filter.notLanguage, conditions, builder);
|
|
1717
|
+
queryPlan.push(`Language(${key}$notLanguage)`);
|
|
1718
|
+
}
|
|
1719
|
+
if (filter.langMatches !== undefined) {
|
|
1720
|
+
this.appendLanguageCondition(key, alias, '$langMatches', filter.langMatches, conditions, builder);
|
|
1721
|
+
queryPlan.push(`Language(${key}$langMatches)`);
|
|
1722
|
+
}
|
|
1723
|
+
if (filter.datatype !== undefined) {
|
|
1724
|
+
this.appendDatatypeCondition(key, alias, '$datatype', filter.datatype, conditions, builder);
|
|
1725
|
+
queryPlan.push(`Datatype(${key}$datatype)`);
|
|
1726
|
+
}
|
|
1727
|
+
if (filter.notDatatype !== undefined) {
|
|
1728
|
+
this.appendDatatypeCondition(key, alias, '$notDatatype', filter.notDatatype, conditions, builder);
|
|
1729
|
+
queryPlan.push(`Datatype(${key}$notDatatype)`);
|
|
1730
|
+
}
|
|
1731
|
+
for (const search of filter.textSearches ?? []) {
|
|
1732
|
+
this.appendTextSearchCondition(key, alias, search, conditions, builder, queryPlan);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
appendTermTypeCondition(key, alias, termType, conditions, builder) {
|
|
1736
|
+
const possibleKinds = termKindsForPatternKey(key);
|
|
1737
|
+
if (termType === 'numeric') {
|
|
1738
|
+
conditions.push(possibleKinds.includes('literal')
|
|
1739
|
+
? `${alias}.kind = 'literal' AND ${alias}.numeric_value IS NOT NULL`
|
|
1740
|
+
: '1 = 0');
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (!possibleKinds.includes(termType)) {
|
|
1744
|
+
conditions.push('1 = 0');
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
conditions.push(`${alias}.kind = ${builder.add(termType)}`);
|
|
1748
|
+
}
|
|
1749
|
+
appendLanguageCondition(key, alias, operator, language, conditions, builder) {
|
|
1750
|
+
if (!termKindsForPatternKey(key).includes('literal')) {
|
|
1751
|
+
conditions.push('1 = 0');
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
if (operator === '$language') {
|
|
1755
|
+
conditions.push(`${alias}.kind = 'literal' AND COALESCE(${alias}.lang, '') = ${builder.add(language)}`);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
if (operator === '$notLanguage') {
|
|
1759
|
+
conditions.push(`${alias}.kind = 'literal' AND COALESCE(${alias}.lang, '') != ${builder.add(language)}`);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
if (language === '*') {
|
|
1763
|
+
conditions.push(`${alias}.kind = 'literal' AND ${alias}.lang IS NOT NULL AND ${alias}.lang != ''`);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
conditions.push(`${alias}.kind = 'literal'
|
|
1767
|
+
AND (lower(${alias}.lang) = lower(${builder.add(language)}) OR lower(${alias}.lang) LIKE lower(${builder.add(`${escapeLikePattern(language)}-%`)}) ESCAPE '\\')`);
|
|
1768
|
+
}
|
|
1769
|
+
appendDatatypeCondition(key, alias, operator, datatype, conditions, builder) {
|
|
1770
|
+
if (!termKindsForPatternKey(key).includes('literal')) {
|
|
1771
|
+
conditions.push('1 = 0');
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (datatype.kind === 'xsd-string') {
|
|
1775
|
+
conditions.push(operator === '$datatype'
|
|
1776
|
+
? `${alias}.kind = 'literal' AND ${alias}.lang IS NULL AND ${alias}.datatype_id IS NULL`
|
|
1777
|
+
: `${alias}.kind = 'literal' AND NOT (${alias}.lang IS NULL AND ${alias}.datatype_id IS NULL)`);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
if (datatype.kind === 'unknown') {
|
|
1781
|
+
conditions.push(operator === '$datatype' ? '1 = 0' : `${alias}.kind = 'literal'`);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
conditions.push(operator === '$datatype'
|
|
1785
|
+
? `${alias}.kind = 'literal' AND ${alias}.datatype_id = ${builder.add(datatype.id)}`
|
|
1786
|
+
: `${alias}.kind = 'literal' AND (${alias}.datatype_id IS NULL OR ${alias}.datatype_id != ${builder.add(datatype.id)})`);
|
|
1787
|
+
}
|
|
1788
|
+
appendTextSearchCondition(key, alias, search, conditions, builder, queryPlan) {
|
|
1789
|
+
const kinds = termKindsForPatternKey(key);
|
|
1790
|
+
const kindArray = builder.add(kinds);
|
|
1791
|
+
const normalized = search.value.toLowerCase();
|
|
1792
|
+
switch (search.operator) {
|
|
1793
|
+
case '$contains':
|
|
1794
|
+
conditions.push(`${alias}.kind = ANY(${kindArray}::text[])
|
|
1795
|
+
AND ${alias}.normalized_text LIKE ${builder.add(`%${escapeLikePattern(normalized)}%`)} ESCAPE '\\'
|
|
1796
|
+
AND strpos(${alias}.value, ${builder.add(search.value)}) > 0`);
|
|
1797
|
+
queryPlan.push(`TextSearch(${key}$contains)`);
|
|
1798
|
+
return;
|
|
1799
|
+
case '$endsWith':
|
|
1800
|
+
conditions.push(`${alias}.kind = ANY(${kindArray}::text[])
|
|
1801
|
+
AND ${alias}.normalized_text LIKE ${builder.add(`%${escapeLikePattern(normalized)}`)} ESCAPE '\\'
|
|
1802
|
+
AND right(${alias}.value, length(${builder.add(search.value)})) = ${builder.add(search.value)}`);
|
|
1803
|
+
queryPlan.push(`TextSearch(${key}$endsWith)`);
|
|
1804
|
+
return;
|
|
1805
|
+
default: {
|
|
1806
|
+
const exhaustive = search.operator;
|
|
1807
|
+
throw new Error(`Unsupported PostgreSQL RDF-3X text search operator: ${exhaustive}`);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
appendObjectRangeCondition(alias, range, conditions, builder, queryPlan) {
|
|
1812
|
+
if (range.mode === 'numeric') {
|
|
1813
|
+
conditions.push(`${alias}.kind = 'literal'`);
|
|
1814
|
+
conditions.push(`${alias}.numeric_value IS NOT NULL`);
|
|
1815
|
+
if (range.min !== undefined)
|
|
1816
|
+
conditions.push(`${alias}.numeric_value ${range.minInclusive ? '>=' : '>'} ${builder.add(range.min)}`);
|
|
1817
|
+
if (range.max !== undefined)
|
|
1818
|
+
conditions.push(`${alias}.numeric_value ${range.maxInclusive ? '<=' : '<'} ${builder.add(range.max)}`);
|
|
1819
|
+
queryPlan.push(`NumericRange(object${rangeSuffix(range)})`);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
conditions.push(`${alias}.kind = ANY(${builder.add(OBJECT_RANGE_KINDS)}::text[])`);
|
|
1823
|
+
if (range.min !== undefined)
|
|
1824
|
+
conditions.push(`${alias}.value ${range.minInclusive ? '>=' : '>'} ${builder.add(range.min)}`);
|
|
1825
|
+
if (range.max !== undefined)
|
|
1826
|
+
conditions.push(`${alias}.value ${range.maxInclusive ? '<=' : '<'} ${builder.add(range.max)}`);
|
|
1827
|
+
queryPlan.push(`LexicalRange(object${rangeSuffix(range)})`);
|
|
1828
|
+
}
|
|
1829
|
+
mergeJoinConditions(source, variableColumns) {
|
|
1830
|
+
const conditions = [];
|
|
1831
|
+
for (const key of PATTERN_KEYS) {
|
|
1832
|
+
const variableName = source.entry.variables[key];
|
|
1833
|
+
if (!variableName)
|
|
1834
|
+
continue;
|
|
1835
|
+
const existing = variableColumns.get(variableName);
|
|
1836
|
+
if (existing) {
|
|
1837
|
+
conditions.push(`${existing} = ${source.alias}.${TERM_COLUMN[key]}`);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return conditions;
|
|
1841
|
+
}
|
|
1842
|
+
choosePermutation(resolved) {
|
|
1843
|
+
const has = (key) => resolved.ids[key] !== undefined || Boolean(resolved.idSets[key]?.length);
|
|
1844
|
+
const hasObjectConstraint = has('object') || Boolean(resolved.objectRange) || Boolean(resolved.termFilters.object);
|
|
1845
|
+
if (has('subject') && has('predicate'))
|
|
1846
|
+
return this.permutation('SPO');
|
|
1847
|
+
if (has('subject') && hasObjectConstraint)
|
|
1848
|
+
return this.permutation('SOP');
|
|
1849
|
+
if (has('predicate') && has('subject'))
|
|
1850
|
+
return this.permutation('PSO');
|
|
1851
|
+
if (has('predicate') && hasObjectConstraint)
|
|
1852
|
+
return this.permutation('POS');
|
|
1853
|
+
if (hasObjectConstraint && has('subject'))
|
|
1854
|
+
return this.permutation('OSP');
|
|
1855
|
+
if (hasObjectConstraint && has('predicate'))
|
|
1856
|
+
return this.permutation('OPS');
|
|
1857
|
+
if (has('subject'))
|
|
1858
|
+
return this.permutation('SPO');
|
|
1859
|
+
if (has('predicate'))
|
|
1860
|
+
return this.permutation('PSO');
|
|
1861
|
+
if (hasObjectConstraint)
|
|
1862
|
+
return this.permutation('OSP');
|
|
1863
|
+
return this.permutation('SPO');
|
|
1864
|
+
}
|
|
1865
|
+
permutation(name) {
|
|
1866
|
+
const permutation = PERMUTATIONS.find((candidate) => candidate.name === name);
|
|
1867
|
+
if (!permutation)
|
|
1868
|
+
throw new Error(`Unknown PostgreSQL RDF-3X permutation: ${name}`);
|
|
1869
|
+
return permutation;
|
|
1870
|
+
}
|
|
1871
|
+
async estimateResolvedRows(resolved) {
|
|
1872
|
+
const compiled = this.compileScanSql(resolved, { limit: 0 });
|
|
1873
|
+
return this.scalarCount(compiled.countSql, compiled.countParams);
|
|
1874
|
+
}
|
|
1875
|
+
orderJoinSources(sources) {
|
|
1876
|
+
const remaining = [...sources];
|
|
1877
|
+
const selected = [];
|
|
1878
|
+
const selectedVariables = new Set();
|
|
1879
|
+
while (remaining.length > 0) {
|
|
1880
|
+
const hasSelectedVariables = selectedVariables.size > 0;
|
|
1881
|
+
remaining.sort((left, right) => {
|
|
1882
|
+
const leftConnected = !hasSelectedVariables || joinSourceVariables(left).some((variableName) => selectedVariables.has(variableName));
|
|
1883
|
+
const rightConnected = !hasSelectedVariables || joinSourceVariables(right).some((variableName) => selectedVariables.has(variableName));
|
|
1884
|
+
return Number(rightConnected) - Number(leftConnected)
|
|
1885
|
+
|| left.estimateRows - right.estimateRows
|
|
1886
|
+
|| left.inputIndex - right.inputIndex;
|
|
1887
|
+
});
|
|
1888
|
+
const [next] = remaining.splice(0, 1);
|
|
1889
|
+
selected.push(next);
|
|
1890
|
+
for (const variableName of joinSourceVariables(next)) {
|
|
1891
|
+
selectedVariables.add(variableName);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
return selected;
|
|
1895
|
+
}
|
|
1896
|
+
buildOrderClause(options, alias) {
|
|
1897
|
+
if (!options?.order || options.order.length === 0)
|
|
1898
|
+
return '';
|
|
1899
|
+
const directions = 'orderDirections' in options && Array.isArray(options.orderDirections)
|
|
1900
|
+
? options.orderDirections
|
|
1901
|
+
: options.order.map(() => (options.reverse ? 'desc' : 'asc'));
|
|
1902
|
+
return ` ORDER BY ${options.order.map((key, index) => `${alias}.${TERM_COLUMN[key]} ${(directions[index] ?? 'asc').toUpperCase()}`).join(', ')}`;
|
|
1903
|
+
}
|
|
1904
|
+
buildJoinOrderClause(orderBy, variableColumns) {
|
|
1905
|
+
if (!orderBy || orderBy.length === 0)
|
|
1906
|
+
return { joins: '', orderBy: '' };
|
|
1907
|
+
const joins = [];
|
|
1908
|
+
const orders = [];
|
|
1909
|
+
for (const [index, entry] of orderBy.entries()) {
|
|
1910
|
+
const column = variableColumns.get(entry.variable);
|
|
1911
|
+
if (!column)
|
|
1912
|
+
throw new Error(`Postgres RDF-3X join cannot order by unbound variable: ${entry.variable}`);
|
|
1913
|
+
const alias = `join_order_t${index}`;
|
|
1914
|
+
joins.push(` JOIN rdf_terms ${alias} ON ${alias}.id = ${column}`);
|
|
1915
|
+
orders.push(`${alias}.value ${entry.direction === 'desc' ? 'DESC' : 'ASC'}`);
|
|
1916
|
+
}
|
|
1917
|
+
return { joins: joins.join(''), orderBy: ` ORDER BY ${orders.join(', ')}` };
|
|
1918
|
+
}
|
|
1919
|
+
buildPagination(options, builder) {
|
|
1920
|
+
let sql = '';
|
|
1921
|
+
let paramCount = 0;
|
|
1922
|
+
if (options?.limit !== undefined) {
|
|
1923
|
+
sql += ` LIMIT ${builder.add(Math.max(0, options.limit))}`;
|
|
1924
|
+
paramCount++;
|
|
1925
|
+
}
|
|
1926
|
+
if (options?.offset !== undefined) {
|
|
1927
|
+
if (options.limit === undefined) {
|
|
1928
|
+
sql += ' LIMIT ALL';
|
|
1929
|
+
}
|
|
1930
|
+
sql += ` OFFSET ${builder.add(Math.max(0, options.offset))}`;
|
|
1931
|
+
paramCount++;
|
|
1932
|
+
}
|
|
1933
|
+
return { sql, paramCount };
|
|
1934
|
+
}
|
|
1935
|
+
async rowsToQuads(rows) {
|
|
1936
|
+
const ids = rows.flatMap((row) => [row.graph_id, row.subject_id, row.predicate_id, row.object_id]);
|
|
1937
|
+
const terms = await this.requireDictionary().rowsForIds(ids);
|
|
1938
|
+
return rows.map((row) => quad(this.requiredTerm(terms, row.subject_id), this.requiredTerm(terms, row.predicate_id), this.requiredTerm(terms, row.object_id), this.requiredTerm(terms, row.graph_id)));
|
|
1939
|
+
}
|
|
1940
|
+
async joinRowsToBindings(rows, variableAliases, aggregateAliases, aggregateTypes) {
|
|
1941
|
+
const termIds = rows.flatMap((row) => [...variableAliases.values()]
|
|
1942
|
+
.map((alias) => row[alias])
|
|
1943
|
+
.map(pgInteger)
|
|
1944
|
+
.filter((value) => value !== undefined));
|
|
1945
|
+
const terms = await this.requireDictionary().rowsForIds(termIds);
|
|
1946
|
+
return rows.map((row) => {
|
|
1947
|
+
const binding = {};
|
|
1948
|
+
for (const [variableName, alias] of variableAliases) {
|
|
1949
|
+
const id = pgInteger(row[alias]);
|
|
1950
|
+
if (id !== undefined)
|
|
1951
|
+
binding[variableName] = this.requiredTerm(terms, id);
|
|
1952
|
+
}
|
|
1953
|
+
for (const [variableName, alias] of aggregateAliases ?? []) {
|
|
1954
|
+
const value = pgInteger(row[alias]);
|
|
1955
|
+
if (value !== undefined) {
|
|
1956
|
+
binding[variableName] = n3_1.DataFactory.literal(String(value), n3_1.DataFactory.namedNode(aggregateTypes?.get(variableName) === 'decimal' ? XSD_DECIMAL : XSD_INTEGER));
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return binding;
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
indexMetrics(indexChoice, matchedRows, returnedRows, start, queryPlan) {
|
|
1963
|
+
return {
|
|
1964
|
+
engine: 'solid-rdf',
|
|
1965
|
+
indexChoice,
|
|
1966
|
+
matchedRows,
|
|
1967
|
+
returnedRows,
|
|
1968
|
+
durationMs: Date.now() - start,
|
|
1969
|
+
queryPlan,
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
localMetrics(start, scannedRows, joinedRows, returnedRows, indexChoices, plan, filtersPushedDown = 0) {
|
|
1973
|
+
return {
|
|
1974
|
+
engine: 'solid-rdf',
|
|
1975
|
+
plan,
|
|
1976
|
+
scannedRows,
|
|
1977
|
+
joinedRows,
|
|
1978
|
+
returnedRows,
|
|
1979
|
+
durationMs: Date.now() - start,
|
|
1980
|
+
indexChoices,
|
|
1981
|
+
cardinalityEstimates: 0,
|
|
1982
|
+
distinctCardinalityEstimates: 0,
|
|
1983
|
+
searchCardinalityEstimates: 0,
|
|
1984
|
+
filtersApplied: 0,
|
|
1985
|
+
filtersPushedDown,
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
async rebuildCacheFromStore() {
|
|
1989
|
+
if (this.cacheEngine) {
|
|
1990
|
+
await this.cacheEngine.close();
|
|
1991
|
+
this.cacheEngine = null;
|
|
1992
|
+
}
|
|
1993
|
+
if (this.cacheDir) {
|
|
1994
|
+
await (0, promises_1.rm)(this.cacheDir, { recursive: true, force: true });
|
|
1995
|
+
}
|
|
1996
|
+
this.cacheDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'xpod-pg-rdf-cache-'));
|
|
1997
|
+
this.cachePath = (0, node_path_1.join)(this.cacheDir, 'rdf-cache.sqlite');
|
|
1998
|
+
this.cacheEngine = new SolidRdfEngine_1.SolidRdfEngine({
|
|
1999
|
+
index: { path: this.cachePath },
|
|
2000
|
+
autoOpen: true,
|
|
2001
|
+
});
|
|
2002
|
+
const sources = await this.requireExecutor().query('SELECT * FROM rdf_sources ORDER BY id');
|
|
2003
|
+
const nullSourceRows = await this.requireExecutor().query('SELECT * FROM rdf_quads WHERE source_file_id IS NULL ORDER BY graph_id, subject_id, predicate_id, object_id');
|
|
2004
|
+
await this.loadCacheQuads(nullSourceRows, undefined);
|
|
2005
|
+
for (const source of sources) {
|
|
2006
|
+
const quadRows = await this.requireExecutor().query('SELECT * FROM rdf_quads WHERE source_file_id = $1 ORDER BY graph_id, subject_id, predicate_id, object_id', [source.id]);
|
|
2007
|
+
await this.loadCacheQuads(quadRows, source);
|
|
2008
|
+
}
|
|
2009
|
+
await this.requireCache().refreshDerivedIndexes();
|
|
2010
|
+
this.factsDataVersion = await this.readFactsDataVersion();
|
|
2011
|
+
this.cacheDirty = false;
|
|
2012
|
+
}
|
|
2013
|
+
async loadCacheQuads(rows, source) {
|
|
2014
|
+
if (rows.length === 0) {
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
const dictionary = this.requireDictionary();
|
|
2018
|
+
const termIds = Array.from(new Set(rows.flatMap((row) => [row.graph_id, row.subject_id, row.predicate_id, row.object_id])));
|
|
2019
|
+
const terms = await dictionary.rowsForIds(termIds);
|
|
2020
|
+
const quads = rows.map((row) => quad(this.requiredTerm(terms, row.subject_id), this.requiredTerm(terms, row.predicate_id), this.requiredTerm(terms, row.object_id), this.requiredTerm(terms, row.graph_id)));
|
|
2021
|
+
const sourceInput = source ? this.sourceRowToInput(source) : undefined;
|
|
2022
|
+
if (sourceInput) {
|
|
2023
|
+
this.requireCache().replaceSource(quads, sourceInput);
|
|
2024
|
+
}
|
|
2025
|
+
else {
|
|
2026
|
+
this.requireCache().put(quads);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
async insertQuads(executor, dictionary, quads, sourceId, sourceLineNo) {
|
|
2030
|
+
for (const quadValue of quads) {
|
|
2031
|
+
const graphId = await dictionary.getOrCreate(quadValue.graph);
|
|
2032
|
+
const subjectId = await dictionary.getOrCreate(quadValue.subject);
|
|
2033
|
+
const predicateId = await dictionary.getOrCreate(quadValue.predicate);
|
|
2034
|
+
const objectId = await dictionary.getOrCreate(quadValue.object);
|
|
2035
|
+
await executor.exec(`
|
|
2036
|
+
INSERT INTO rdf_quads (
|
|
2037
|
+
graph_id,
|
|
2038
|
+
subject_id,
|
|
2039
|
+
predicate_id,
|
|
2040
|
+
object_id,
|
|
2041
|
+
source_file_id,
|
|
2042
|
+
source_line_no
|
|
2043
|
+
)
|
|
2044
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
2045
|
+
ON CONFLICT (graph_id, subject_id, predicate_id, object_id)
|
|
2046
|
+
DO UPDATE SET
|
|
2047
|
+
source_file_id = EXCLUDED.source_file_id,
|
|
2048
|
+
source_line_no = EXCLUDED.source_line_no
|
|
2049
|
+
`, [graphId, subjectId, predicateId, objectId, sourceId, sourceLineNo]);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
async deleteExactQuad(executor, dictionary, value) {
|
|
2053
|
+
const graphId = await dictionary.find(value.graph);
|
|
2054
|
+
const subjectId = await dictionary.find(value.subject);
|
|
2055
|
+
const predicateId = await dictionary.find(value.predicate);
|
|
2056
|
+
const objectId = await dictionary.find(value.object);
|
|
2057
|
+
if (graphId === undefined || subjectId === undefined || predicateId === undefined || objectId === undefined) {
|
|
2058
|
+
return 0;
|
|
2059
|
+
}
|
|
2060
|
+
const result = await executor.query(`
|
|
2061
|
+
DELETE FROM rdf_quads
|
|
2062
|
+
WHERE graph_id = $1
|
|
2063
|
+
AND subject_id = $2
|
|
2064
|
+
AND predicate_id = $3
|
|
2065
|
+
AND object_id = $4
|
|
2066
|
+
RETURNING 1 AS one
|
|
2067
|
+
`, [graphId, subjectId, predicateId, objectId]);
|
|
2068
|
+
return result.length;
|
|
2069
|
+
}
|
|
2070
|
+
sourceRowToInput(row) {
|
|
2071
|
+
return {
|
|
2072
|
+
source: row.source,
|
|
2073
|
+
workspace: row.workspace,
|
|
2074
|
+
localPath: row.local_path !== null ? row.local_path : undefined,
|
|
2075
|
+
contentType: row.content_type !== null ? row.content_type : undefined,
|
|
2076
|
+
sourceVersion: row.source_version !== null ? row.source_version : undefined,
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
async upsertSource(source, executor = this.requireExecutor()) {
|
|
2080
|
+
const row = await executor.query(`
|
|
2081
|
+
INSERT INTO rdf_sources (
|
|
2082
|
+
source,
|
|
2083
|
+
workspace,
|
|
2084
|
+
local_path,
|
|
2085
|
+
content_type,
|
|
2086
|
+
source_version,
|
|
2087
|
+
last_indexed_at
|
|
2088
|
+
)
|
|
2089
|
+
VALUES ($1, $2, $3, $4, $5, NOW())
|
|
2090
|
+
ON CONFLICT (source) DO UPDATE
|
|
2091
|
+
SET
|
|
2092
|
+
workspace = EXCLUDED.workspace,
|
|
2093
|
+
local_path = EXCLUDED.local_path,
|
|
2094
|
+
content_type = EXCLUDED.content_type,
|
|
2095
|
+
source_version = EXCLUDED.source_version,
|
|
2096
|
+
last_indexed_at = NOW()
|
|
2097
|
+
RETURNING id
|
|
2098
|
+
`, [
|
|
2099
|
+
source.source,
|
|
2100
|
+
source.workspace,
|
|
2101
|
+
source.localPath ?? null,
|
|
2102
|
+
source.contentType ?? null,
|
|
2103
|
+
source.sourceVersion ?? null,
|
|
2104
|
+
]);
|
|
2105
|
+
const id = row[0]?.id;
|
|
2106
|
+
if (id === undefined) {
|
|
2107
|
+
throw new Error(`Failed to upsert RDF source ${source.source}`);
|
|
2108
|
+
}
|
|
2109
|
+
return id;
|
|
2110
|
+
}
|
|
2111
|
+
async findSourceRow(source, executor = this.requireExecutor()) {
|
|
2112
|
+
const rows = await executor.query('SELECT * FROM rdf_sources WHERE source = $1', [source]);
|
|
2113
|
+
return rows[0];
|
|
2114
|
+
}
|
|
2115
|
+
async bumpFactsDataVersion(executor = this.requireExecutor()) {
|
|
2116
|
+
await executor.exec(`
|
|
2117
|
+
UPDATE rdf_index_metadata
|
|
2118
|
+
SET value = (COALESCE(NULLIF(value, ''), '0')::bigint + 1)::text
|
|
2119
|
+
WHERE key = 'data_version'
|
|
2120
|
+
`);
|
|
2121
|
+
}
|
|
2122
|
+
async readFactsDataVersion() {
|
|
2123
|
+
const row = await this.requireExecutor().query("SELECT value FROM rdf_index_metadata WHERE key = 'data_version'");
|
|
2124
|
+
return Number(row[0]?.value ?? 0) || 0;
|
|
2125
|
+
}
|
|
2126
|
+
async readRdf3xFactsDataVersion() {
|
|
2127
|
+
try {
|
|
2128
|
+
const row = await this.requireExecutor().query("SELECT value FROM rdf3x_metadata WHERE key = 'facts_data_version'");
|
|
2129
|
+
return Number(row[0]?.value ?? 0) || 0;
|
|
2130
|
+
}
|
|
2131
|
+
catch {
|
|
2132
|
+
return 0;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
async ensureReady() {
|
|
2136
|
+
await this.open();
|
|
2137
|
+
const currentVersion = await this.readFactsDataVersion();
|
|
2138
|
+
if (currentVersion !== this.factsDataVersion) {
|
|
2139
|
+
this.factsDataVersion = currentVersion;
|
|
2140
|
+
this.markDerivedDirty();
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
async ensureCacheReady() {
|
|
2144
|
+
await this.ensureReady();
|
|
2145
|
+
if (this.cacheDirty || !this.cacheEngine) {
|
|
2146
|
+
await this.rebuildCacheFromStore();
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
markDerivedDirty() {
|
|
2150
|
+
this.rdf3xDirty = true;
|
|
2151
|
+
this.cacheDirty = true;
|
|
2152
|
+
}
|
|
2153
|
+
async scalarCount(sql, params = []) {
|
|
2154
|
+
const row = await this.requireExecutor().query(sql, params);
|
|
2155
|
+
return Number(row[0]?.count ?? 0) || 0;
|
|
2156
|
+
}
|
|
2157
|
+
async factsStats() {
|
|
2158
|
+
const [termCount, quadCount, sourceCount, graphCount] = await Promise.all([
|
|
2159
|
+
this.scalarCount('SELECT COUNT(*) AS count FROM rdf_terms'),
|
|
2160
|
+
this.scalarCount(`SELECT COUNT(*) AS count FROM ${RDF_FACTS_TABLE}`),
|
|
2161
|
+
this.scalarCount('SELECT COUNT(*) AS count FROM rdf_sources'),
|
|
2162
|
+
this.scalarCount(`SELECT COUNT(DISTINCT graph_id) AS count FROM ${RDF_FACTS_TABLE}`),
|
|
2163
|
+
]);
|
|
2164
|
+
const spaceObjects = await this.collectSpaceObjects(false);
|
|
2165
|
+
const databaseBytes = spaceObjects.reduce((sum, object) => sum + object.bytes, 0);
|
|
2166
|
+
return {
|
|
2167
|
+
termCount,
|
|
2168
|
+
quadCount,
|
|
2169
|
+
sourceCount,
|
|
2170
|
+
graphCount,
|
|
2171
|
+
databaseBytes,
|
|
2172
|
+
tableBytes: sumSpaceObjects(spaceObjects, 'table'),
|
|
2173
|
+
indexBytes: sumSpaceObjects(spaceObjects, 'index'),
|
|
2174
|
+
spaceObjects,
|
|
2175
|
+
serializedTermTextBytes: await this.scalarCount('SELECT COALESCE(SUM(length(value)), 0) AS count FROM rdf_terms'),
|
|
2176
|
+
literalDatatypeDistribution: [],
|
|
2177
|
+
cardinalityDistributions: {
|
|
2178
|
+
graphs: [],
|
|
2179
|
+
predicates: [],
|
|
2180
|
+
predicateObjects: [],
|
|
2181
|
+
subjectPredicates: [],
|
|
2182
|
+
},
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
async rdf3xStats() {
|
|
2186
|
+
const spaceObjects = await this.collectSpaceObjects(true);
|
|
2187
|
+
const databaseBytes = spaceObjects.reduce((sum, object) => sum + object.bytes, 0);
|
|
2188
|
+
const uniqueTriples = await this.scalarCount(`
|
|
2189
|
+
SELECT COUNT(*) AS count
|
|
2190
|
+
FROM (
|
|
2191
|
+
SELECT DISTINCT subject_id, predicate_id, object_id
|
|
2192
|
+
FROM ${RDF_FACTS_TABLE}
|
|
2193
|
+
) distinct_triples
|
|
2194
|
+
`);
|
|
2195
|
+
return {
|
|
2196
|
+
uniqueTriples,
|
|
2197
|
+
membershipCount: await this.scalarCount(`SELECT COUNT(*) AS count FROM ${RDF_FACTS_TABLE}`),
|
|
2198
|
+
graphCount: await this.scalarCount(`SELECT COUNT(*) AS count FROM ${Rdf3xSchema_1.RDF3X_GRAPH_PROJECTION_TABLE}`),
|
|
2199
|
+
factsDataVersion: await this.readRdf3xFactsDataVersion(),
|
|
2200
|
+
permutationRows: Object.fromEntries(PERMUTATIONS.map((permutation) => [permutation.name, uniqueTriples])),
|
|
2201
|
+
pairProjectionRows: Object.fromEntries(await Promise.all(PAIR_PROJECTIONS.map(async (projection) => [
|
|
2202
|
+
projection.name,
|
|
2203
|
+
await this.scalarCount(`SELECT COUNT(*) AS count FROM ${projection.table}`),
|
|
2204
|
+
]))),
|
|
2205
|
+
termProjectionRows: Object.fromEntries(await Promise.all(TERM_PROJECTIONS.map(async (projection) => [
|
|
2206
|
+
projection.name,
|
|
2207
|
+
await this.scalarCount(`SELECT COUNT(*) AS count FROM ${projection.table}`),
|
|
2208
|
+
]))),
|
|
2209
|
+
databaseBytes,
|
|
2210
|
+
tableBytes: sumSpaceObjects(spaceObjects, 'table'),
|
|
2211
|
+
indexBytes: sumSpaceObjects(spaceObjects, 'index'),
|
|
2212
|
+
spaceObjects,
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
async collectSpaceObjects(derived) {
|
|
2216
|
+
try {
|
|
2217
|
+
const rows = await this.requireExecutor().query(`
|
|
2218
|
+
SELECT
|
|
2219
|
+
rel.relname AS name,
|
|
2220
|
+
CASE WHEN rel.relkind = 'i' THEN 'index' ELSE 'table' END AS kind,
|
|
2221
|
+
tbl.relname AS table_name,
|
|
2222
|
+
pg_total_relation_size(rel.oid) AS bytes
|
|
2223
|
+
FROM pg_class rel
|
|
2224
|
+
LEFT JOIN pg_index idx ON idx.indexrelid = rel.oid
|
|
2225
|
+
LEFT JOIN pg_class tbl ON tbl.oid = idx.indrelid
|
|
2226
|
+
WHERE rel.relkind IN ('r', 'i')
|
|
2227
|
+
AND ${derived
|
|
2228
|
+
? "(rel.relname LIKE 'rdf3x_%' OR tbl.relname LIKE 'rdf3x_%')"
|
|
2229
|
+
: "(rel.relname LIKE 'rdf_%' OR tbl.relname LIKE 'rdf_%') AND rel.relname NOT LIKE 'rdf3x_%' AND COALESCE(tbl.relname, '') NOT LIKE 'rdf3x_%'"}
|
|
2230
|
+
ORDER BY rel.relname
|
|
2231
|
+
`);
|
|
2232
|
+
return rows.map((row) => ({
|
|
2233
|
+
name: row.name,
|
|
2234
|
+
kind: row.kind,
|
|
2235
|
+
...(row.table_name && row.table_name !== row.name ? { tableName: row.table_name } : {}),
|
|
2236
|
+
bytes: Number(row.bytes ?? 0),
|
|
2237
|
+
pages: 0,
|
|
2238
|
+
estimated: false,
|
|
2239
|
+
}));
|
|
2240
|
+
}
|
|
2241
|
+
catch {
|
|
2242
|
+
const tables = derived
|
|
2243
|
+
? ['rdf3x_metadata', Rdf3xSchema_1.RDF3X_GRAPH_PROJECTION_TABLE, ...PAIR_PROJECTIONS.map((projection) => projection.table), ...TERM_PROJECTIONS.map((projection) => projection.table)]
|
|
2244
|
+
: ['rdf_terms', 'rdf_sources', RDF_FACTS_TABLE, 'rdf_index_metadata'];
|
|
2245
|
+
const rows = await Promise.all(tables.map(async (table) => ({
|
|
2246
|
+
name: table,
|
|
2247
|
+
kind: 'table',
|
|
2248
|
+
rows: await this.scalarCount(`SELECT COUNT(*) AS count FROM ${table}`).catch(() => 0),
|
|
2249
|
+
})));
|
|
2250
|
+
return rows.map((row) => ({
|
|
2251
|
+
name: row.name,
|
|
2252
|
+
kind: row.kind,
|
|
2253
|
+
bytes: Math.max(4096, row.rows * 128),
|
|
2254
|
+
pages: Math.max(1, Math.ceil(Math.max(4096, row.rows * 128) / 4096)),
|
|
2255
|
+
estimated: true,
|
|
2256
|
+
}));
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
requireExecutor() {
|
|
2260
|
+
if (!this.executor) {
|
|
2261
|
+
throw new Error('PostgresRdfEngine is not open');
|
|
2262
|
+
}
|
|
2263
|
+
return this.executor;
|
|
2264
|
+
}
|
|
2265
|
+
requireDictionary() {
|
|
2266
|
+
if (!this.termDictionary) {
|
|
2267
|
+
throw new Error('PostgresRdfEngine term dictionary is not initialized');
|
|
2268
|
+
}
|
|
2269
|
+
return this.termDictionary;
|
|
2270
|
+
}
|
|
2271
|
+
requireCache() {
|
|
2272
|
+
if (!this.cacheEngine) {
|
|
2273
|
+
throw new Error('PostgresRdfEngine cache is not initialized');
|
|
2274
|
+
}
|
|
2275
|
+
return this.cacheEngine;
|
|
2276
|
+
}
|
|
2277
|
+
requiredTerm(termMap, id) {
|
|
2278
|
+
const term = termMap.get(id);
|
|
2279
|
+
if (!term) {
|
|
2280
|
+
throw new Error(`RDF term not found while reading quad row: ${id}`);
|
|
2281
|
+
}
|
|
2282
|
+
return term;
|
|
2283
|
+
}
|
|
2284
|
+
async openExecutor() {
|
|
2285
|
+
if (this.executor) {
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
if (this.pgOptions.pool) {
|
|
2289
|
+
this.pgPool = this.pgOptions.pool;
|
|
2290
|
+
this.sharedPoolConfig = null;
|
|
2291
|
+
this.executor = new PgPoolExecutor(this.pgPool);
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
if (this.pgOptions.driver === 'pglite') {
|
|
2295
|
+
this.pglite = new pglite_1.PGlite(this.pgOptions.dataDir);
|
|
2296
|
+
await this.pglite.waitReady;
|
|
2297
|
+
this.executor = new PgliteExecutor(this.pglite);
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
this.sharedPoolConfig = {
|
|
2301
|
+
connectionString: this.pgOptions.connectionString,
|
|
2302
|
+
host: this.pgOptions.host,
|
|
2303
|
+
port: this.pgOptions.port,
|
|
2304
|
+
database: this.pgOptions.database,
|
|
2305
|
+
user: this.pgOptions.user,
|
|
2306
|
+
password: this.pgOptions.password,
|
|
2307
|
+
};
|
|
2308
|
+
this.pgPool = (0, PostgresPoolManager_1.getSharedPool)(this.sharedPoolConfig);
|
|
2309
|
+
this.executor = new PgPoolExecutor(this.pgPool);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
exports.PostgresRdfEngine = PostgresRdfEngine;
|
|
2313
|
+
function restoreRow(row) {
|
|
2314
|
+
if (!row || typeof row !== 'object') {
|
|
2315
|
+
return row;
|
|
2316
|
+
}
|
|
2317
|
+
const restored = {};
|
|
2318
|
+
for (const [key, value] of Object.entries(row)) {
|
|
2319
|
+
restored[key] = restoreValue(key, value);
|
|
2320
|
+
}
|
|
2321
|
+
return restored;
|
|
2322
|
+
}
|
|
2323
|
+
function restoreValue(key, value) {
|
|
2324
|
+
if (typeof value !== 'string') {
|
|
2325
|
+
return value;
|
|
2326
|
+
}
|
|
2327
|
+
if (isIntegerResultKey(key) && /^-?\d+$/.test(value)) {
|
|
2328
|
+
return Number(value);
|
|
2329
|
+
}
|
|
2330
|
+
return fromPgSafe(value);
|
|
2331
|
+
}
|
|
2332
|
+
function isIntegerResultKey(key) {
|
|
2333
|
+
return INTEGER_RESULT_KEYS.has(key) || INTEGER_ALIAS_RESULT_KEY.test(key);
|
|
2334
|
+
}
|
|
2335
|
+
function pgInteger(value) {
|
|
2336
|
+
if (typeof value === 'number') {
|
|
2337
|
+
return Number.isFinite(value) ? value : undefined;
|
|
2338
|
+
}
|
|
2339
|
+
if (typeof value === 'string' && /^-?\d+$/.test(value)) {
|
|
2340
|
+
const parsed = Number(value);
|
|
2341
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2342
|
+
}
|
|
2343
|
+
return undefined;
|
|
2344
|
+
}
|
|
2345
|
+
function isVariable(value) {
|
|
2346
|
+
return Boolean(value && typeof value === 'object' && 'variable' in value);
|
|
2347
|
+
}
|
|
2348
|
+
function variablesInPattern(pattern) {
|
|
2349
|
+
return uniqueStrings(PATTERN_KEYS
|
|
2350
|
+
.map((key) => pattern[key])
|
|
2351
|
+
.filter(isVariable)
|
|
2352
|
+
.map((value) => value.variable));
|
|
2353
|
+
}
|
|
2354
|
+
function isPgRdf3xCompatiblePattern(pattern) {
|
|
2355
|
+
return PATTERN_KEYS.every((key) => {
|
|
2356
|
+
const value = pattern[key];
|
|
2357
|
+
if (!value || (0, types_1.isTerm)(value))
|
|
2358
|
+
return true;
|
|
2359
|
+
if (value === null || typeof value !== 'object' || 'termType' in value)
|
|
2360
|
+
return false;
|
|
2361
|
+
const operators = value;
|
|
2362
|
+
const allowed = new Set([
|
|
2363
|
+
'$eq',
|
|
2364
|
+
'$ne',
|
|
2365
|
+
'$in',
|
|
2366
|
+
'$notIn',
|
|
2367
|
+
'$termType',
|
|
2368
|
+
'$language',
|
|
2369
|
+
'$notLanguage',
|
|
2370
|
+
'$langMatches',
|
|
2371
|
+
'$datatype',
|
|
2372
|
+
'$notDatatype',
|
|
2373
|
+
...(key === 'graph' ? ['$startsWith'] : []),
|
|
2374
|
+
...(key === 'object' ? ['$gt', '$gte', '$lt', '$lte', '$contains', '$endsWith'] : []),
|
|
2375
|
+
]);
|
|
2376
|
+
if (Object.keys(operators).length === 0 || Object.keys(operators).some((operator) => !allowed.has(operator)))
|
|
2377
|
+
return false;
|
|
2378
|
+
if (operators.$eq !== undefined && !(0, types_1.isTerm)(operators.$eq))
|
|
2379
|
+
return false;
|
|
2380
|
+
if (operators.$ne !== undefined && !(0, types_1.isTerm)(operators.$ne))
|
|
2381
|
+
return false;
|
|
2382
|
+
if (operators.$in !== undefined && (!Array.isArray(operators.$in) || operators.$in.length === 0 || !operators.$in.every((entry) => (0, types_1.isTerm)(entry))))
|
|
2383
|
+
return false;
|
|
2384
|
+
if (operators.$notIn !== undefined && (!Array.isArray(operators.$notIn) || operators.$notIn.length === 0 || !operators.$notIn.every((entry) => (0, types_1.isTerm)(entry))))
|
|
2385
|
+
return false;
|
|
2386
|
+
if (operators.$startsWith !== undefined && typeof operators.$startsWith !== 'string')
|
|
2387
|
+
return false;
|
|
2388
|
+
if (operators.$termType !== undefined && !['iri', 'blank', 'literal', 'numeric'].includes(operators.$termType))
|
|
2389
|
+
return false;
|
|
2390
|
+
for (const languageOperator of ['$language', '$notLanguage', '$langMatches']) {
|
|
2391
|
+
if (operators[languageOperator] !== undefined && typeof operators[languageOperator] !== 'string')
|
|
2392
|
+
return false;
|
|
2393
|
+
}
|
|
2394
|
+
for (const datatypeOperator of ['$datatype', '$notDatatype']) {
|
|
2395
|
+
const datatype = operators[datatypeOperator];
|
|
2396
|
+
if (datatype !== undefined && (!(0, types_1.isTerm)(datatype) || datatype.termType !== 'NamedNode'))
|
|
2397
|
+
return false;
|
|
2398
|
+
}
|
|
2399
|
+
if (key === 'object') {
|
|
2400
|
+
for (const rangeOperator of ['$gt', '$gte', '$lt', '$lte']) {
|
|
2401
|
+
const value = operators[rangeOperator];
|
|
2402
|
+
if (value !== undefined && !isObjectRangeValue(value))
|
|
2403
|
+
return false;
|
|
2404
|
+
}
|
|
2405
|
+
for (const textOperator of ['$contains', '$endsWith']) {
|
|
2406
|
+
if (operators[textOperator] !== undefined && typeof operators[textOperator] !== 'string')
|
|
2407
|
+
return false;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
return true;
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
function isObjectRangeValue(value) {
|
|
2414
|
+
return typeof value === 'number'
|
|
2415
|
+
? Number.isFinite(value)
|
|
2416
|
+
: typeof value === 'string'
|
|
2417
|
+
? true
|
|
2418
|
+
: (0, types_1.isTerm)(value);
|
|
2419
|
+
}
|
|
2420
|
+
function shouldUseMembershipSource(resolved) {
|
|
2421
|
+
return resolved.ids.graph !== undefined
|
|
2422
|
+
|| Boolean(resolved.idSets.graph?.length)
|
|
2423
|
+
|| Boolean(resolved.excludedIdSets.graph?.length)
|
|
2424
|
+
|| resolved.graphPrefix !== undefined;
|
|
2425
|
+
}
|
|
2426
|
+
function termKindsForPatternKey(key) {
|
|
2427
|
+
switch (key) {
|
|
2428
|
+
case 'object':
|
|
2429
|
+
return ['iri', 'literal', 'blank'];
|
|
2430
|
+
case 'subject':
|
|
2431
|
+
return ['iri', 'blank'];
|
|
2432
|
+
case 'graph':
|
|
2433
|
+
return ['iri', 'default_graph'];
|
|
2434
|
+
case 'predicate':
|
|
2435
|
+
return ['iri'];
|
|
2436
|
+
default: {
|
|
2437
|
+
const exhaustive = key;
|
|
2438
|
+
throw new Error(`Unsupported RDF pattern key: ${exhaustive}`);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
function escapeLikePattern(value) {
|
|
2443
|
+
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
2444
|
+
}
|
|
2445
|
+
function rangeSuffix(range) {
|
|
2446
|
+
return `${range.min !== undefined ? (range.minInclusive ? '$gte' : '$gt') : ''}${range.max !== undefined ? (range.maxInclusive ? '$lte' : '$lt') : ''}`;
|
|
2447
|
+
}
|
|
2448
|
+
function describeScanOrder(options) {
|
|
2449
|
+
const order = options?.order ?? [];
|
|
2450
|
+
const directions = 'orderDirections' in (options ?? {})
|
|
2451
|
+
? (options?.orderDirections ?? [])
|
|
2452
|
+
: order.map(() => (options?.reverse ? 'desc' : 'asc'));
|
|
2453
|
+
return order.map((entry, index) => `${directions[index] ?? 'asc'}:${entry}`).join(',');
|
|
2454
|
+
}
|
|
2455
|
+
function describePatternSource(source) {
|
|
2456
|
+
return PATTERN_KEYS
|
|
2457
|
+
.map((key) => {
|
|
2458
|
+
const variableName = source.variables[key];
|
|
2459
|
+
if (variableName)
|
|
2460
|
+
return `${key}:?${variableName}`;
|
|
2461
|
+
const value = source.pattern[key];
|
|
2462
|
+
return value ? `${key}:${termMatchKey(value)}` : undefined;
|
|
2463
|
+
})
|
|
2464
|
+
.filter(Boolean)
|
|
2465
|
+
.join(',');
|
|
2466
|
+
}
|
|
2467
|
+
function termMatchKey(match) {
|
|
2468
|
+
if (!match)
|
|
2469
|
+
return '*';
|
|
2470
|
+
if ((0, types_1.isTerm)(match))
|
|
2471
|
+
return `${match.termType}:${match.value}`;
|
|
2472
|
+
return JSON.stringify(match);
|
|
2473
|
+
}
|
|
2474
|
+
function queryAggregates(query) {
|
|
2475
|
+
if (query.aggregates && query.aggregates.length > 0)
|
|
2476
|
+
return query.aggregates;
|
|
2477
|
+
return query.aggregate ? [query.aggregate] : [];
|
|
2478
|
+
}
|
|
2479
|
+
function aggregatePlan(aggregates, grouped) {
|
|
2480
|
+
return `Aggregate(${grouped ? 'group-' : ''}${aggregates.map((aggregate) => (`${aggregate.type}${aggregate.distinct ? ':DISTINCT' : ''}(${aggregate.variable ? `?${aggregate.variable}` : '*'})`)).join(',')})`;
|
|
2481
|
+
}
|
|
2482
|
+
function describeFilter(filter) {
|
|
2483
|
+
return `?${filter.variable}${filter.operator}`;
|
|
2484
|
+
}
|
|
2485
|
+
function describeQueryOrder(orderBy) {
|
|
2486
|
+
return orderBy.map((entry) => `${entry.direction ?? 'asc'}:${entry.variable}`).join(',');
|
|
2487
|
+
}
|
|
2488
|
+
function isNativeAggregateHavingOperator(operator) {
|
|
2489
|
+
return operator === '$eq'
|
|
2490
|
+
|| operator === '$ne'
|
|
2491
|
+
|| operator === '$gt'
|
|
2492
|
+
|| operator === '$gte'
|
|
2493
|
+
|| operator === '$lt'
|
|
2494
|
+
|| operator === '$lte';
|
|
2495
|
+
}
|
|
2496
|
+
function aggregateSqlOperator(operator) {
|
|
2497
|
+
switch (operator) {
|
|
2498
|
+
case '$eq':
|
|
2499
|
+
return '=';
|
|
2500
|
+
case '$ne':
|
|
2501
|
+
return '!=';
|
|
2502
|
+
case '$gt':
|
|
2503
|
+
return '>';
|
|
2504
|
+
case '$gte':
|
|
2505
|
+
return '>=';
|
|
2506
|
+
case '$lt':
|
|
2507
|
+
return '<';
|
|
2508
|
+
case '$lte':
|
|
2509
|
+
return '<=';
|
|
2510
|
+
default:
|
|
2511
|
+
throw new Error(`Unsupported PostgreSQL RDF-3X aggregate HAVING operator: ${operator}`);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
function patternEqualities(slotsByVariable) {
|
|
2515
|
+
const equalities = [];
|
|
2516
|
+
for (const [variable, slots] of slotsByVariable) {
|
|
2517
|
+
const [first, ...rest] = slots;
|
|
2518
|
+
if (!first)
|
|
2519
|
+
continue;
|
|
2520
|
+
for (const slot of rest) {
|
|
2521
|
+
equalities.push({ variable, left: first, right: slot });
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
return equalities;
|
|
2525
|
+
}
|
|
2526
|
+
function storagePlanMarkers(queryPlan) {
|
|
2527
|
+
return (queryPlan ?? []).filter((entry) => (entry.startsWith('TextSearch(')
|
|
2528
|
+
|| entry.startsWith('Rdf3x')
|
|
2529
|
+
|| entry === 'GraphMembershipFilter'
|
|
2530
|
+
|| entry === 'GraphPrefixMembershipFilter'
|
|
2531
|
+
|| entry.startsWith('LexicalRange(')
|
|
2532
|
+
|| entry.startsWith('NumericRange(')
|
|
2533
|
+
|| entry.startsWith('TermIn(')
|
|
2534
|
+
|| entry.startsWith('TermNotIn(')
|
|
2535
|
+
|| entry.startsWith('TermType(')
|
|
2536
|
+
|| entry.startsWith('Language(')
|
|
2537
|
+
|| entry.startsWith('Datatype(')));
|
|
2538
|
+
}
|
|
2539
|
+
function uniqueStrings(values) {
|
|
2540
|
+
return [...new Set(values)];
|
|
2541
|
+
}
|
|
2542
|
+
function uniqueNumbers(values) {
|
|
2543
|
+
return [...new Set(values)];
|
|
2544
|
+
}
|
|
2545
|
+
function joinSourceVariables(source) {
|
|
2546
|
+
return uniqueStrings(Object.values(source.entry.variables).filter((value) => Boolean(value)));
|
|
2547
|
+
}
|
|
2548
|
+
function joinSolutionMappingKeyExpression(variableAliases, variables) {
|
|
2549
|
+
const variableNames = uniqueStrings(variables ?? [...variableAliases.keys()]);
|
|
2550
|
+
if (variableNames.length === 0)
|
|
2551
|
+
return '1';
|
|
2552
|
+
return variableNames.map((variableName) => {
|
|
2553
|
+
const alias = variableAliases.get(variableName);
|
|
2554
|
+
if (!alias)
|
|
2555
|
+
throw new Error(`Postgres RDF-3X COUNT(DISTINCT *) cannot read unbound variable: ${variableName}`);
|
|
2556
|
+
return `source.${alias}`;
|
|
2557
|
+
}).join(` || ':' || `);
|
|
2558
|
+
}
|
|
2559
|
+
function pairProjectionRowTotal(rows) {
|
|
2560
|
+
return Object.values(rows).reduce((sum, count) => sum + count, 0);
|
|
2561
|
+
}
|
|
2562
|
+
function termProjectionRowTotal(rows) {
|
|
2563
|
+
return Object.values(rows).reduce((sum, count) => sum + count, 0);
|
|
2564
|
+
}
|
|
2565
|
+
function sumSpaceObjects(objects, kind) {
|
|
2566
|
+
return objects
|
|
2567
|
+
.filter((object) => object.kind === kind)
|
|
2568
|
+
.reduce((sum, object) => sum + object.bytes, 0);
|
|
2569
|
+
}
|
|
2570
|
+
const INTEGER_RESULT_KEYS = new Set([
|
|
2571
|
+
'id',
|
|
2572
|
+
'graph_id',
|
|
2573
|
+
'subject_id',
|
|
2574
|
+
'predicate_id',
|
|
2575
|
+
'object_id',
|
|
2576
|
+
'source_file_id',
|
|
2577
|
+
'source_line_no',
|
|
2578
|
+
'datatype_id',
|
|
2579
|
+
'count',
|
|
2580
|
+
'term_count',
|
|
2581
|
+
'quad_count',
|
|
2582
|
+
'source_count',
|
|
2583
|
+
'graph_count',
|
|
2584
|
+
]);
|
|
2585
|
+
const INTEGER_ALIAS_RESULT_KEY = /^(?:v|a)\d+$/;
|
|
2586
|
+
function uniqueQuads(quads) {
|
|
2587
|
+
const seen = new Set();
|
|
2588
|
+
const result = [];
|
|
2589
|
+
for (const value of quads) {
|
|
2590
|
+
const key = [
|
|
2591
|
+
value.graph.termType,
|
|
2592
|
+
value.graph.value,
|
|
2593
|
+
value.subject.termType,
|
|
2594
|
+
value.subject.value,
|
|
2595
|
+
value.predicate.termType,
|
|
2596
|
+
value.predicate.value,
|
|
2597
|
+
value.object.termType,
|
|
2598
|
+
value.object.value,
|
|
2599
|
+
value.object.termType === 'Literal' ? value.object.language : '',
|
|
2600
|
+
value.object.termType === 'Literal' ? value.object.datatype.value : '',
|
|
2601
|
+
].join('\u001f');
|
|
2602
|
+
if (!seen.has(key)) {
|
|
2603
|
+
seen.add(key);
|
|
2604
|
+
result.push(value);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
return result;
|
|
2608
|
+
}
|
|
2609
|
+
//# sourceMappingURL=PostgresRdfEngine.js.map
|