jinaga-server 3.5.2 → 3.5.3

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 (57) hide show
  1. package/dist/authorization/authorization-keystore.js +2 -2
  2. package/dist/authorization/authorization-keystore.js.map +1 -1
  3. package/dist/http/csv-metadata.d.ts +24 -0
  4. package/dist/http/csv-metadata.js +3 -0
  5. package/dist/http/csv-metadata.js.map +1 -0
  6. package/dist/http/csv-validator.d.ts +23 -0
  7. package/dist/http/csv-validator.js +83 -0
  8. package/dist/http/csv-validator.js.map +1 -0
  9. package/dist/http/output-formatters.d.ts +28 -0
  10. package/dist/http/output-formatters.js +197 -0
  11. package/dist/http/output-formatters.js.map +1 -0
  12. package/dist/http/result-stream.d.ts +35 -0
  13. package/dist/http/result-stream.js +78 -0
  14. package/dist/http/result-stream.js.map +1 -0
  15. package/dist/http/router.d.ts +7 -1
  16. package/dist/http/router.js +93 -46
  17. package/dist/http/router.js.map +1 -1
  18. package/dist/postgres/postgres-store.js +7 -5
  19. package/dist/postgres/postgres-store.js.map +1 -1
  20. package/dist/postgres/purge-sql.js +15 -2
  21. package/dist/postgres/purge-sql.js.map +1 -1
  22. package/dist/postgres/specification-result-sql.d.ts +2 -0
  23. package/dist/postgres/specification-result-sql.js +49 -5
  24. package/dist/postgres/specification-result-sql.js.map +1 -1
  25. package/dist/postgres/specification-sql.js +28 -6
  26. package/dist/postgres/specification-sql.js.map +1 -1
  27. package/docs/design/feeds-persistent-proxy-plan.md +294 -0
  28. package/integration-test/integration-test/jinaga-test/package-lock.json +6 -0
  29. package/integration-test/jinaga-test/Dockerfile +1 -6
  30. package/integration-test/jinaga-test/http-test-helpers.js +77 -0
  31. package/integration-test/jinaga-test/jinaga.test.js +9 -9
  32. package/integration-test/jinaga-test/package-lock.json +329 -32
  33. package/integration-test/jinaga-test/package.json +5 -1
  34. package/integration-test/jinaga-test/read-csv.test.js +330 -0
  35. package/integration-test/jinaga-test/read-endpoint.test.js +442 -0
  36. package/integration-test/jinaga-test/read-performance.test.js +221 -0
  37. package/integration-test/jinaga-test/read-streaming.test.js +199 -0
  38. package/integration-test/jinaga-test/specification-conditions.test.js +147 -0
  39. package/integration-test/jinaga-test/test-helpers.js +12 -0
  40. package/integration-test/jinaga-test/time-projection.test.js +235 -0
  41. package/package.json +3 -2
  42. package/src/authorization/authorization-keystore.ts +4 -4
  43. package/src/http/csv-metadata.ts +34 -0
  44. package/src/http/csv-validator.ts +90 -0
  45. package/src/http/output-formatters.ts +196 -0
  46. package/src/http/result-stream.ts +69 -0
  47. package/src/http/router.ts +112 -33
  48. package/src/postgres/postgres-store.ts +13 -6
  49. package/src/postgres/purge-sql.ts +18 -2
  50. package/src/postgres/specification-result-sql.ts +70 -8
  51. package/src/postgres/specification-sql.ts +36 -8
  52. package/test/http/csv-validatorSpec.ts +205 -0
  53. package/test/http/result-streamSpec.ts +151 -0
  54. package/test/postgres/postgresReadSpec.ts +192 -14
  55. package/test/postgres/postgresSpecificationSpec.ts +3 -3
  56. package/test/postgres/specificationConditionsSpec.ts +268 -0
  57. package/documentation/streaming-feeds-full-results-solution.md +0 -230
@@ -35,13 +35,13 @@ function sqlFor(descriptiveString: string, bookmarks: string[] = []) {
35
35
  },
36
36
  emptyRoleMap());
37
37
  const start = specification.given.map(input => {
38
- if (input.type === "Company") {
38
+ if (input.label.type === "Company") {
39
39
  return company;
40
40
  }
41
- if (input.type === "Jinaga.User") {
41
+ if (input.label.type === "Jinaga.User") {
42
42
  return user;
43
43
  }
44
- throw new Error(`Unknown input type ${input.type}`);
44
+ throw new Error(`Unknown input type ${input.label.type}`);
45
45
  });
46
46
  const composer = resultSqlFromSpecification(start, specification, factTypes, roleMap, "public");
47
47
  if (!composer) {
@@ -75,8 +75,8 @@ describe("Postgres read", () => {
75
75
  const sql = composer.getSqlQueries().sqlQuery.sql;
76
76
  expect(sql).toEqual(
77
77
  `SELECT ` +
78
- `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, ` +
79
- `f2.hash as hash2, f2.fact_id as id2, f2.data as data2 ` +
78
+ `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
79
+ `f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
80
80
  `FROM public.fact f1 ` +
81
81
  `JOIN public.edge e1 ` +
82
82
  `ON e1.predecessor_fact_id = f1.fact_id ` +
@@ -106,8 +106,8 @@ describe("Postgres read", () => {
106
106
  const sql = tree.sqlQuery.sql;
107
107
  expect(sql).toEqual(
108
108
  `SELECT ` +
109
- `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, ` +
110
- `f3.hash as hash3, f3.fact_id as id3, f3.data as data3 ` +
109
+ `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
110
+ `f3.hash as hash3, f3.fact_id as id3, f3.data as data3, f3.date_learned as timestamp3 ` +
111
111
  `FROM public.fact f1 ` +
112
112
  `JOIN public.edge e1 ` +
113
113
  `ON e1.predecessor_fact_id = f1.fact_id ` +
@@ -157,8 +157,8 @@ describe("Postgres read", () => {
157
157
  const sql = tree.sqlQuery.sql;
158
158
  expect(sql).toEqual(
159
159
  `SELECT ` +
160
- `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, ` +
161
- `f3.hash as hash3, f3.fact_id as id3, f3.data as data3 ` +
160
+ `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
161
+ `f3.hash as hash3, f3.fact_id as id3, f3.data as data3, f3.date_learned as timestamp3 ` +
162
162
  `FROM public.fact f1 ` +
163
163
  `JOIN public.edge e1 ` +
164
164
  `ON e1.predecessor_fact_id = f1.fact_id ` +
@@ -213,8 +213,8 @@ describe("Postgres read", () => {
213
213
  const sql = tree.sqlQuery.sql;
214
214
  expect(sql).toEqual(
215
215
  `SELECT ` +
216
- `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, ` +
217
- `f2.hash as hash2, f2.fact_id as id2, f2.data as data2 ` +
216
+ `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
217
+ `f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
218
218
  `FROM public.fact f1 ` +
219
219
  `JOIN public.edge e1 ` +
220
220
  `ON e1.predecessor_fact_id = f1.fact_id ` +
@@ -293,9 +293,9 @@ describe("Postgres read", () => {
293
293
  const queries = composer.getSqlQueries();
294
294
  expect(queries.sqlQuery.sql).toEqual(
295
295
  `SELECT ` +
296
- `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, ` +
297
- `f3.hash as hash3, f3.fact_id as id3, f3.data as data3, ` +
298
- `f9.hash as hash9, f9.fact_id as id9, f9.data as data9 ` +
296
+ `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
297
+ `f3.hash as hash3, f3.fact_id as id3, f3.data as data3, f3.date_learned as timestamp3, ` +
298
+ `f9.hash as hash9, f9.fact_id as id9, f9.data as data9, f9.date_learned as timestamp9 ` +
299
299
  `FROM public.fact f1 ` +
300
300
  `JOIN public.edge e1 ` +
301
301
  `ON e1.predecessor_fact_id = f1.fact_id ` +
@@ -349,4 +349,182 @@ describe("Postgres read", () => {
349
349
  `) ` +
350
350
  `ORDER BY f3.fact_id ASC, f9.fact_id ASC`);
351
351
  });
352
+
353
+ it("should read time projection for a successor", () => {
354
+ const { composer, factTypes, roleMap } = sqlFor(`
355
+ (company: Company) {
356
+ department: Department [
357
+ department->company: Company = company
358
+ ]
359
+ } => @department
360
+ `);
361
+
362
+ const tree = composer.getSqlQueries();
363
+ const sql = tree.sqlQuery.sql;
364
+ expect(sql).toEqual(
365
+ `SELECT ` +
366
+ `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
367
+ `f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
368
+ `FROM public.fact f1 ` +
369
+ `JOIN public.edge e1 ` +
370
+ `ON e1.predecessor_fact_id = f1.fact_id ` +
371
+ `AND e1.role_id = $3 ` +
372
+ `JOIN public.fact f2 ` +
373
+ `ON f2.fact_id = e1.successor_fact_id ` +
374
+ `WHERE f1.fact_type_id = $1 AND f1.hash = $2 ` +
375
+ `ORDER BY f2.fact_id ASC`
376
+ );
377
+ expect(tree.sqlQuery.parameters).toEqual([
378
+ getFactTypeId(factTypes, 'Company'),
379
+ companyHash,
380
+ roleParameter(roleMap, factTypes, 'Department', 'company')
381
+ ]);
382
+ });
383
+
384
+ it("should read time projection with field projection", () => {
385
+ const { composer, factTypes, roleMap } = sqlFor(`
386
+ (company: Company) {
387
+ department: Department [
388
+ department->company: Company = company
389
+ ]
390
+ } => {
391
+ name = department.name
392
+ createdAt = @department
393
+ }
394
+ `);
395
+
396
+ const tree = composer.getSqlQueries();
397
+ const sql = tree.sqlQuery.sql;
398
+ expect(sql).toEqual(
399
+ `SELECT ` +
400
+ `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
401
+ `f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
402
+ `FROM public.fact f1 ` +
403
+ `JOIN public.edge e1 ` +
404
+ `ON e1.predecessor_fact_id = f1.fact_id ` +
405
+ `AND e1.role_id = $3 ` +
406
+ `JOIN public.fact f2 ` +
407
+ `ON f2.fact_id = e1.successor_fact_id ` +
408
+ `WHERE f1.fact_type_id = $1 AND f1.hash = $2 ` +
409
+ `ORDER BY f2.fact_id ASC`
410
+ );
411
+ expect(tree.sqlQuery.parameters).toEqual([
412
+ getFactTypeId(factTypes, 'Company'),
413
+ companyHash,
414
+ roleParameter(roleMap, factTypes, 'Department', 'company')
415
+ ]);
416
+ });
417
+
418
+ it("should read time projection with fact projection", () => {
419
+ const { composer, factTypes, roleMap } = sqlFor(`
420
+ (company: Company) {
421
+ department: Department [
422
+ department->company: Company = company
423
+ ]
424
+ } => {
425
+ department = department
426
+ createdAt = @department
427
+ }
428
+ `);
429
+
430
+ const tree = composer.getSqlQueries();
431
+ const sql = tree.sqlQuery.sql;
432
+ expect(sql).toEqual(
433
+ `SELECT ` +
434
+ `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
435
+ `f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
436
+ `FROM public.fact f1 ` +
437
+ `JOIN public.edge e1 ` +
438
+ `ON e1.predecessor_fact_id = f1.fact_id ` +
439
+ `AND e1.role_id = $3 ` +
440
+ `JOIN public.fact f2 ` +
441
+ `ON f2.fact_id = e1.successor_fact_id ` +
442
+ `WHERE f1.fact_type_id = $1 AND f1.hash = $2 ` +
443
+ `ORDER BY f2.fact_id ASC`
444
+ );
445
+ expect(tree.sqlQuery.parameters).toEqual([
446
+ getFactTypeId(factTypes, 'Company'),
447
+ companyHash,
448
+ roleParameter(roleMap, factTypes, 'Department', 'company')
449
+ ]);
450
+ });
451
+
452
+ it("should read time projection with multiple timestamps", () => {
453
+ const { composer, factTypes, roleMap } = sqlFor(`
454
+ (company: Company) {
455
+ department: Department [
456
+ department->company: Company = company
457
+ ]
458
+ } => {
459
+ companyCreatedAt = @company
460
+ department = department
461
+ departmentCreatedAt = @department
462
+ }
463
+ `);
464
+
465
+ const tree = composer.getSqlQueries();
466
+ const sql = tree.sqlQuery.sql;
467
+ expect(sql).toEqual(
468
+ `SELECT ` +
469
+ `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
470
+ `f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2 ` +
471
+ `FROM public.fact f1 ` +
472
+ `JOIN public.edge e1 ` +
473
+ `ON e1.predecessor_fact_id = f1.fact_id ` +
474
+ `AND e1.role_id = $3 ` +
475
+ `JOIN public.fact f2 ` +
476
+ `ON f2.fact_id = e1.successor_fact_id ` +
477
+ `WHERE f1.fact_type_id = $1 AND f1.hash = $2 ` +
478
+ `ORDER BY f2.fact_id ASC`
479
+ );
480
+ expect(tree.sqlQuery.parameters).toEqual([
481
+ getFactTypeId(factTypes, 'Company'),
482
+ companyHash,
483
+ roleParameter(roleMap, factTypes, 'Department', 'company')
484
+ ]);
485
+ });
486
+
487
+ it("should read time projection with nested successor", () => {
488
+ const { composer, factTypes, roleMap } = sqlFor(`
489
+ (company: Company) {
490
+ department: Department [
491
+ department->company: Company = company
492
+ ]
493
+ project: Project [
494
+ project->department: Department = department
495
+ ]
496
+ } => {
497
+ projectName = project.name
498
+ projectCreatedAt = @project
499
+ }
500
+ `);
501
+
502
+ const tree = composer.getSqlQueries();
503
+ const sql = tree.sqlQuery.sql;
504
+ expect(sql).toEqual(
505
+ `SELECT ` +
506
+ `f1.hash as hash1, f1.fact_id as id1, f1.data as data1, f1.date_learned as timestamp1, ` +
507
+ `f2.hash as hash2, f2.fact_id as id2, f2.data as data2, f2.date_learned as timestamp2, ` +
508
+ `f3.hash as hash3, f3.fact_id as id3, f3.data as data3, f3.date_learned as timestamp3 ` +
509
+ `FROM public.fact f1 ` +
510
+ `JOIN public.edge e1 ` +
511
+ `ON e1.predecessor_fact_id = f1.fact_id ` +
512
+ `AND e1.role_id = $3 ` +
513
+ `JOIN public.fact f2 ` +
514
+ `ON f2.fact_id = e1.successor_fact_id ` +
515
+ `JOIN public.edge e2 ` +
516
+ `ON e2.predecessor_fact_id = f2.fact_id ` +
517
+ `AND e2.role_id = $4 ` +
518
+ `JOIN public.fact f3 ` +
519
+ `ON f3.fact_id = e2.successor_fact_id ` +
520
+ `WHERE f1.fact_type_id = $1 AND f1.hash = $2 ` +
521
+ `ORDER BY f2.fact_id ASC, f3.fact_id ASC`
522
+ );
523
+ expect(tree.sqlQuery.parameters).toEqual([
524
+ getFactTypeId(factTypes, 'Company'),
525
+ companyHash,
526
+ roleParameter(roleMap, factTypes, 'Department', 'company'),
527
+ roleParameter(roleMap, factTypes, 'Project', 'department')
528
+ ]);
529
+ });
352
530
  });
@@ -36,13 +36,13 @@ function sqlFor(descriptiveString: string, bookmarks: string[] = []) {
36
36
  },
37
37
  emptyRoleMap());
38
38
  const start = specification.given.map(input => {
39
- if (input.type === 'Root') {
39
+ if (input.label.type === 'Root') {
40
40
  return root;
41
41
  }
42
- if (input.type === 'Jinaga.User') {
42
+ if (input.label.type === 'Jinaga.User') {
43
43
  return user;
44
44
  }
45
- throw new Error(`Unknown input type ${input.type}`);
45
+ throw new Error(`Unknown input type ${input.label.type}`);
46
46
  });
47
47
  const sqlQueries = sqlFromSpecification(start, "public", bookmarks, 100, specification, factTypes, roleMap);
48
48
  return { sqlQueries, factTypes, roleMap };
@@ -0,0 +1,268 @@
1
+ import { dehydrateReference, getAllFactTypes, getAllRoles, SpecificationParser } from "jinaga";
2
+
3
+ import { addFactType, addRole, emptyFactTypeMap, emptyRoleMap, getFactTypeId, getRoleId } from "../../src/postgres/maps";
4
+ import { sqlFromSpecification } from "../../src/postgres/specification-sql";
5
+
6
+ const root = dehydrateReference({ type: 'Root' });
7
+ const rootHash = root.hash;
8
+
9
+ function parseSpecification(input: string) {
10
+ const parser = new SpecificationParser(input);
11
+ parser.skipWhitespace();
12
+ return parser.parseSpecification();
13
+ }
14
+
15
+ function sqlFor(descriptiveString: string, bookmarks: string[] = []) {
16
+ const specification = parseSpecification(descriptiveString);
17
+ const factTypeNames = getAllFactTypes(specification);
18
+
19
+ // Build a fact type map containing all fact types in the specification.
20
+ // Filter out fact types named "Unknown".
21
+ const factTypes = factTypeNames.filter(t => t !== 'Unknown').reduce(
22
+ (f, factType, i) => addFactType(f, factType, i + 1),
23
+ emptyFactTypeMap());
24
+
25
+ // Build a role map containing all roles in the specification.
26
+ // Filter out the roles named "unknown", and those of unknown fact types.
27
+ let roleMap = getAllRoles(specification).filter(r => r.name !== 'unknown').reduce(
28
+ (r, role, i) => {
29
+ const factTypeId = getFactTypeId(factTypes, role.successorType);
30
+ if (!factTypeId) {
31
+ return r;
32
+ }
33
+ return addRole(r, factTypeId, role.name, i + 1);
34
+ },
35
+ emptyRoleMap());
36
+ const start = specification.given.map(input => {
37
+ if (input.label.type === 'Root') {
38
+ return root;
39
+ }
40
+ throw new Error(`Unknown input type ${input.label.type}`);
41
+ });
42
+ const sqlQueries = sqlFromSpecification(start, "public", bookmarks, 100, specification, factTypes, roleMap);
43
+ return { sqlQueries, factTypes, roleMap };
44
+ }
45
+
46
+ function roleParameter(roleMap: Map<number, Map<string, number>>, factTypes: Map<string, number>, factTypeName: string, roleName: string): number {
47
+ const factTypeId = getFactTypeId(factTypes, factTypeName);
48
+ if (!factTypeId) {
49
+ throw new Error(`Unknown fact type ${factTypeName}`);
50
+ }
51
+ const roleId = getRoleId(roleMap, factTypeId, roleName);
52
+ if (!roleId) {
53
+ throw new Error(`Unknown role ${roleName} in fact type ${factTypeName}`);
54
+ }
55
+ return roleId;
56
+ }
57
+
58
+ describe("Specification conditions", () => {
59
+ describe("Processing given conditions", () => {
60
+ it("should generate SQL for single existential condition on given", () => {
61
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
62
+ (root: Root [
63
+ E {
64
+ tag: MyApplication.Tag [
65
+ tag->root: Root = root
66
+ ]
67
+ }
68
+ ]) {
69
+ project: MyApplication.Project [
70
+ project->root: Root = root
71
+ ]
72
+ }
73
+ `);
74
+
75
+ expect(sqlQueries.length).toBe(1);
76
+ const query = sqlQueries[0];
77
+
78
+ // The SQL should include an EXISTS clause for the condition on the given
79
+ expect(query.sql).toContain('EXISTS');
80
+ expect(query.sql).toEqual(
81
+ 'SELECT f2.hash as hash2, ' +
82
+ 'sort(array[f2.fact_id], \'desc\') as bookmark ' +
83
+ 'FROM public.fact f1 ' +
84
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
85
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
86
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
87
+ 'AND EXISTS (' +
88
+ 'SELECT 1 ' +
89
+ 'FROM public.edge e2 ' +
90
+ 'JOIN public.fact f3 ON f3.fact_id = e2.successor_fact_id ' +
91
+ 'WHERE e2.predecessor_fact_id = f1.fact_id AND e2.role_id = $4' +
92
+ ') ' +
93
+ 'AND sort(array[f2.fact_id], \'desc\') > $5 ' +
94
+ 'ORDER BY bookmark ASC ' +
95
+ 'LIMIT $6'
96
+ );
97
+ expect(query.parameters).toEqual([
98
+ getFactTypeId(factTypes, 'Root'),
99
+ rootHash,
100
+ roleParameter(roleMap, factTypes, 'MyApplication.Project', 'root'),
101
+ roleParameter(roleMap, factTypes, 'MyApplication.Tag', 'root'),
102
+ [],
103
+ 100
104
+ ]);
105
+ expect(query.labels).toEqual([
106
+ {
107
+ type: 'MyApplication.Project',
108
+ index: 2
109
+ }
110
+ ]);
111
+ });
112
+ it("should generate SQL for multiple conditions on same given", () => {
113
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
114
+ (root: Root [
115
+ E {
116
+ tag: MyApplication.Tag [
117
+ tag->root: Root = root
118
+ ]
119
+ }
120
+ E {
121
+ category: MyApplication.Category [
122
+ category->root: Root = root
123
+ ]
124
+ }
125
+ ]) {
126
+ project: MyApplication.Project [
127
+ project->root: Root = root
128
+ ]
129
+ }
130
+ `);
131
+
132
+ expect(sqlQueries.length).toBe(1);
133
+ const query = sqlQueries[0];
134
+
135
+ // The SQL should include TWO EXISTS clauses for the two conditions on the given
136
+ const existsCount = (query.sql.match(/EXISTS/g) || []).length;
137
+ expect(existsCount).toBe(2);
138
+
139
+ // Both EXISTS clauses should be connected with AND
140
+ expect(query.sql).toContain('AND EXISTS');
141
+
142
+ // Verify the complete SQL structure with both EXISTS clauses
143
+ expect(query.sql).toEqual(
144
+ 'SELECT f2.hash as hash2, ' +
145
+ 'sort(array[f2.fact_id], \'desc\') as bookmark ' +
146
+ 'FROM public.fact f1 ' +
147
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
148
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
149
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
150
+ 'AND EXISTS (' +
151
+ 'SELECT 1 ' +
152
+ 'FROM public.edge e2 ' +
153
+ 'JOIN public.fact f3 ON f3.fact_id = e2.successor_fact_id ' +
154
+ 'WHERE e2.predecessor_fact_id = f1.fact_id AND e2.role_id = $4' +
155
+ ') ' +
156
+ 'AND EXISTS (' +
157
+ 'SELECT 1 ' +
158
+ 'FROM public.edge e3 ' +
159
+ 'JOIN public.fact f4 ON f4.fact_id = e3.successor_fact_id ' +
160
+ 'WHERE e3.predecessor_fact_id = f1.fact_id AND e3.role_id = $5' +
161
+ ') ' +
162
+ 'AND sort(array[f2.fact_id], \'desc\') > $6 ' +
163
+ 'ORDER BY bookmark ASC ' +
164
+ 'LIMIT $7'
165
+ );
166
+ expect(query.parameters).toEqual([
167
+ getFactTypeId(factTypes, 'Root'),
168
+ rootHash,
169
+ roleParameter(roleMap, factTypes, 'MyApplication.Project', 'root'),
170
+ roleParameter(roleMap, factTypes, 'MyApplication.Tag', 'root'),
171
+ roleParameter(roleMap, factTypes, 'MyApplication.Category', 'root'),
172
+ [],
173
+ 100
174
+ ]);
175
+ expect(query.labels).toEqual([
176
+ {
177
+ type: 'MyApplication.Project',
178
+ index: 2
179
+ }
180
+ ]);
181
+ });
182
+
183
+ it("should generate SQL for conditions on multiple givens", () => {
184
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
185
+ (root1: Root [
186
+ E {
187
+ tag1: MyApplication.Tag [
188
+ tag1->root: Root = root1
189
+ ]
190
+ }
191
+ ], root2: Root [
192
+ E {
193
+ tag2: MyApplication.Tag [
194
+ tag2->root: Root = root2
195
+ ]
196
+ }
197
+ ]) {
198
+ project: MyApplication.Project [
199
+ project->root1: Root = root1
200
+ project->root2: Root = root2
201
+ ]
202
+ }
203
+ `);
204
+
205
+ expect(sqlQueries.length).toBe(1);
206
+ const query = sqlQueries[0];
207
+
208
+ // The SQL should include TWO EXISTS clauses, one for each given's condition
209
+ const existsCount = (query.sql.match(/EXISTS/g) || []).length;
210
+ expect(existsCount).toBe(2);
211
+
212
+ // Verify the SQL contains conditions for both givens
213
+ expect(query.sql).toContain('AND EXISTS');
214
+
215
+ // The SQL should reference both f1 (root1) and f3 (root2) in the EXISTS clauses
216
+ expect(query.sql).toMatch(/WHERE e\d+\.predecessor_fact_id = f1\.fact_id/);
217
+ expect(query.sql).toMatch(/WHERE e\d+\.predecessor_fact_id = f3\.fact_id/);
218
+
219
+ // Verify the complete SQL structure
220
+ expect(query.sql).toEqual(
221
+ 'SELECT f2.hash as hash2, ' +
222
+ 'sort(array[f2.fact_id], \'desc\') as bookmark ' +
223
+ 'FROM public.fact f1 ' +
224
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
225
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
226
+ 'JOIN public.edge e2 ON e2.successor_fact_id = f2.fact_id AND e2.role_id = $6 ' +
227
+ 'JOIN public.fact f3 ON f3.fact_id = e2.predecessor_fact_id ' +
228
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
229
+ 'AND f3.fact_type_id = $4 AND f3.hash = $5 ' +
230
+ 'AND EXISTS (' +
231
+ 'SELECT 1 ' +
232
+ 'FROM public.edge e3 ' +
233
+ 'JOIN public.fact f4 ON f4.fact_id = e3.successor_fact_id ' +
234
+ 'WHERE e3.predecessor_fact_id = f1.fact_id AND e3.role_id = $7' +
235
+ ') ' +
236
+ 'AND EXISTS (' +
237
+ 'SELECT 1 ' +
238
+ 'FROM public.edge e4 ' +
239
+ 'JOIN public.fact f5 ON f5.fact_id = e4.successor_fact_id ' +
240
+ 'WHERE e4.predecessor_fact_id = f3.fact_id AND e4.role_id = $8' +
241
+ ') ' +
242
+ 'AND sort(array[f2.fact_id], \'desc\') > $9 ' +
243
+ 'ORDER BY bookmark ASC ' +
244
+ 'LIMIT $10'
245
+ );
246
+
247
+ expect(query.parameters).toEqual([
248
+ getFactTypeId(factTypes, 'Root'),
249
+ rootHash,
250
+ roleParameter(roleMap, factTypes, 'MyApplication.Project', 'root1'),
251
+ getFactTypeId(factTypes, 'Root'),
252
+ rootHash,
253
+ roleParameter(roleMap, factTypes, 'MyApplication.Project', 'root2'),
254
+ roleParameter(roleMap, factTypes, 'MyApplication.Tag', 'root'),
255
+ roleParameter(roleMap, factTypes, 'MyApplication.Tag', 'root'),
256
+ [],
257
+ 100
258
+ ]);
259
+
260
+ expect(query.labels).toEqual([
261
+ {
262
+ type: 'MyApplication.Project',
263
+ index: 2
264
+ }
265
+ ]);
266
+ });
267
+ });
268
+ });