jinaga-server 2.4.2 → 2.6.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 (62) hide show
  1. package/dist/authorization/authorization-keystore.d.ts +5 -4
  2. package/dist/authorization/authorization-keystore.js +16 -13
  3. package/dist/authorization/authorization-keystore.js.map +1 -1
  4. package/dist/http/router.d.ts +5 -3
  5. package/dist/http/router.js +125 -52
  6. package/dist/http/router.js.map +1 -1
  7. package/dist/http/stream.d.ts +10 -0
  8. package/dist/http/stream.js +35 -0
  9. package/dist/http/stream.js.map +1 -0
  10. package/dist/index.d.ts +2 -3
  11. package/dist/index.js +3 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/jinaga-server.js +7 -8
  14. package/dist/jinaga-server.js.map +1 -1
  15. package/dist/postgres/connection.js +1 -3
  16. package/dist/postgres/connection.js.map +1 -1
  17. package/dist/postgres/postgres-store.d.ts +4 -4
  18. package/dist/postgres/postgres-store.js +2 -2
  19. package/dist/postgres/postgres-store.js.map +1 -1
  20. package/dist/postgres/query-description.d.ts +75 -0
  21. package/dist/postgres/query-description.js +283 -0
  22. package/dist/postgres/query-description.js.map +1 -0
  23. package/dist/postgres/specification-result-sql.d.ts +2 -2
  24. package/dist/postgres/specification-result-sql.js +39 -324
  25. package/dist/postgres/specification-result-sql.js.map +1 -1
  26. package/dist/postgres/specification-sql.d.ts +2 -2
  27. package/dist/postgres/specification-sql.js +60 -163
  28. package/dist/postgres/specification-sql.js.map +1 -1
  29. package/http/sans-auth/.env.local +1 -0
  30. package/http/sans-auth/Create post sans auth.http +27 -0
  31. package/http/sans-auth/Create site sans auth.http +11 -0
  32. package/http/sans-auth/Feed sites.http +1 -0
  33. package/http/sans-auth/Feeds sites.http +12 -0
  34. package/http/sans-auth/Get posts sans auth.http +35 -0
  35. package/http/sans-auth/Get sites sans auth.http +12 -0
  36. package/http/sans-auth/stream.sh +2 -0
  37. package/integration-test/jinaga-test/Dockerfile +1 -1
  38. package/integration-test/jinaga-test/package-lock.json +917 -748
  39. package/integration-test/jinaga-test/package.json +3 -3
  40. package/package.json +2 -2
  41. package/replicator/package-lock.json +16 -16
  42. package/replicator/package.json +1 -1
  43. package/src/authorization/authorization-keystore.ts +23 -14
  44. package/src/http/router.ts +154 -64
  45. package/src/http/stream.ts +32 -0
  46. package/src/index.ts +3 -4
  47. package/src/jinaga-server.ts +8 -8
  48. package/src/postgres/connection.ts +1 -3
  49. package/src/postgres/postgres-store.ts +5 -8
  50. package/src/postgres/query-description.ts +435 -0
  51. package/src/postgres/specification-result-sql.ts +56 -489
  52. package/src/postgres/specification-sql.ts +71 -232
  53. package/test/authorization/authorizationSpec.ts +12 -2
  54. package/test/postgres/postgresSpecificationSpec.ts +15 -15
  55. package/dist/http/feed-cache.d.ts +0 -12
  56. package/dist/http/feed-cache.js +0 -3
  57. package/dist/http/feed-cache.js.map +0 -1
  58. package/dist/memory/memory-feed-cache.d.ts +0 -6
  59. package/dist/memory/memory-feed-cache.js +0 -17
  60. package/dist/memory/memory-feed-cache.js.map +0 -1
  61. package/src/http/feed-cache.ts +0 -14
  62. package/src/memory/memory-feed-cache.ts +0 -14
@@ -8,327 +8,50 @@ import {
8
8
  hydrateFromTree,
9
9
  Label,
10
10
  Match,
11
- PathCondition,
12
11
  PredecessorCollection,
13
12
  ProjectedResult,
14
13
  Projection,
15
14
  ReferencesByName,
16
15
  SingularProjection,
17
16
  Specification,
18
- SpecificationProjection
17
+ SpecificationProjection,
18
+ validateGiven
19
19
  } from "jinaga";
20
20
 
21
- import { ensureGetFactTypeId, FactTypeMap, getFactTypeId, getRoleId, RoleMap } from "./maps";
22
-
23
- interface SpecificationLabel {
24
- name: string;
25
- index: number;
26
- type: string;
27
- }
28
- interface FactDescription {
29
- type: string;
30
- factIndex: number;
31
- }
32
- interface EdgeDescription {
33
- edgeIndex: number;
34
- predecessorFactIndex: number;
35
- successorFactIndex: number;
36
- roleParameter: number;
37
- }
38
- interface ExistentialConditionDescription {
39
- exists: boolean;
40
- inputs: InputDescription[];
41
- edges: EdgeDescription[];
42
- existentialConditions: ExistentialConditionDescription[];
43
- }
44
- interface SpecificationSqlQuery {
45
- sql: string;
46
- parameters: (string | number | number[])[];
47
- labels: SpecificationLabel[];
48
- bookmark: string;
49
- };
50
- interface InputDescription {
51
- label: string;
52
- type: string;
53
- factIndex: number;
54
- factTypeParameter: number;
55
- factHashParameter: number;
56
- }
57
- interface OutputDescription {
58
- label: string;
59
- type: string;
60
- factIndex: number;
61
- }
62
-
63
- function countEdges(existentialConditions: ExistentialConditionDescription[]): number {
64
- return existentialConditions.reduce((count, c) => count + c.edges.length + countEdges(c.existentialConditions),
65
- 0);
66
- }
67
-
68
- class QueryDescription {
69
- // An unsatisfiable query description will produce no results.
70
- static unsatisfiable: QueryDescription = new QueryDescription(
71
- [], [], [], [], [], []
72
- );
73
-
74
- constructor(
75
- private readonly inputs: InputDescription[],
76
- private readonly parameters: (string | number)[],
77
- private readonly outputs: OutputDescription[],
78
- private readonly facts: FactDescription[],
79
- private readonly edges: EdgeDescription[],
80
- private readonly existentialConditions: ExistentialConditionDescription[] = []
81
- ) {}
82
-
83
- public withParameter(parameter: string | number): { query: QueryDescription; parameterIndex: number; } {
84
- const parameterIndex = this.parameters.length + 1;
85
- const query = new QueryDescription(
86
- this.inputs,
87
- this.parameters.concat(parameter),
88
- this.outputs,
89
- this.facts,
90
- this.edges,
91
- this.existentialConditions
92
- );
93
- return { query, parameterIndex };
94
- }
95
-
96
- public withInputParameter(label: Label, factTypeId: number, factHash: string, path: number[]): { queryDescription: QueryDescription, factDescription: FactDescription } {
97
- const factTypeParameter = this.parameters.length + 1;
98
- const factHashParameter = factTypeParameter + 1;
99
- const factIndex = this.facts.length + 1;
100
- const factDescription: FactDescription = {
101
- factIndex: factIndex,
102
- type: label.type
103
- };
104
- const facts = [
105
- ...this.facts,
106
- factDescription
107
- ]
108
- const input: InputDescription = {
109
- label: label.name,
21
+ import { FactTypeMap, RoleMap } from "./maps";
22
+ import { EdgeDescription, ExistentialConditionDescription, FactByLabel, QueryDescription, QueryDescriptionBuider } from "./query-description";
23
+
24
+ function generateResultSqlQuery(queryDescription: QueryDescription, schema: string): SpecificationSqlQuery {
25
+ const allLabels = [ ...queryDescription.inputs, ...queryDescription.outputs ];
26
+ const columns = allLabels
27
+ .map(label => `f${label.factIndex}.hash as hash${label.factIndex}, f${label.factIndex}.fact_id as id${label.factIndex}, f${label.factIndex}.data as data${label.factIndex}`)
28
+ .join(", ");
29
+ const firstEdge = queryDescription.edges[0];
30
+ const predecessorFact = queryDescription.inputs.find(i => i.factIndex === firstEdge.predecessorFactIndex);
31
+ const successorFact = queryDescription.inputs.find(i => i.factIndex === firstEdge.successorFactIndex);
32
+ const firstFactIndex = predecessorFact ? predecessorFact.factIndex : successorFact!.factIndex;
33
+ const writtenFactIndexes = new Set<number>().add(firstFactIndex);
34
+ const joins: string[] = generateJoins(queryDescription.edges, writtenFactIndexes, schema);
35
+ const inputWhereClauses = queryDescription.inputs
36
+ .map(input => `f${input.factIndex}.fact_type_id = $${input.factTypeParameter} AND f${input.factIndex}.hash = $${input.factHashParameter}`)
37
+ .join(" AND ");
38
+ const existentialWhereClauses = queryDescription.existentialConditions
39
+ .map(existentialCondition => ` AND ${existentialCondition.exists ? "EXISTS" : "NOT EXISTS"} (${generateExistentialWhereClause(existentialCondition, writtenFactIndexes, schema)})`)
40
+ .join("");
41
+ const orderByClause = queryDescription.outputs
42
+ .map(output => `f${output.factIndex}.fact_id ASC`)
43
+ .join(", ");
44
+ const sql = `SELECT ${columns} FROM ${schema}.fact f${firstFactIndex}${joins.join("")} WHERE ${inputWhereClauses}${existentialWhereClauses} ORDER BY ${orderByClause}`;
45
+ return {
46
+ sql,
47
+ parameters: queryDescription.parameters,
48
+ labels: allLabels.map(label => ({
49
+ name: label.label,
110
50
  type: label.type,
111
- factIndex,
112
- factTypeParameter,
113
- factHashParameter
114
- };
115
- const parameters = this.parameters.concat(factTypeId, factHash);
116
- if (path.length === 0) {
117
- const inputs = [
118
- ...this.inputs,
119
- input
120
- ];
121
- const queryDescription = new QueryDescription(
122
- inputs,
123
- parameters,
124
- this.outputs,
125
- facts,
126
- this.edges,
127
- this.existentialConditions
128
- );
129
- return { queryDescription, factDescription };
130
- }
131
- else {
132
- const existentialConditions = existentialsWithInput(this.existentialConditions, input, path);
133
- const queryDescription = new QueryDescription(
134
- this.inputs,
135
- parameters,
136
- this.outputs,
137
- facts,
138
- this.edges,
139
- existentialConditions
140
- );
141
- return { queryDescription, factDescription };
142
- }
143
- }
144
-
145
- public withFact(type: string): { query: QueryDescription; factIndex: number; } {
146
- const factIndex = this.facts.length + 1;
147
- const fact = { factIndex, type };
148
- const query = new QueryDescription(
149
- this.inputs,
150
- this.parameters,
151
- this.outputs,
152
- this.facts.concat(fact),
153
- this.edges,
154
- this.existentialConditions
155
- );
156
- return { query, factIndex };
157
- }
158
-
159
- public withOutput(label: string, type: string, factIndex: number): QueryDescription {
160
- const output = { label, type, factIndex };
161
- const query = new QueryDescription(
162
- this.inputs,
163
- this.parameters,
164
- this.outputs.concat(output),
165
- this.facts,
166
- this.edges,
167
- this.existentialConditions
168
- );
169
- return query;
170
- }
171
-
172
- public withEdge(predecessorFactIndex: number, successorFactIndex: number, roleParameter: number, path: number[]) {
173
- const edge = {
174
- edgeIndex: this.edges.length + countEdges(this.existentialConditions) + 1,
175
- predecessorFactIndex,
176
- successorFactIndex,
177
- roleParameter
178
- };
179
- const query = (path.length === 0)
180
- ? new QueryDescription(
181
- this.inputs,
182
- this.parameters,
183
- this.outputs,
184
- this.facts,
185
- this.edges.concat(edge),
186
- this.existentialConditions
187
- )
188
- : new QueryDescription(
189
- this.inputs,
190
- this.parameters,
191
- this.outputs,
192
- this.facts,
193
- this.edges,
194
- existentialsWithEdge(this.existentialConditions, edge, path)
195
- );
196
- return query;
197
- }
198
-
199
- public withExistentialCondition(exists: boolean, path: number[]): { query: QueryDescription; path: number[]; } {
200
- const { existentialConditions: newExistentialConditions, path: newPath } = existentialsWithNewCondition(this.existentialConditions, exists, path);
201
- const query = new QueryDescription(
202
- this.inputs,
203
- this.parameters,
204
- this.outputs,
205
- this.facts,
206
- this.edges,
207
- newExistentialConditions
208
- );
209
- return { query, path: newPath };
210
- }
211
-
212
- isSatisfiable() {
213
- return this.inputs.length > 0;
214
- }
215
-
216
- hasOutput(label: string) {
217
- return this.outputs.some(o => o.label === label);
218
- }
219
-
220
- inputByLabel(label: string): InputDescription | undefined {
221
- return this.inputs.find(i => i.label === label);
222
- }
223
-
224
- outputLength(): number {
225
- return this.inputs.length + this.outputs.length;
226
- }
227
-
228
- generateResultSqlQuery(schema: string): SpecificationSqlQuery {
229
- const allLabels = [ ...this.inputs, ...this.outputs ];
230
- const columns = allLabels
231
- .map(label => `f${label.factIndex}.hash as hash${label.factIndex}, f${label.factIndex}.fact_id as id${label.factIndex}, f${label.factIndex}.data as data${label.factIndex}`)
232
- .join(", ");
233
- const firstEdge = this.edges[0];
234
- const predecessorFact = this.inputs.find(i => i.factIndex === firstEdge.predecessorFactIndex);
235
- const successorFact = this.inputs.find(i => i.factIndex === firstEdge.successorFactIndex);
236
- const firstFactIndex = predecessorFact ? predecessorFact.factIndex : successorFact!.factIndex;
237
- const writtenFactIndexes = new Set<number>().add(firstFactIndex);
238
- const joins: string[] = generateJoins(this.edges, writtenFactIndexes, schema);
239
- const inputWhereClauses = this.inputs
240
- .map(input => `f${input.factIndex}.fact_type_id = $${input.factTypeParameter} AND f${input.factIndex}.hash = $${input.factHashParameter}`)
241
- .join(" AND ");
242
- const existentialWhereClauses = this.existentialConditions
243
- .map(existentialCondition => ` AND ${existentialCondition.exists ? "EXISTS" : "NOT EXISTS"} (${generateExistentialWhereClause(existentialCondition, writtenFactIndexes, schema)})`)
244
- .join("");
245
- const orderByClause = this.outputs
246
- .map(output => `f${output.factIndex}.fact_id ASC`)
247
- .join(", ");
248
- const sql = `SELECT ${columns} FROM ${schema}.fact f${firstFactIndex}${joins.join("")} WHERE ${inputWhereClauses}${existentialWhereClauses} ORDER BY ${orderByClause}`;
249
- return {
250
- sql,
251
- parameters: this.parameters,
252
- labels: allLabels.map(label => ({
253
- name: label.label,
254
- type: label.type,
255
- index: label.factIndex
256
- })),
257
- bookmark: "[]"
258
- };
259
- }
260
- }
261
-
262
- function existentialsWithInput(existentialConditions: ExistentialConditionDescription[], input: InputDescription, path: number[]): ExistentialConditionDescription[] {
263
- if (path.length === 1) {
264
- return existentialConditions.map((c, i) => i === path[0] ?
265
- {
266
- ...c,
267
- inputs: [...c.inputs, input]
268
- } :
269
- c
270
- );
271
- }
272
- else {
273
- return existentialConditions.map((c, i) => i === path[0] ?
274
- {
275
- ...c,
276
- existentialConditions: existentialsWithInput(c.existentialConditions, input, path.slice(1))
277
- } :
278
- c
279
- );
280
- }
281
- }
282
-
283
- function existentialsWithEdge(existentialConditions: ExistentialConditionDescription[], edge: EdgeDescription, path: number[]): ExistentialConditionDescription[] {
284
- if (path.length === 1) {
285
- return existentialConditions.map((c, i) => i === path[0] ?
286
- {
287
- ...c,
288
- edges: [...c.edges, edge]
289
- } :
290
- c
291
- );
292
- }
293
- else {
294
- return existentialConditions.map((c, i) => i === path[0] ?
295
- {
296
- ...c,
297
- existentialConditions: existentialsWithEdge(c.existentialConditions, edge, path.slice(1))
298
- } :
299
- c
300
- );
301
- }
302
- }
303
-
304
- function existentialsWithNewCondition(existentialConditions: ExistentialConditionDescription[], exists: boolean, path: number[]): { existentialConditions: ExistentialConditionDescription[]; path: number[]; } {
305
- if (path.length === 0) {
306
- path = [existentialConditions.length];
307
- existentialConditions = [
308
- ...existentialConditions,
309
- {
310
- exists: exists,
311
- inputs: [],
312
- edges: [],
313
- existentialConditions: []
314
- }
315
- ];
316
- return { existentialConditions: existentialConditions, path };
317
- }
318
- else {
319
- const { existentialConditions: newExistentialConditions, path: newPath } = existentialsWithNewCondition(existentialConditions[path[0]].existentialConditions, exists, path.slice(1));
320
- existentialConditions = existentialConditions.map((c, i) => i === path[0] ?
321
- {
322
- exists: c.exists,
323
- inputs: c.inputs,
324
- edges: c.edges,
325
- existentialConditions: newExistentialConditions
326
- } :
327
- c
328
- );
329
- path = [path[0], ...newPath];
330
- return { existentialConditions: existentialConditions, path };
331
- }
51
+ index: label.factIndex
52
+ })),
53
+ bookmark: "[]"
54
+ };
332
55
  }
333
56
 
334
57
  function generateJoins(edges: EdgeDescription[], writtenFactIndexes: Set<number>, schema: string) {
@@ -421,10 +144,6 @@ function generateExistentialWhereClause(existentialCondition: ExistentialConditi
421
144
  return `SELECT 1 FROM ${schema}.edge e${firstEdge.edgeIndex}${joins.join("")} WHERE ${whereClause.join(" AND ")}${inputWhereClauses}${existentialWhereClauses}`;
422
145
  }
423
146
 
424
- type FactByLabel = {
425
- [label: string]: FactDescription;
426
- };
427
-
428
147
  interface ResultDescription {
429
148
  queryDescription: QueryDescription;
430
149
  resultProjection: Projection;
@@ -447,6 +166,19 @@ interface ChildResults {
447
166
  results: ProjectedResult[];
448
167
  }
449
168
 
169
+ export interface SpecificationLabel {
170
+ name: string;
171
+ index: number;
172
+ type: string;
173
+ }
174
+
175
+ export interface SpecificationSqlQuery {
176
+ sql: string;
177
+ parameters: (string | number | number[])[];
178
+ labels: SpecificationLabel[];
179
+ bookmark: string;
180
+ }
181
+
450
182
  export interface SqlQueryTree {
451
183
  sqlQuery: SpecificationSqlQuery;
452
184
  childQueries: NamedSqlQueryTree[];
@@ -749,28 +481,13 @@ interface NamedResultComposer {
749
481
 
750
482
  class ResultDescriptionBuilder {
751
483
  constructor(
752
- private factTypes: FactTypeMap,
753
- private roleMap: RoleMap
484
+ private queryDescriptionBuilder: QueryDescriptionBuider
754
485
  ) { }
755
486
 
756
487
  buildDescription(start: FactReference[], specification: Specification): ResultDescription {
757
- // Verify that the number of start facts equals the number of inputs
758
- if (start.length !== specification.given.length) {
759
- throw new Error(`The number of start facts (${start.length}) does not equal the number of inputs (${specification.given.length})`);
760
- }
761
- // Verify that the input type matches the start fact type
762
- for (let i = 0; i < start.length; i++) {
763
- if (start[i].type !== specification.given[i].type) {
764
- throw new Error(`The type of start fact ${i} (${start[i].type}) does not match the type of input ${i} (${specification.given[i].type})`);
765
- }
766
- }
488
+ validateGiven(start, specification);
767
489
 
768
- // The QueryDescription is an immutable data type.
769
- // Initialize it with the inputs and facts.
770
- // The DescriptionBuilder will branch at various points, and
771
- // build on the current query description along each branch.
772
- const initialQueryDescription = new QueryDescription([], [], [], [], [], []);
773
- return this.createResultDescription(initialQueryDescription, specification.given, start, specification.matches, specification.projection, {}, []);
490
+ return this.createResultDescription(QueryDescription.unsatisfiable, specification.given, start, specification.matches, specification.projection, {}, []);
774
491
  }
775
492
 
776
493
  private createResultDescription(queryDescription: QueryDescription, given: Label[], start: FactReference[], matches: Match[], projection: Projection, knownFacts: FactByLabel, path: number[]): ResultDescription {
@@ -778,7 +495,7 @@ class ResultDescriptionBuilder {
778
495
  ...acc,
779
496
  [label.name]: start[index]
780
497
  }), {} as ReferencesByName);
781
- ({ queryDescription, knownFacts } = this.addEdges(queryDescription, given, start, knownFacts, path, matches));
498
+ ({ queryDescription, knownFacts } = this.queryDescriptionBuilder.addEdges(queryDescription, given, start, knownFacts, path, matches));
782
499
  if (!queryDescription.isSatisfiable()) {
783
500
  // Abort the branch if the query is not satisfiable
784
501
  return {
@@ -823,157 +540,6 @@ class ResultDescriptionBuilder {
823
540
  }
824
541
  }
825
542
  }
826
-
827
- private addEdges(queryDescription: QueryDescription, given: Label[], start: FactReference[], knownFacts: FactByLabel, path: number[], matches: Match[]): { queryDescription: QueryDescription, knownFacts: FactByLabel } {
828
- for (const match of matches) {
829
- for (const condition of match.conditions) {
830
- if (condition.type === "path") {
831
- ({queryDescription, knownFacts} = this.addPathCondition(queryDescription, given, start, knownFacts, path, match.unknown, "", condition));
832
- }
833
- else if (condition.type === "existential") {
834
- // Apply the where clause and continue with the tuple where it is true.
835
- // The path describes which not-exists condition we are currently building on.
836
- // Because the path is not empty, labeled facts will be included in the output.
837
- const { query: queryDescriptionWithExistential, path: conditionalPath } = queryDescription.withExistentialCondition(condition.exists, path);
838
- const { queryDescription: queryDescriptionConditional } = this.addEdges(queryDescriptionWithExistential, given, start, knownFacts, conditionalPath, condition.matches);
839
-
840
- // If the negative existential condition is not satisfiable, then
841
- // that means that the condition will always be true.
842
- // We can therefore skip the branch for the negative existential condition.
843
- if (queryDescriptionConditional.isSatisfiable()) {
844
- queryDescription = queryDescriptionConditional;
845
- }
846
- }
847
- if (!queryDescription.isSatisfiable()) {
848
- break;
849
- }
850
- }
851
- if (!queryDescription.isSatisfiable()) {
852
- break;
853
- }
854
- }
855
- return {
856
- queryDescription,
857
- knownFacts
858
- };
859
- }
860
-
861
- private addPathCondition(queryDescription: QueryDescription, given: Label[], start: FactReference[], knownFacts: FactByLabel, path: number[], unknown: Label, prefix: string, condition: PathCondition): { queryDescription: QueryDescription, knownFacts: FactByLabel } {
862
- // If no input parameter has been allocated, allocate one now.
863
- if (!knownFacts.hasOwnProperty(condition.labelRight)) {
864
- const givenIndex = given.findIndex(given => given.name === condition.labelRight);
865
- if (givenIndex < 0) {
866
- throw new Error(`No input parameter found for label ${condition.labelRight}`);
867
- }
868
- const { queryDescription: newQueryDescription, factDescription } = queryDescription.withInputParameter(
869
- given[givenIndex],
870
- ensureGetFactTypeId(this.factTypes, start[givenIndex].type),
871
- start[givenIndex].hash,
872
- path
873
- );
874
- queryDescription = newQueryDescription;
875
- knownFacts = {
876
- ...knownFacts,
877
- [condition.labelRight]: factDescription
878
- };
879
- }
880
-
881
- // Determine whether we have already written the output.
882
- const knownFact = knownFacts[unknown.name];
883
- const roleCount = condition.rolesLeft.length + condition.rolesRight.length;
884
-
885
- // Walk up the right-hand side.
886
- // This generates predecessor joins from a given or prior label.
887
- let fact = knownFacts[condition.labelRight];
888
- if (!fact) {
889
- throw new Error(`Label ${condition.labelRight} not found. Known labels: ${Object.keys(knownFacts).join(", ")}`);
890
- }
891
- let type = fact.type;
892
- let factIndex = fact.factIndex;
893
- for (const [i, role] of condition.rolesRight.entries()) {
894
- // If the type or role is not known, then no facts matching the condition can
895
- // exist. The query is unsatisfiable.
896
- const typeId = getFactTypeId(this.factTypes, type);
897
- if (!typeId) {
898
- return { queryDescription: QueryDescription.unsatisfiable, knownFacts };
899
- }
900
- const roleId = getRoleId(this.roleMap, typeId, role.name);
901
- if (!roleId) {
902
- return { queryDescription: QueryDescription.unsatisfiable, knownFacts };
903
- }
904
-
905
- const { query: queryWithParameter, parameterIndex: roleParameter } = queryDescription.withParameter(roleId);
906
- if (i === roleCount - 1 && knownFact) {
907
- // If we have already written the output, we can use the fact index.
908
- queryDescription = queryWithParameter.withEdge(knownFact.factIndex, factIndex, roleParameter, path);
909
- factIndex = knownFact.factIndex;
910
- }
911
- else {
912
- // If we have not written the fact, we need to write it now.
913
- const { query, factIndex: predecessorFactIndex } = queryWithParameter.withFact(role.predecessorType);
914
- queryDescription = query.withEdge(predecessorFactIndex, factIndex, roleParameter, path);
915
- factIndex = predecessorFactIndex;
916
- }
917
- type = role.predecessorType;
918
- }
919
-
920
- const rightType = type;
921
-
922
- // Walk up the left-hand side.
923
- // We will need to reverse this walk to generate successor joins.
924
- type = unknown.type;
925
- const newEdges: {
926
- roleId: number;
927
- declaringType: string;
928
- }[] = [];
929
- for (const role of condition.rolesLeft) {
930
- // If the type or role is not known, then no facts matching the condition can
931
- // exist. The query is unsatisfiable.
932
- const typeId = getFactTypeId(this.factTypes, type);
933
- if (!typeId) {
934
- return { queryDescription: QueryDescription.unsatisfiable, knownFacts };
935
- }
936
- const roleId = getRoleId(this.roleMap, typeId, role.name);
937
- if (!roleId) {
938
- return { queryDescription: QueryDescription.unsatisfiable, knownFacts };
939
- }
940
-
941
- newEdges.push({
942
- roleId,
943
- declaringType: type
944
- });
945
- type = role.predecessorType;
946
- }
947
-
948
- if (type !== rightType) {
949
- throw new Error(`Type mismatch: ${type} is compared to ${rightType}`);
950
- }
951
-
952
- newEdges.reverse().forEach(({ roleId, declaringType }, i) => {
953
- const { query: queryWithParameter, parameterIndex: roleParameter } = queryDescription.withParameter(roleId);
954
- if (condition.rolesRight.length + i === roleCount - 1 && knownFact) {
955
- queryDescription = queryWithParameter.withEdge(factIndex, knownFact.factIndex, roleParameter, path);
956
- factIndex = knownFact.factIndex;
957
- }
958
- else {
959
- const { query: queryWithFact, factIndex: successorFactIndex } = queryWithParameter.withFact(declaringType);
960
- queryDescription = queryWithFact.withEdge(factIndex, successorFactIndex, roleParameter, path);
961
- factIndex = successorFactIndex;
962
- }
963
- });
964
-
965
- // If we have not captured the known fact, add it now.
966
- if (!knownFact) {
967
- knownFacts = { ...knownFacts, [unknown.name]: { factIndex, type: unknown.type } };
968
- // If we have not written the output, write it now.
969
- // Only write the output if we are not inside of an existential condition.
970
- // Use the prefix, which will be set for projections.
971
- if (path.length === 0) {
972
- queryDescription = queryDescription.withOutput(prefix + unknown.name, unknown.type, factIndex);
973
- }
974
- }
975
- return { queryDescription, knownFacts };
976
- }
977
543
  }
978
544
 
979
545
  function idsEqual(a: number[], b: number[]) {
@@ -981,7 +547,8 @@ function idsEqual(a: number[], b: number[]) {
981
547
  }
982
548
 
983
549
  export function resultSqlFromSpecification(start: FactReference[], specification: Specification, factTypes: FactTypeMap, roleMap: RoleMap, schema: string): ResultComposer | null {
984
- const descriptionBuilder = new ResultDescriptionBuilder(factTypes, roleMap);
550
+ const queryDescriptionBuilder = new QueryDescriptionBuider(factTypes, roleMap);
551
+ const descriptionBuilder = new ResultDescriptionBuilder(queryDescriptionBuilder);
985
552
  const description = descriptionBuilder.buildDescription(start, specification);
986
553
 
987
554
  if (!description.queryDescription.isSatisfiable()) {
@@ -991,7 +558,7 @@ export function resultSqlFromSpecification(start: FactReference[], specification
991
558
  }
992
559
 
993
560
  function createResultComposer(description: ResultDescription, parentFactIdLength: number, schema: string): ResultComposer {
994
- const sqlQuery = description.queryDescription.generateResultSqlQuery(schema);
561
+ const sqlQuery = generateResultSqlQuery(description.queryDescription, schema);
995
562
  const resultProjection = description.resultProjection;
996
563
  const childResultComposers = description.childResultDescriptions
997
564
  .filter(child => child.queryDescription.isSatisfiable())
@@ -1000,4 +567,4 @@ function createResultComposer(description: ResultDescription, parentFactIdLength
1000
567
  resultComposer: createResultComposer(child, description.queryDescription.outputLength(), schema)
1001
568
  }));
1002
569
  return new ResultComposer(sqlQuery, resultProjection, parentFactIdLength, description.givenTuple, childResultComposers);
1003
- }
570
+ }