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.
Files changed (127) hide show
  1. package/LICENSE +22 -0
  2. package/dist/authentication/authentication-device.d.ts +20 -0
  3. package/dist/authentication/authentication-device.js +61 -0
  4. package/dist/authentication/authentication-device.js.map +1 -0
  5. package/dist/authentication/authentication-impl.d.ts +21 -0
  6. package/dist/authentication/authentication-impl.js +61 -0
  7. package/dist/authentication/authentication-impl.js.map +1 -0
  8. package/dist/authentication/authentication-session.d.ts +23 -0
  9. package/dist/authentication/authentication-session.js +85 -0
  10. package/dist/authentication/authentication-session.js.map +1 -0
  11. package/dist/authentication/authentication.d.ts +5 -0
  12. package/dist/authentication/authentication.js +3 -0
  13. package/dist/authentication/authentication.js.map +1 -0
  14. package/dist/authentication.d.ts +17 -0
  15. package/dist/authentication.js +52 -0
  16. package/dist/authentication.js.map +1 -0
  17. package/dist/authorization/authorization-keystore.d.ts +13 -0
  18. package/dist/authorization/authorization-keystore.js +61 -0
  19. package/dist/authorization/authorization-keystore.js.map +1 -0
  20. package/dist/http/node-http.d.ts +7 -0
  21. package/dist/http/node-http.js +126 -0
  22. package/dist/http/node-http.js.map +1 -0
  23. package/dist/http/router.d.ts +20 -0
  24. package/dist/http/router.js +223 -0
  25. package/dist/http/router.js.map +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +6 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/jinaga-server.d.ts +18 -0
  30. package/dist/jinaga-server.js +101 -0
  31. package/dist/jinaga-server.js.map +1 -0
  32. package/dist/keystore.d.ts +9 -0
  33. package/dist/keystore.js +3 -0
  34. package/dist/keystore.js.map +1 -0
  35. package/dist/memory/memory-keystore.d.ts +16 -0
  36. package/dist/memory/memory-keystore.js +86 -0
  37. package/dist/memory/memory-keystore.js.map +1 -0
  38. package/dist/postgres/connection.d.ts +12 -0
  39. package/dist/postgres/connection.js +83 -0
  40. package/dist/postgres/connection.js.map +1 -0
  41. package/dist/postgres/edge-record.d.ts +9 -0
  42. package/dist/postgres/edge-record.js +30 -0
  43. package/dist/postgres/edge-record.js.map +1 -0
  44. package/dist/postgres/maps.d.ts +21 -0
  45. package/dist/postgres/maps.js +86 -0
  46. package/dist/postgres/maps.js.map +1 -0
  47. package/dist/postgres/postgres-keystore.d.ts +20 -0
  48. package/dist/postgres/postgres-keystore.js +168 -0
  49. package/dist/postgres/postgres-keystore.js.map +1 -0
  50. package/dist/postgres/postgres-store.d.ts +20 -0
  51. package/dist/postgres/postgres-store.js +717 -0
  52. package/dist/postgres/postgres-store.js.map +1 -0
  53. package/dist/postgres/query-description-builder.d.ts +13 -0
  54. package/dist/postgres/query-description-builder.js +104 -0
  55. package/dist/postgres/query-description-builder.js.map +1 -0
  56. package/dist/postgres/query-description.d.ts +71 -0
  57. package/dist/postgres/query-description.js +256 -0
  58. package/dist/postgres/query-description.js.map +1 -0
  59. package/dist/postgres/specification-result-sql.d.ts +41 -0
  60. package/dist/postgres/specification-result-sql.js +260 -0
  61. package/dist/postgres/specification-result-sql.js.map +1 -0
  62. package/dist/postgres/specification-sql.d.ts +3 -0
  63. package/dist/postgres/specification-sql.js +119 -0
  64. package/dist/postgres/specification-sql.js.map +1 -0
  65. package/dist/postgres/sql.d.ts +10 -0
  66. package/dist/postgres/sql.js +396 -0
  67. package/dist/postgres/sql.js.map +1 -0
  68. package/dist/util/fn.d.ts +6 -0
  69. package/dist/util/fn.js +81 -0
  70. package/dist/util/fn.js.map +1 -0
  71. package/dist/util/promise.d.ts +1 -0
  72. package/dist/util/promise.js +10 -0
  73. package/dist/util/promise.js.map +1 -0
  74. package/docs/registrations.sql +22 -0
  75. package/docs/specification.txt +41 -0
  76. package/docs/sql-generator.gv +25 -0
  77. package/integration-test/docker-compose.yaml +13 -0
  78. package/integration-test/jinaga-test/Dockerfile +12 -0
  79. package/integration-test/jinaga-test/jinaga.test.js +425 -0
  80. package/integration-test/jinaga-test/package-lock.json +6767 -0
  81. package/integration-test/jinaga-test/package.json +25 -0
  82. package/integration-test/postgres/0-init-app-db.sh +16 -0
  83. package/integration-test/postgres/1-setup.sql +219 -0
  84. package/integration-test/postgres/2-grant.sh +17 -0
  85. package/integration-test/postgres/Dockerfile +5 -0
  86. package/integration-test/run.sh +17 -0
  87. package/integration-test/start_db.sh +2 -0
  88. package/integration-test/stop_db.sh +3 -0
  89. package/integration-test/test.sh +8 -0
  90. package/interactive/index.js +56 -0
  91. package/jest.config.js +12 -0
  92. package/package.json +51 -0
  93. package/scripts/combined.sql +46 -0
  94. package/scripts/cte.sql +55 -0
  95. package/scripts/separate.sql +27 -0
  96. package/setup.sh +34 -0
  97. package/setup.sql +329 -0
  98. package/src/authentication/authentication-device.ts +68 -0
  99. package/src/authentication/authentication-impl.ts +59 -0
  100. package/src/authentication/authentication-session.ts +95 -0
  101. package/src/authentication/authentication.ts +6 -0
  102. package/src/authentication.ts +45 -0
  103. package/src/authorization/authorization-keystore.ts +67 -0
  104. package/src/http/node-http.ts +106 -0
  105. package/src/http/router.ts +237 -0
  106. package/src/index.ts +1 -0
  107. package/src/jinaga-server.ts +125 -0
  108. package/src/keystore.ts +10 -0
  109. package/src/memory/memory-keystore.ts +93 -0
  110. package/src/postgres/connection.ts +73 -0
  111. package/src/postgres/edge-record.ts +36 -0
  112. package/src/postgres/maps.ts +89 -0
  113. package/src/postgres/postgres-keystore.ts +167 -0
  114. package/src/postgres/postgres-store.ts +874 -0
  115. package/src/postgres/query-description-builder.ts +116 -0
  116. package/src/postgres/query-description.ts +385 -0
  117. package/src/postgres/specification-result-sql.ts +362 -0
  118. package/src/postgres/specification-sql.ts +138 -0
  119. package/src/postgres/sql.ts +482 -0
  120. package/src/util/fn.ts +63 -0
  121. package/src/util/promise.ts +5 -0
  122. package/test/authorization/authorizationSpec.ts +209 -0
  123. package/test/postgres/postgresSpec.ts +385 -0
  124. package/test/postgres/postgresSpecificationSpec.ts +742 -0
  125. package/test/reporter.js +25 -0
  126. package/tsconfig.json +25 -0
  127. 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
+ }