jinaga-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/dist/authentication/authentication-device.d.ts +20 -0
- package/dist/authentication/authentication-device.js +61 -0
- package/dist/authentication/authentication-device.js.map +1 -0
- package/dist/authentication/authentication-impl.d.ts +21 -0
- package/dist/authentication/authentication-impl.js +61 -0
- package/dist/authentication/authentication-impl.js.map +1 -0
- package/dist/authentication/authentication-session.d.ts +23 -0
- package/dist/authentication/authentication-session.js +85 -0
- package/dist/authentication/authentication-session.js.map +1 -0
- package/dist/authentication/authentication.d.ts +5 -0
- package/dist/authentication/authentication.js +3 -0
- package/dist/authentication/authentication.js.map +1 -0
- package/dist/authentication.d.ts +17 -0
- package/dist/authentication.js +52 -0
- package/dist/authentication.js.map +1 -0
- package/dist/authorization/authorization-keystore.d.ts +13 -0
- package/dist/authorization/authorization-keystore.js +61 -0
- package/dist/authorization/authorization-keystore.js.map +1 -0
- package/dist/http/node-http.d.ts +7 -0
- package/dist/http/node-http.js +126 -0
- package/dist/http/node-http.js.map +1 -0
- package/dist/http/router.d.ts +20 -0
- package/dist/http/router.js +223 -0
- package/dist/http/router.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/jinaga-server.d.ts +18 -0
- package/dist/jinaga-server.js +101 -0
- package/dist/jinaga-server.js.map +1 -0
- package/dist/keystore.d.ts +9 -0
- package/dist/keystore.js +3 -0
- package/dist/keystore.js.map +1 -0
- package/dist/memory/memory-keystore.d.ts +16 -0
- package/dist/memory/memory-keystore.js +86 -0
- package/dist/memory/memory-keystore.js.map +1 -0
- package/dist/postgres/connection.d.ts +12 -0
- package/dist/postgres/connection.js +83 -0
- package/dist/postgres/connection.js.map +1 -0
- package/dist/postgres/edge-record.d.ts +9 -0
- package/dist/postgres/edge-record.js +30 -0
- package/dist/postgres/edge-record.js.map +1 -0
- package/dist/postgres/maps.d.ts +21 -0
- package/dist/postgres/maps.js +86 -0
- package/dist/postgres/maps.js.map +1 -0
- package/dist/postgres/postgres-keystore.d.ts +20 -0
- package/dist/postgres/postgres-keystore.js +168 -0
- package/dist/postgres/postgres-keystore.js.map +1 -0
- package/dist/postgres/postgres-store.d.ts +20 -0
- package/dist/postgres/postgres-store.js +717 -0
- package/dist/postgres/postgres-store.js.map +1 -0
- package/dist/postgres/query-description-builder.d.ts +13 -0
- package/dist/postgres/query-description-builder.js +104 -0
- package/dist/postgres/query-description-builder.js.map +1 -0
- package/dist/postgres/query-description.d.ts +71 -0
- package/dist/postgres/query-description.js +256 -0
- package/dist/postgres/query-description.js.map +1 -0
- package/dist/postgres/specification-result-sql.d.ts +41 -0
- package/dist/postgres/specification-result-sql.js +260 -0
- package/dist/postgres/specification-result-sql.js.map +1 -0
- package/dist/postgres/specification-sql.d.ts +3 -0
- package/dist/postgres/specification-sql.js +119 -0
- package/dist/postgres/specification-sql.js.map +1 -0
- package/dist/postgres/sql.d.ts +10 -0
- package/dist/postgres/sql.js +396 -0
- package/dist/postgres/sql.js.map +1 -0
- package/dist/util/fn.d.ts +6 -0
- package/dist/util/fn.js +81 -0
- package/dist/util/fn.js.map +1 -0
- package/dist/util/promise.d.ts +1 -0
- package/dist/util/promise.js +10 -0
- package/dist/util/promise.js.map +1 -0
- package/docs/registrations.sql +22 -0
- package/docs/specification.txt +41 -0
- package/docs/sql-generator.gv +25 -0
- package/integration-test/docker-compose.yaml +13 -0
- package/integration-test/jinaga-test/Dockerfile +12 -0
- package/integration-test/jinaga-test/jinaga.test.js +425 -0
- package/integration-test/jinaga-test/package-lock.json +6767 -0
- package/integration-test/jinaga-test/package.json +25 -0
- package/integration-test/postgres/0-init-app-db.sh +16 -0
- package/integration-test/postgres/1-setup.sql +219 -0
- package/integration-test/postgres/2-grant.sh +17 -0
- package/integration-test/postgres/Dockerfile +5 -0
- package/integration-test/run.sh +17 -0
- package/integration-test/start_db.sh +2 -0
- package/integration-test/stop_db.sh +3 -0
- package/integration-test/test.sh +8 -0
- package/interactive/index.js +56 -0
- package/jest.config.js +12 -0
- package/package.json +51 -0
- package/scripts/combined.sql +46 -0
- package/scripts/cte.sql +55 -0
- package/scripts/separate.sql +27 -0
- package/setup.sh +34 -0
- package/setup.sql +329 -0
- package/src/authentication/authentication-device.ts +68 -0
- package/src/authentication/authentication-impl.ts +59 -0
- package/src/authentication/authentication-session.ts +95 -0
- package/src/authentication/authentication.ts +6 -0
- package/src/authentication.ts +45 -0
- package/src/authorization/authorization-keystore.ts +67 -0
- package/src/http/node-http.ts +106 -0
- package/src/http/router.ts +237 -0
- package/src/index.ts +1 -0
- package/src/jinaga-server.ts +125 -0
- package/src/keystore.ts +10 -0
- package/src/memory/memory-keystore.ts +93 -0
- package/src/postgres/connection.ts +73 -0
- package/src/postgres/edge-record.ts +36 -0
- package/src/postgres/maps.ts +89 -0
- package/src/postgres/postgres-keystore.ts +167 -0
- package/src/postgres/postgres-store.ts +874 -0
- package/src/postgres/query-description-builder.ts +116 -0
- package/src/postgres/query-description.ts +385 -0
- package/src/postgres/specification-result-sql.ts +362 -0
- package/src/postgres/specification-sql.ts +138 -0
- package/src/postgres/sql.ts +482 -0
- package/src/util/fn.ts +63 -0
- package/src/util/promise.ts +5 -0
- package/test/authorization/authorizationSpec.ts +209 -0
- package/test/postgres/postgresSpec.ts +385 -0
- package/test/postgres/postgresSpecificationSpec.ts +742 -0
- package/test/reporter.js +25 -0
- package/tsconfig.json +25 -0
- package/upgrade.sh +21 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChildProjections,
|
|
3
|
+
ElementProjection,
|
|
4
|
+
FactReference,
|
|
5
|
+
Match,
|
|
6
|
+
ResultProjection,
|
|
7
|
+
SingularProjection,
|
|
8
|
+
Specification,
|
|
9
|
+
SpecificationProjection,
|
|
10
|
+
} from "jinaga";
|
|
11
|
+
|
|
12
|
+
import { FactTypeMap, getFactTypeId, RoleMap } from "./maps";
|
|
13
|
+
import { FactDescription, InputDescription, QueryDescription, SpecificationSqlQuery } from "./query-description";
|
|
14
|
+
import { QueryDescriptionBuilder } from "./query-description-builder";
|
|
15
|
+
|
|
16
|
+
export type FactByIdentifier = {
|
|
17
|
+
[identifier: string]: FactDescription;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface ResultDescription {
|
|
21
|
+
queryDescription: QueryDescription;
|
|
22
|
+
resultProjection: ResultProjection;
|
|
23
|
+
childResultDescriptions: NamedResultDescription[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface NamedResultDescription extends ResultDescription {
|
|
27
|
+
name: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface IdentifiedResults {
|
|
31
|
+
factIds: number[];
|
|
32
|
+
result: any;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ChildResults {
|
|
36
|
+
parentFactIds: number[];
|
|
37
|
+
results: any[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SqlQueryTree {
|
|
41
|
+
sqlQuery: SpecificationSqlQuery;
|
|
42
|
+
childQueries: NamedSqlQueryTree[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface NamedSqlQueryTree extends SqlQueryTree {
|
|
46
|
+
name: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ResultSetTree {
|
|
50
|
+
resultSet: any[];
|
|
51
|
+
childResultSets: NamedResultSetTree[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface NamedResultSetTree extends ResultSetTree {
|
|
55
|
+
name: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class ResultComposer {
|
|
59
|
+
constructor(
|
|
60
|
+
private readonly sqlQuery: SpecificationSqlQuery,
|
|
61
|
+
private readonly resultProjection: ResultProjection,
|
|
62
|
+
private readonly parentFactIdLength: number,
|
|
63
|
+
private readonly childResultComposers: NamedResultComposer[]
|
|
64
|
+
) { }
|
|
65
|
+
|
|
66
|
+
public getSqlQueries(): SqlQueryTree {
|
|
67
|
+
const childQueries: NamedSqlQueryTree[] = [];
|
|
68
|
+
for (const childResultComposer of this.childResultComposers) {
|
|
69
|
+
childQueries.push(({
|
|
70
|
+
name: childResultComposer.name,
|
|
71
|
+
...childResultComposer.resultComposer.getSqlQueries()
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
sqlQuery: this.sqlQuery,
|
|
76
|
+
childQueries
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public compose(
|
|
81
|
+
resultSets: ResultSetTree
|
|
82
|
+
): any[] {
|
|
83
|
+
const childResults = this.composeInternal(resultSets);
|
|
84
|
+
if (childResults.length === 0) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
return childResults[0].results;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private composeInternal(
|
|
93
|
+
resultSets: ResultSetTree
|
|
94
|
+
): ChildResults[] {
|
|
95
|
+
const rows = resultSets.resultSet;
|
|
96
|
+
if (rows.length === 0) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Project all rows and their identifiers
|
|
101
|
+
const identifiedResults: IdentifiedResults[] = rows.map(row => ({
|
|
102
|
+
factIds: this.identifierOf(row),
|
|
103
|
+
result: this.projectionOf(row)
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
// Compose child results
|
|
107
|
+
for (const childResultComposer of this.childResultComposers) {
|
|
108
|
+
const childResultSet = resultSets.childResultSets.find(childResultSet =>
|
|
109
|
+
childResultSet.name === childResultComposer.name);
|
|
110
|
+
if (!childResultSet) {
|
|
111
|
+
const availableNames = resultSets.childResultSets.map(childResultSet => childResultSet.name);
|
|
112
|
+
throw new Error(`Child result set ${childResultComposer.name} not found in (${availableNames.join(", ")})`);
|
|
113
|
+
}
|
|
114
|
+
const composedResults = childResultComposer.resultComposer.composeInternal(childResultSet);
|
|
115
|
+
|
|
116
|
+
// Add the child results
|
|
117
|
+
let index = 0;
|
|
118
|
+
for (const identifiedResult of identifiedResults) {
|
|
119
|
+
let results: any[] = [];
|
|
120
|
+
if (index < composedResults.length && idsEqual(identifiedResult.factIds, composedResults[index].parentFactIds)) {
|
|
121
|
+
results = composedResults[index].results;
|
|
122
|
+
index++;
|
|
123
|
+
}
|
|
124
|
+
identifiedResult.result = {
|
|
125
|
+
...identifiedResult.result,
|
|
126
|
+
[childResultComposer.name]: results
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Group the results by their parent identifiers
|
|
132
|
+
const childResults: ChildResults[] = [];
|
|
133
|
+
let parentFactIds: number[] = identifiedResults[0].factIds.slice(0, this.parentFactIdLength);
|
|
134
|
+
let results: any[] = [ identifiedResults[0].result ];
|
|
135
|
+
for (const identifiedResult of identifiedResults.slice(1)) {
|
|
136
|
+
const nextParentFactIds = identifiedResult.factIds.slice(0, this.parentFactIdLength);
|
|
137
|
+
if (idsEqual(nextParentFactIds, parentFactIds)) {
|
|
138
|
+
results.push(identifiedResult.result);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
childResults.push({
|
|
142
|
+
parentFactIds,
|
|
143
|
+
results
|
|
144
|
+
});
|
|
145
|
+
parentFactIds = nextParentFactIds;
|
|
146
|
+
results = [ identifiedResult.result ];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
childResults.push({
|
|
150
|
+
parentFactIds,
|
|
151
|
+
results
|
|
152
|
+
});
|
|
153
|
+
return childResults;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private identifierOf(row: any): number[] {
|
|
157
|
+
return this.sqlQuery.labels.map(label => row[`id${label.index}`]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private projectionOf(row: any): any {
|
|
161
|
+
if (!Array.isArray(this.resultProjection)) {
|
|
162
|
+
return this.singularValue(this.resultProjection, row);
|
|
163
|
+
}
|
|
164
|
+
else if (this.resultProjection.length === 0 && this.childResultComposers.length === 0) {
|
|
165
|
+
return this.sqlQuery.labels
|
|
166
|
+
.slice(this.parentFactIdLength)
|
|
167
|
+
.reduce((acc, label) => ({
|
|
168
|
+
...acc,
|
|
169
|
+
[label.name]: row[`data${label.index}`].fields
|
|
170
|
+
}), {})
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
return this.resultProjection.reduce((acc, elementProjection) => ({
|
|
174
|
+
...acc,
|
|
175
|
+
[elementProjection.name]: this.elementValue(elementProjection, row)
|
|
176
|
+
}), {});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private elementValue(projection: ElementProjection, row: any): any {
|
|
181
|
+
const label = this.getLabel(projection.label);
|
|
182
|
+
if (projection.type === "field") {
|
|
183
|
+
return row[`data${label.index}`].fields[projection.field];
|
|
184
|
+
}
|
|
185
|
+
else if (projection.type === "hash") {
|
|
186
|
+
return row[`hash${label.index}`];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private singularValue(projection: SingularProjection, row: any): any {
|
|
191
|
+
const label = this.getLabel(projection.label);
|
|
192
|
+
return row[`data${label.index}`].fields[projection.field];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private getLabel(name: string) {
|
|
196
|
+
const label = this.sqlQuery.labels.find(label => label.name === name);
|
|
197
|
+
if (!label) {
|
|
198
|
+
throw new Error(`Label ${name} not found. Known labels: ${this.sqlQuery.labels.map(label => label.name).join(", ")}`);
|
|
199
|
+
}
|
|
200
|
+
return label;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface NamedResultComposer {
|
|
205
|
+
name: string;
|
|
206
|
+
resultComposer: ResultComposer;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
class ResultDescriptionBuilder extends QueryDescriptionBuilder {
|
|
210
|
+
constructor(
|
|
211
|
+
factTypes: FactTypeMap,
|
|
212
|
+
roleMap: RoleMap
|
|
213
|
+
) {
|
|
214
|
+
super(factTypes, roleMap);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
buildDescription(start: FactReference[], specification: Specification): ResultDescription {
|
|
218
|
+
// Verify that the number of start facts equals the number of inputs
|
|
219
|
+
if (start.length !== specification.given.length) {
|
|
220
|
+
throw new Error(`The number of start facts (${start.length}) does not equal the number of inputs (${specification.given.length})`);
|
|
221
|
+
}
|
|
222
|
+
// Verify that the input type matches the start fact type
|
|
223
|
+
for (let i = 0; i < start.length; i++) {
|
|
224
|
+
if (start[i].type !== specification.given[i].type) {
|
|
225
|
+
throw new Error(`The type of start fact ${i} (${start[i].type}) does not match the type of input ${i} (${specification.given[i].type})`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Allocate a fact table for each given.
|
|
230
|
+
// While the fact type and hash parameters are zero, the join will not be written.
|
|
231
|
+
const inputs: InputDescription[] = specification.given
|
|
232
|
+
.map((label, i) => ({
|
|
233
|
+
label: label.name,
|
|
234
|
+
factIndex: i+1,
|
|
235
|
+
factTypeId: getFactTypeId(this.factTypes, label.type),
|
|
236
|
+
factHash: start[i].hash,
|
|
237
|
+
factTypeParameter: 0,
|
|
238
|
+
factHashParameter: 0
|
|
239
|
+
}));
|
|
240
|
+
const facts: FactDescription[] = specification.given
|
|
241
|
+
.map((label, i) => ({
|
|
242
|
+
factIndex: i+1,
|
|
243
|
+
type: label.type
|
|
244
|
+
}));
|
|
245
|
+
const givenFacts = specification.given.reduce((knownFacts, label, i) => ({
|
|
246
|
+
...knownFacts,
|
|
247
|
+
[label.name]: facts[i]
|
|
248
|
+
}), {} as FactByIdentifier);
|
|
249
|
+
|
|
250
|
+
// The QueryDescription is an immutable data type.
|
|
251
|
+
// Initialize it with the inputs and facts.
|
|
252
|
+
// The DescriptionBuilder will branch at various points, and
|
|
253
|
+
// build on the current query description along each branch.
|
|
254
|
+
const initialQueryDescription = new QueryDescription(inputs, [], [], facts, [], []);
|
|
255
|
+
return this.createResultDescription(initialQueryDescription, specification.matches, specification.childProjections, givenFacts, []);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private createResultDescription(queryDescription: QueryDescription, matches: Match[], childProjections: ChildProjections, knownFacts: FactByIdentifier, path: number[]): ResultDescription {
|
|
259
|
+
({ queryDescription, knownFacts } = this.addEdges(queryDescription, knownFacts, path, matches));
|
|
260
|
+
if (!queryDescription.isSatisfiable()) {
|
|
261
|
+
// Abort the branch if the query is not satisfiable
|
|
262
|
+
return {
|
|
263
|
+
queryDescription,
|
|
264
|
+
resultProjection: [],
|
|
265
|
+
childResultDescriptions: []
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const childResultDescriptions: NamedResultDescription[] = [];
|
|
269
|
+
if (Array.isArray(childProjections)) {
|
|
270
|
+
const specificationProjections = childProjections
|
|
271
|
+
.filter(projection => projection.type === "specification") as SpecificationProjection[];
|
|
272
|
+
const elementProjections = childProjections
|
|
273
|
+
.filter(projection => projection.type === "field" || projection.type === "hash") as ElementProjection[];
|
|
274
|
+
for (const child of specificationProjections) {
|
|
275
|
+
const childResultDescription = this.createResultDescription(queryDescription, child.matches, child.childProjections, knownFacts, []);
|
|
276
|
+
childResultDescriptions.push({
|
|
277
|
+
name: child.name,
|
|
278
|
+
...childResultDescription
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
queryDescription,
|
|
283
|
+
resultProjection: elementProjections,
|
|
284
|
+
childResultDescriptions
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
return {
|
|
289
|
+
queryDescription,
|
|
290
|
+
resultProjection: childProjections,
|
|
291
|
+
childResultDescriptions: []
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private addEdges(queryDescription: QueryDescription, knownFacts: FactByIdentifier, path: number[], matches: Match[]): { queryDescription: QueryDescription, knownFacts: FactByIdentifier } {
|
|
297
|
+
for (const match of matches) {
|
|
298
|
+
for (const condition of match.conditions) {
|
|
299
|
+
if (condition.type === "path") {
|
|
300
|
+
({queryDescription, knownFacts} = this.addPathCondition(queryDescription, knownFacts, path, match.unknown, "", condition));
|
|
301
|
+
}
|
|
302
|
+
else if (condition.type === "existential") {
|
|
303
|
+
if (condition.exists) {
|
|
304
|
+
// Include the edges of the existential condition into the current
|
|
305
|
+
// query description.
|
|
306
|
+
({ queryDescription } = this.addEdges(queryDescription, knownFacts, path, condition.matches));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
// Apply the where clause and continue with the tuple where it is true.
|
|
310
|
+
// The path describes which not-exists condition we are currently building on.
|
|
311
|
+
// Because the path is not empty, labeled facts will be included in the output.
|
|
312
|
+
const { query: queryDescriptionWithNotExist, path: conditionalPath } = queryDescription.withNotExistsCondition(path);
|
|
313
|
+
const { queryDescription: queryDescriptionConditional } = this.addEdges(queryDescriptionWithNotExist, knownFacts, conditionalPath, condition.matches);
|
|
314
|
+
|
|
315
|
+
// If the negative existential condition is not satisfiable, then
|
|
316
|
+
// that means that the condition will always be true.
|
|
317
|
+
// We can therefore skip the branch for the negative existential condition.
|
|
318
|
+
if (queryDescriptionConditional.isSatisfiable()) {
|
|
319
|
+
queryDescription = queryDescriptionConditional;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!queryDescription.isSatisfiable()) {
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (!queryDescription.isSatisfiable()) {
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
queryDescription,
|
|
333
|
+
knownFacts
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function idsEqual(a: number[], b: number[]) {
|
|
339
|
+
return a.every((value, index) => value === b[index]);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function resultSqlFromSpecification(start: FactReference[], specification: Specification, factTypes: FactTypeMap, roleMap: RoleMap): ResultComposer {
|
|
343
|
+
const descriptionBuilder = new ResultDescriptionBuilder(factTypes, roleMap);
|
|
344
|
+
const description = descriptionBuilder.buildDescription(start, specification);
|
|
345
|
+
|
|
346
|
+
if (!description.queryDescription.isSatisfiable()) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
return createResultComposer(description, 0);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function createResultComposer(description: ResultDescription, parentFactIdLength: number): ResultComposer {
|
|
353
|
+
const sqlQuery = description.queryDescription.generateResultSqlQuery();
|
|
354
|
+
const resultProjection = description.resultProjection;
|
|
355
|
+
const childResultComposers = description.childResultDescriptions
|
|
356
|
+
.filter(child => child.queryDescription.isSatisfiable())
|
|
357
|
+
.map(child => ({
|
|
358
|
+
name: child.name,
|
|
359
|
+
resultComposer: createResultComposer(child, description.queryDescription.outputLength())
|
|
360
|
+
}));
|
|
361
|
+
return new ResultComposer(sqlQuery, resultProjection, parentFactIdLength, childResultComposers);
|
|
362
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { FactBookmark, FactReference, Match, Projection, Specification } from "jinaga";
|
|
2
|
+
|
|
3
|
+
import { FactTypeMap, getFactTypeId, RoleMap } from "./maps";
|
|
4
|
+
import { FactDescription, InputDescription, QueryDescription, SpecificationSqlQuery } from "./query-description";
|
|
5
|
+
import { QueryDescriptionBuilder } from "./query-description-builder";
|
|
6
|
+
|
|
7
|
+
type FactByIdentifier = {
|
|
8
|
+
[identifier: string]: FactDescription;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class DescriptionBuilder extends QueryDescriptionBuilder {
|
|
12
|
+
constructor(
|
|
13
|
+
factTypes: FactTypeMap,
|
|
14
|
+
roleMap: RoleMap
|
|
15
|
+
) { super(factTypes, roleMap); }
|
|
16
|
+
|
|
17
|
+
public buildDescriptions(start: FactReference[], specification: Specification): QueryDescription[] {
|
|
18
|
+
// Verify that the number of start facts equals the number of inputs
|
|
19
|
+
if (start.length !== specification.given.length) {
|
|
20
|
+
throw new Error(`The number of start facts (${start.length}) does not equal the number of inputs (${specification.given.length})`);
|
|
21
|
+
}
|
|
22
|
+
// Verify that the input type matches the start fact type
|
|
23
|
+
for (let i = 0; i < start.length; i++) {
|
|
24
|
+
if (start[i].type !== specification.given[i].type) {
|
|
25
|
+
throw new Error(`The type of start fact ${i} (${start[i].type}) does not match the type of input ${i} (${specification.given[i].type})`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Allocate a fact table for each given.
|
|
30
|
+
// While the fact type and hash parameters are zero, the join will not be written.
|
|
31
|
+
const inputs: InputDescription[] = specification.given
|
|
32
|
+
.map((label, i) => ({
|
|
33
|
+
label: label.name,
|
|
34
|
+
factIndex: i+1,
|
|
35
|
+
factTypeId: getFactTypeId(this.factTypes, label.type),
|
|
36
|
+
factHash: start[i].hash,
|
|
37
|
+
factTypeParameter: 0,
|
|
38
|
+
factHashParameter: 0
|
|
39
|
+
}));
|
|
40
|
+
const facts: FactDescription[] = specification.given
|
|
41
|
+
.map((label, i) => ({
|
|
42
|
+
factIndex: i+1,
|
|
43
|
+
type: label.type
|
|
44
|
+
}));
|
|
45
|
+
const givenFacts = specification.given.reduce((knownFacts, label, i) => ({
|
|
46
|
+
...knownFacts,
|
|
47
|
+
[label.name]: facts[i]
|
|
48
|
+
}), {} as FactByIdentifier);
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
// The QueryDescription is an immutable data type.
|
|
52
|
+
// Initialize it with the inputs and facts.
|
|
53
|
+
// The DescriptionBuilder will branch at various points, and
|
|
54
|
+
// build on the current query description along each branch.
|
|
55
|
+
const initialQueryDescription = new QueryDescription(inputs, [], [], facts, [], []);
|
|
56
|
+
const { queryDescriptions, knownFacts } = this.addEdges(initialQueryDescription, givenFacts, [], "", specification.matches);
|
|
57
|
+
|
|
58
|
+
// The final query description represents the complete tuple.
|
|
59
|
+
// Build projections onto that one.
|
|
60
|
+
const finalQueryDescription = queryDescriptions[queryDescriptions.length - 1];
|
|
61
|
+
if (Array.isArray(specification.childProjections)) {
|
|
62
|
+
const queryDescriptionsWithProjections = this.addProjections(finalQueryDescription, knownFacts, specification.childProjections);
|
|
63
|
+
return [ ...queryDescriptions, ...queryDescriptionsWithProjections ];
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
return queryDescriptions;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private addEdges(queryDescription: QueryDescription, knownFacts: FactByIdentifier, path: number[], prefix: string, matches: Match[]): { queryDescriptions: QueryDescription[], knownFacts: FactByIdentifier } {
|
|
71
|
+
const queryDescriptions: QueryDescription[] = [];
|
|
72
|
+
for (const match of matches) {
|
|
73
|
+
for (const condition of match.conditions) {
|
|
74
|
+
if (condition.type === "path") {
|
|
75
|
+
({queryDescription, knownFacts} = this.addPathCondition(queryDescription, knownFacts, path, match.unknown, prefix, condition));
|
|
76
|
+
}
|
|
77
|
+
else if (condition.type === "existential") {
|
|
78
|
+
if (condition.exists) {
|
|
79
|
+
// Include the edges of the existential condition into the current
|
|
80
|
+
// query description.
|
|
81
|
+
const { queryDescriptions: newQueryDescriptions } = this.addEdges(queryDescription, knownFacts, path, prefix, condition.matches);
|
|
82
|
+
const last = newQueryDescriptions.length - 1;
|
|
83
|
+
queryDescriptions.push(...newQueryDescriptions.slice(0, last));
|
|
84
|
+
queryDescription = newQueryDescriptions[last];
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Branch from the current query description and follow the
|
|
88
|
+
// edges of the existential condition.
|
|
89
|
+
// This will produce tuples that prove the condition false.
|
|
90
|
+
const { queryDescriptions: newQueryDescriptions } = this.addEdges(queryDescription, knownFacts, path, prefix, condition.matches);
|
|
91
|
+
|
|
92
|
+
// Then apply the where clause and continue with the tuple where it is true.
|
|
93
|
+
// The path describes which not-exists condition we are currently building on.
|
|
94
|
+
// Because the path is not empty, labeled facts will be included in the output.
|
|
95
|
+
const { query: queryDescriptionWithNotExist, path: conditionalPath } = queryDescription.withNotExistsCondition(path);
|
|
96
|
+
const { queryDescriptions: newQueryDescriptionsWithNotExists } = this.addEdges(queryDescriptionWithNotExist, knownFacts, conditionalPath, prefix, condition.matches);
|
|
97
|
+
const last = newQueryDescriptionsWithNotExists.length - 1;
|
|
98
|
+
const queryDescriptionConditional = newQueryDescriptionsWithNotExists[last];
|
|
99
|
+
|
|
100
|
+
// If the negative existential condition is not satisfiable, then
|
|
101
|
+
// that means that the condition will always be true.
|
|
102
|
+
// We can therefore skip the branch for the negative existential condition.
|
|
103
|
+
if (queryDescriptionConditional.isSatisfiable()) {
|
|
104
|
+
queryDescriptions.push(...newQueryDescriptions);
|
|
105
|
+
queryDescriptions.push(...newQueryDescriptionsWithNotExists.slice(0, last));
|
|
106
|
+
queryDescription = queryDescriptionConditional;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
queryDescriptions.push(queryDescription);
|
|
113
|
+
return { queryDescriptions, knownFacts };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
addProjections(queryDescription: QueryDescription, knownFacts: FactByIdentifier, projections: Projection[]): QueryDescription[] {
|
|
117
|
+
const queryDescriptions: QueryDescription[] = [];
|
|
118
|
+
projections.forEach(projection => {
|
|
119
|
+
if (projection.type === "specification") {
|
|
120
|
+
// Produce more facts in the tuple, and prefix the labels with the projection name.
|
|
121
|
+
const prefix = projection.name + ".";
|
|
122
|
+
const { queryDescriptions: queryDescriptionsWithEdges } = this.addEdges(queryDescription, knownFacts, [], prefix, projection.matches);
|
|
123
|
+
queryDescriptions.push(...queryDescriptionsWithEdges);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return queryDescriptions;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function sqlFromSpecification(start: FactReference[], bookmarks: FactBookmark[], limit: number, specification: Specification, factTypes: Map<string, number>, roleMap: Map<number, Map<string, number>>): SpecificationSqlQuery[] {
|
|
131
|
+
const descriptionBuilder = new DescriptionBuilder(factTypes, roleMap);
|
|
132
|
+
const descriptions = descriptionBuilder.buildDescriptions(start, specification);
|
|
133
|
+
|
|
134
|
+
// Only generate SQL for satisfiable queries.
|
|
135
|
+
return descriptions
|
|
136
|
+
.filter(description => description.isSatisfiable())
|
|
137
|
+
.map(description => description.generateSqlQuery(bookmarks, limit));
|
|
138
|
+
}
|