jinaga-server 3.1.5 → 3.3.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/.github/workflows/main.yml +1 -1
- package/.github/workflows/publish.yml +3 -3
- package/dist/http/router.js +37 -84
- package/dist/http/router.js.map +1 -1
- package/dist/jinaga-server.d.ts +2 -1
- package/dist/jinaga-server.js +18 -4
- package/dist/jinaga-server.js.map +1 -1
- package/dist/postgres/postgres-store.d.ts +2 -0
- package/dist/postgres/postgres-store.js +34 -0
- package/dist/postgres/postgres-store.js.map +1 -1
- package/dist/postgres/purge-sql.d.ts +7 -0
- package/dist/postgres/purge-sql.js +151 -0
- package/dist/postgres/purge-sql.js.map +1 -0
- package/dist/postgres/query-description.d.ts +1 -1
- package/dist/postgres/query-description.js +3 -3
- package/dist/postgres/query-description.js.map +1 -1
- package/dist/postgres/specification-result-sql.js +1 -1
- package/dist/postgres/specification-result-sql.js.map +1 -1
- package/dist/postgres/specification-sql.js +2 -2
- package/dist/postgres/specification-sql.js.map +1 -1
- package/integration-test/docker-compose.yaml +0 -1
- package/integration-test/jinaga-test/Dockerfile +2 -0
- package/integration-test/jinaga-test/orderModel.js +104 -0
- package/integration-test/jinaga-test/package-lock.json +147 -86
- package/integration-test/jinaga-test/package.json +3 -3
- package/integration-test/jinaga-test/purge.test.js +224 -0
- package/integration-test/postgres/1-setup.sql +8 -3
- package/integration-test/postgres/2-grant.sh +4 -4
- package/integration-test/postgres/Dockerfile +1 -1
- package/package.json +5 -4
- package/replicator/package-lock.json +139 -71
- package/replicator/package.json +2 -2
- package/scripts/migrate_purge.sql +45 -0
- package/scripts/purge.sql +33 -0
- package/setup.sh +4 -4
- package/setup.sql +59 -3
- package/src/http/router.ts +38 -84
- package/src/jinaga-server.ts +27 -5
- package/src/postgres/postgres-store.ts +37 -0
- package/src/postgres/purge-sql.ts +176 -0
- package/src/postgres/query-description.ts +1 -1
- package/src/postgres/specification-result-sql.ts +3 -3
- package/src/postgres/specification-sql.ts +4 -4
- package/test/authorization/authorizationSpec.ts +1 -1
- package/test/models/blog.ts +92 -0
- package/test/postgres/purgeDescendantsSqlSpec.ts +41 -0
- package/test/postgres/purgeSqlSpec.ts +246 -0
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "jinaga";
|
|
20
20
|
|
|
21
21
|
import { FactTypeMap, RoleMap } from "./maps";
|
|
22
|
-
import { EdgeDescription, ExistentialConditionDescription, FactByLabel, QueryDescription,
|
|
22
|
+
import { EdgeDescription, ExistentialConditionDescription, FactByLabel, QueryDescription, QueryDescriptionBuilder } from "./query-description";
|
|
23
23
|
|
|
24
24
|
function generateResultSqlQuery(queryDescription: QueryDescription, schema: string): SpecificationSqlQuery {
|
|
25
25
|
const allLabels = [ ...queryDescription.inputs, ...queryDescription.outputs ];
|
|
@@ -481,7 +481,7 @@ interface NamedResultComposer {
|
|
|
481
481
|
|
|
482
482
|
class ResultDescriptionBuilder {
|
|
483
483
|
constructor(
|
|
484
|
-
private queryDescriptionBuilder:
|
|
484
|
+
private queryDescriptionBuilder: QueryDescriptionBuilder
|
|
485
485
|
) { }
|
|
486
486
|
|
|
487
487
|
buildDescription(start: FactReference[], specification: Specification): ResultDescription {
|
|
@@ -547,7 +547,7 @@ function idsEqual(a: number[], b: number[]) {
|
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
export function resultSqlFromSpecification(start: FactReference[], specification: Specification, factTypes: FactTypeMap, roleMap: RoleMap, schema: string): ResultComposer | null {
|
|
550
|
-
const queryDescriptionBuilder = new
|
|
550
|
+
const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
|
|
551
551
|
const descriptionBuilder = new ResultDescriptionBuilder(queryDescriptionBuilder);
|
|
552
552
|
const description = descriptionBuilder.buildDescription(start, specification);
|
|
553
553
|
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
validateGiven
|
|
6
6
|
} from "jinaga";
|
|
7
7
|
|
|
8
|
-
import { EdgeDescription, ExistentialConditionDescription, QueryDescription,
|
|
8
|
+
import { EdgeDescription, ExistentialConditionDescription, QueryDescription, QueryDescriptionBuilder } from "./query-description";
|
|
9
9
|
|
|
10
10
|
interface SpecificationLabel {
|
|
11
11
|
type: string;
|
|
@@ -173,7 +173,7 @@ export function sqlFromSpecification(start: FactReference[], schema: string, boo
|
|
|
173
173
|
}, new Map<string, FactReference>());
|
|
174
174
|
|
|
175
175
|
const feeds = buildFeeds(specification);
|
|
176
|
-
const queryDescriptionBuilder = new
|
|
176
|
+
const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
|
|
177
177
|
const feedAndBookmark = feeds.map((feed, index) => ({
|
|
178
178
|
feed,
|
|
179
179
|
bookmark: bookmarks[index]
|
|
@@ -193,7 +193,7 @@ export function sqlFromSpecification(start: FactReference[], schema: string, boo
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
export function sqlFromFeed(feed: Specification, start: FactReference[], schema: string, bookmark: string, limit: number, factTypes: Map<string, number>, roleMap: Map<number, Map<string, number>>): SpecificationSqlQuery | null {
|
|
196
|
-
const queryDescriptionBuilder = new
|
|
196
|
+
const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
|
|
197
197
|
const queryDescription = buildQueryDescription(queryDescriptionBuilder, feed, start);
|
|
198
198
|
if (!queryDescription.isSatisfiable()) {
|
|
199
199
|
return null;
|
|
@@ -202,7 +202,7 @@ export function sqlFromFeed(feed: Specification, start: FactReference[], schema:
|
|
|
202
202
|
return sql;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
function buildQueryDescription(queryDescriptionBuilder:
|
|
205
|
+
function buildQueryDescription(queryDescriptionBuilder: QueryDescriptionBuilder, specification: Specification, start: FactReference[]): QueryDescription {
|
|
206
206
|
const { queryDescription } = queryDescriptionBuilder.addEdges(
|
|
207
207
|
QueryDescription.unsatisfiable,
|
|
208
208
|
specification.given,
|
|
@@ -164,7 +164,7 @@ function givenAuthorizationWithStorage(storage: MemoryStore) {
|
|
|
164
164
|
const fork = new PassThroughFork(storage);
|
|
165
165
|
const observableSource = new ObservableSource(storage);
|
|
166
166
|
const network = new NetworkNoOp();
|
|
167
|
-
const factManager = new FactManager(fork, observableSource, storage, network);
|
|
167
|
+
const factManager = new FactManager(fork, observableSource, storage, network, []);
|
|
168
168
|
const model = buildModel(b => b
|
|
169
169
|
.type(Hashtag)
|
|
170
170
|
.type(User)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { buildModel, PurgeConditions, User } from "jinaga";
|
|
2
|
+
|
|
3
|
+
export class Site {
|
|
4
|
+
static Type = 'Blog.Site' as const;
|
|
5
|
+
type = Site.Type;
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
public readonly creator: User,
|
|
9
|
+
public readonly domain: string
|
|
10
|
+
) { }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class SiteDeleted {
|
|
14
|
+
static Type = 'Blog.Site.Deleted' as const;
|
|
15
|
+
type = SiteDeleted.Type;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
public readonly site: Site
|
|
19
|
+
) { }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class SitePurged {
|
|
23
|
+
static Type = 'Blog.Site.Purged' as const;
|
|
24
|
+
type = SitePurged.Type;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
public readonly deleted: SiteDeleted
|
|
28
|
+
) { }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class Post {
|
|
32
|
+
static Type = 'Blog.Post' as const;
|
|
33
|
+
type = Post.Type;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
public readonly site: Site,
|
|
37
|
+
public readonly createdAt: Date | string
|
|
38
|
+
) { }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class PostDeleted {
|
|
42
|
+
static Type = 'Blog.Post.Deleted' as const;
|
|
43
|
+
type = PostDeleted.Type;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
public readonly post: Post
|
|
47
|
+
) { }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class Title {
|
|
51
|
+
static Type = 'Blog.Post.Title' as const;
|
|
52
|
+
type = Title.Type;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
public readonly post: Post,
|
|
56
|
+
public readonly value: string,
|
|
57
|
+
public readonly prior: Title[]
|
|
58
|
+
) { }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const model = buildModel(b => b
|
|
62
|
+
.type(Site, f => f
|
|
63
|
+
.predecessor('creator', User)
|
|
64
|
+
)
|
|
65
|
+
.type(Post, f => f
|
|
66
|
+
.predecessor('site', Site)
|
|
67
|
+
)
|
|
68
|
+
.type(SiteDeleted, f => f
|
|
69
|
+
.predecessor('site', Site)
|
|
70
|
+
)
|
|
71
|
+
.type(SitePurged, f => f
|
|
72
|
+
.predecessor('deleted', SiteDeleted)
|
|
73
|
+
)
|
|
74
|
+
.type(Title, f => f
|
|
75
|
+
.predecessor('post', Post)
|
|
76
|
+
.predecessor('prior', Title)
|
|
77
|
+
)
|
|
78
|
+
.type(PostDeleted, f => f
|
|
79
|
+
.predecessor('post', Post)
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
export const purgeConditions = (p: PurgeConditions) => p
|
|
84
|
+
.whenExists(model.given(Site).match((site, facts) =>
|
|
85
|
+
facts.ofType(SiteDeleted)
|
|
86
|
+
.join(deleted => deleted.site, site)
|
|
87
|
+
))
|
|
88
|
+
.whenExists(model.given(Post).match((post, facts) =>
|
|
89
|
+
facts.ofType(PostDeleted)
|
|
90
|
+
.join(deleted => deleted.post, post)
|
|
91
|
+
))
|
|
92
|
+
;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { purgeDescendantsSql } from "../../src/postgres/purge-sql";
|
|
2
|
+
|
|
3
|
+
describe("Purge Descendants SQL", () => {
|
|
4
|
+
it("should handle single trigger", () => {
|
|
5
|
+
const sql = purgeDescendantsSql(1, 'public');
|
|
6
|
+
|
|
7
|
+
const expected =
|
|
8
|
+
`WITH purge_root AS (\n` +
|
|
9
|
+
` SELECT pr.fact_id\n` +
|
|
10
|
+
` FROM public.fact pr\n` +
|
|
11
|
+
` WHERE pr.fact_type_id = $1\n` +
|
|
12
|
+
` AND pr.hash = $2\n` +
|
|
13
|
+
`), triggers AS (\n` +
|
|
14
|
+
` SELECT t.fact_id\n` +
|
|
15
|
+
` FROM public.fact t\n` +
|
|
16
|
+
` WHERE (t.fact_type_id = $3 AND t.hash = $4)\n` +
|
|
17
|
+
`), triggers_and_ancestors AS (\n` +
|
|
18
|
+
` SELECT t.fact_id\n` +
|
|
19
|
+
` FROM triggers t\n` +
|
|
20
|
+
` UNION\n` +
|
|
21
|
+
` SELECT a.ancestor_fact_id\n` +
|
|
22
|
+
` FROM public.ancestor a\n` +
|
|
23
|
+
` JOIN triggers t\n` +
|
|
24
|
+
` ON a.fact_id = t.fact_id\n` +
|
|
25
|
+
`), targets AS (\n` +
|
|
26
|
+
` SELECT a.fact_id\n` +
|
|
27
|
+
` FROM public.ancestor a\n` +
|
|
28
|
+
` JOIN purge_root pr\n` +
|
|
29
|
+
` ON a.ancestor_fact_id = pr.fact_id\n` +
|
|
30
|
+
` WHERE a.fact_id NOT IN (SELECT * FROM triggers_and_ancestors)\n` +
|
|
31
|
+
`), facts AS (\n` +
|
|
32
|
+
` DELETE\n` +
|
|
33
|
+
` FROM public.fact f\n` +
|
|
34
|
+
` USING targets t WHERE t.fact_id = f.fact_id\n` +
|
|
35
|
+
` RETURNING f.fact_id\n` +
|
|
36
|
+
`)\n` +
|
|
37
|
+
`SELECT fact_id FROM facts\n`;
|
|
38
|
+
|
|
39
|
+
expect(sql).toBe(expected);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Specification } from "jinaga";
|
|
2
|
+
import { ensureGetFactTypeId, FactTypeMap, getRoleId } from "../../src/postgres/maps";
|
|
3
|
+
import { purgeSqlFromSpecification } from "../../src/postgres/purge-sql";
|
|
4
|
+
import { model, Site, SiteDeleted, SitePurged } from "../models/blog";
|
|
5
|
+
|
|
6
|
+
describe("Purge SQL", () => {
|
|
7
|
+
it("should handle direct successor", () => {
|
|
8
|
+
const specification = model.given(Site).match((site, facts) =>
|
|
9
|
+
facts.ofType(SiteDeleted)
|
|
10
|
+
.join(deleted => deleted.site, site)
|
|
11
|
+
).specification;
|
|
12
|
+
const factTypes = buildFactTypeMap(
|
|
13
|
+
Site.Type,
|
|
14
|
+
SiteDeleted.Type
|
|
15
|
+
);
|
|
16
|
+
const roleMap = buildRoleMap(factTypes,
|
|
17
|
+
[Site.Type, ['creator']],
|
|
18
|
+
[SiteDeleted.Type, ['site']]
|
|
19
|
+
);
|
|
20
|
+
const schema = 'public';
|
|
21
|
+
const { sql, parameters } = whenGeneratePurgeSql(specification, factTypes, roleMap, schema);
|
|
22
|
+
|
|
23
|
+
const expected =
|
|
24
|
+
`WITH candidates AS (
|
|
25
|
+
SELECT
|
|
26
|
+
f1.fact_id as purge_root,
|
|
27
|
+
f2.fact_id as trigger1
|
|
28
|
+
FROM public.fact f1
|
|
29
|
+
JOIN public.edge e1
|
|
30
|
+
ON e1.predecessor_fact_id = f1.fact_id
|
|
31
|
+
AND e1.role_id = $2
|
|
32
|
+
JOIN public.fact f2
|
|
33
|
+
ON f2.fact_id = e1.successor_fact_id
|
|
34
|
+
WHERE f1.fact_type_id = $1
|
|
35
|
+
), targets AS (
|
|
36
|
+
SELECT a.fact_id
|
|
37
|
+
FROM public.ancestor a
|
|
38
|
+
JOIN candidates c ON c.purge_root = a.ancestor_fact_id
|
|
39
|
+
WHERE NOT EXISTS (
|
|
40
|
+
SELECT 1
|
|
41
|
+
FROM candidates c2
|
|
42
|
+
WHERE a.fact_id = c2.trigger1
|
|
43
|
+
)
|
|
44
|
+
AND NOT EXISTS (
|
|
45
|
+
SELECT 1
|
|
46
|
+
FROM candidates c2
|
|
47
|
+
JOIN public.ancestor a2
|
|
48
|
+
ON a2.fact_id = c2.trigger1
|
|
49
|
+
WHERE a.fact_id = a2.ancestor_fact_id
|
|
50
|
+
)
|
|
51
|
+
), facts AS (
|
|
52
|
+
DELETE
|
|
53
|
+
FROM public.fact f
|
|
54
|
+
USING targets t WHERE t.fact_id = f.fact_id
|
|
55
|
+
RETURNING f.fact_id
|
|
56
|
+
)
|
|
57
|
+
SELECT fact_id FROM facts
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
expect(sql).toBe(expected);
|
|
61
|
+
expect(parameters).toEqual([
|
|
62
|
+
ensureGetFactTypeId(factTypes, Site.Type),
|
|
63
|
+
getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site')
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should handle two levels of successors", () => {
|
|
68
|
+
const specification = model.given(Site).match((site, facts) =>
|
|
69
|
+
facts.ofType(SitePurged)
|
|
70
|
+
.join(purged => purged.deleted.site, site)
|
|
71
|
+
).specification;
|
|
72
|
+
const factTypes = buildFactTypeMap(
|
|
73
|
+
Site.Type,
|
|
74
|
+
SiteDeleted.Type,
|
|
75
|
+
SitePurged.Type
|
|
76
|
+
);
|
|
77
|
+
const roleMap = buildRoleMap(factTypes,
|
|
78
|
+
[Site.Type, ['creator']],
|
|
79
|
+
[SiteDeleted.Type, ['site']],
|
|
80
|
+
[SitePurged.Type, ['deleted']]
|
|
81
|
+
);
|
|
82
|
+
const schema = 'public';
|
|
83
|
+
const { sql, parameters } = whenGeneratePurgeSql(specification, factTypes, roleMap, schema);
|
|
84
|
+
|
|
85
|
+
const expected =
|
|
86
|
+
`WITH candidates AS (
|
|
87
|
+
SELECT
|
|
88
|
+
f1.fact_id as purge_root,
|
|
89
|
+
f3.fact_id as trigger1
|
|
90
|
+
FROM public.fact f1
|
|
91
|
+
JOIN public.edge e1
|
|
92
|
+
ON e1.predecessor_fact_id = f1.fact_id
|
|
93
|
+
AND e1.role_id = $2
|
|
94
|
+
JOIN public.fact f2
|
|
95
|
+
ON f2.fact_id = e1.successor_fact_id
|
|
96
|
+
JOIN public.edge e2
|
|
97
|
+
ON e2.predecessor_fact_id = f2.fact_id
|
|
98
|
+
AND e2.role_id = $3
|
|
99
|
+
JOIN public.fact f3
|
|
100
|
+
ON f3.fact_id = e2.successor_fact_id
|
|
101
|
+
WHERE f1.fact_type_id = $1
|
|
102
|
+
), targets AS (
|
|
103
|
+
SELECT a.fact_id
|
|
104
|
+
FROM public.ancestor a
|
|
105
|
+
JOIN candidates c ON c.purge_root = a.ancestor_fact_id
|
|
106
|
+
WHERE NOT EXISTS (
|
|
107
|
+
SELECT 1
|
|
108
|
+
FROM candidates c2
|
|
109
|
+
WHERE a.fact_id = c2.trigger1
|
|
110
|
+
)
|
|
111
|
+
AND NOT EXISTS (
|
|
112
|
+
SELECT 1
|
|
113
|
+
FROM candidates c2
|
|
114
|
+
JOIN public.ancestor a2
|
|
115
|
+
ON a2.fact_id = c2.trigger1
|
|
116
|
+
WHERE a.fact_id = a2.ancestor_fact_id
|
|
117
|
+
)
|
|
118
|
+
), facts AS (
|
|
119
|
+
DELETE
|
|
120
|
+
FROM public.fact f
|
|
121
|
+
USING targets t WHERE t.fact_id = f.fact_id
|
|
122
|
+
RETURNING f.fact_id
|
|
123
|
+
)
|
|
124
|
+
SELECT fact_id FROM facts
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
expect(sql).toBe(expected);
|
|
128
|
+
expect(parameters).toEqual([
|
|
129
|
+
ensureGetFactTypeId(factTypes, Site.Type),
|
|
130
|
+
getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site'),
|
|
131
|
+
getRoleId(roleMap, ensureGetFactTypeId(factTypes, SitePurged.Type), 'deleted')
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should handle two labeled successors", () => {
|
|
136
|
+
const specification = model.given(Site).match((site, facts) =>
|
|
137
|
+
facts.ofType(SiteDeleted)
|
|
138
|
+
.join(deleted => deleted.site, site)
|
|
139
|
+
.selectMany(deleted =>
|
|
140
|
+
facts.ofType(SitePurged)
|
|
141
|
+
.join(purged => purged.deleted, deleted)
|
|
142
|
+
)).specification;
|
|
143
|
+
const factTypes = buildFactTypeMap(
|
|
144
|
+
Site.Type,
|
|
145
|
+
SiteDeleted.Type,
|
|
146
|
+
SitePurged.Type
|
|
147
|
+
);
|
|
148
|
+
const roleMap = buildRoleMap(factTypes,
|
|
149
|
+
[Site.Type, ['creator']],
|
|
150
|
+
[SiteDeleted.Type, ['site']],
|
|
151
|
+
[SitePurged.Type, ['deleted']]
|
|
152
|
+
);
|
|
153
|
+
const schema = 'public';
|
|
154
|
+
const { sql, parameters } = whenGeneratePurgeSql(specification, factTypes, roleMap, schema);
|
|
155
|
+
|
|
156
|
+
const expected =
|
|
157
|
+
`WITH candidates AS (
|
|
158
|
+
SELECT
|
|
159
|
+
f1.fact_id as purge_root,
|
|
160
|
+
f2.fact_id as trigger1,
|
|
161
|
+
f3.fact_id as trigger2
|
|
162
|
+
FROM public.fact f1
|
|
163
|
+
JOIN public.edge e1
|
|
164
|
+
ON e1.predecessor_fact_id = f1.fact_id
|
|
165
|
+
AND e1.role_id = $2
|
|
166
|
+
JOIN public.fact f2
|
|
167
|
+
ON f2.fact_id = e1.successor_fact_id
|
|
168
|
+
JOIN public.edge e2
|
|
169
|
+
ON e2.predecessor_fact_id = f2.fact_id
|
|
170
|
+
AND e2.role_id = $3
|
|
171
|
+
JOIN public.fact f3
|
|
172
|
+
ON f3.fact_id = e2.successor_fact_id
|
|
173
|
+
WHERE f1.fact_type_id = $1
|
|
174
|
+
), targets AS (
|
|
175
|
+
SELECT a.fact_id
|
|
176
|
+
FROM public.ancestor a
|
|
177
|
+
JOIN candidates c ON c.purge_root = a.ancestor_fact_id
|
|
178
|
+
WHERE NOT EXISTS (
|
|
179
|
+
SELECT 1
|
|
180
|
+
FROM candidates c2
|
|
181
|
+
WHERE a.fact_id = c2.trigger1
|
|
182
|
+
OR a.fact_id = c2.trigger2
|
|
183
|
+
)
|
|
184
|
+
AND NOT EXISTS (
|
|
185
|
+
SELECT 1
|
|
186
|
+
FROM candidates c2
|
|
187
|
+
JOIN public.ancestor a2
|
|
188
|
+
ON a2.fact_id = c2.trigger1
|
|
189
|
+
WHERE a.fact_id = a2.ancestor_fact_id
|
|
190
|
+
)
|
|
191
|
+
AND NOT EXISTS (
|
|
192
|
+
SELECT 1
|
|
193
|
+
FROM candidates c2
|
|
194
|
+
JOIN public.ancestor a2
|
|
195
|
+
ON a2.fact_id = c2.trigger2
|
|
196
|
+
WHERE a.fact_id = a2.ancestor_fact_id
|
|
197
|
+
)
|
|
198
|
+
), facts AS (
|
|
199
|
+
DELETE
|
|
200
|
+
FROM public.fact f
|
|
201
|
+
USING targets t WHERE t.fact_id = f.fact_id
|
|
202
|
+
RETURNING f.fact_id
|
|
203
|
+
)
|
|
204
|
+
SELECT fact_id FROM facts
|
|
205
|
+
`;
|
|
206
|
+
|
|
207
|
+
expect(sql).toBe(expected);
|
|
208
|
+
expect(parameters).toEqual([
|
|
209
|
+
ensureGetFactTypeId(factTypes, Site.Type),
|
|
210
|
+
getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site'),
|
|
211
|
+
getRoleId(roleMap, ensureGetFactTypeId(factTypes, SitePurged.Type), 'deleted')
|
|
212
|
+
]);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
function whenGeneratePurgeSql(specification: Specification, factTypes: FactTypeMap, roleMap: Map<number, Map<string, number>>, schema: string) {
|
|
217
|
+
const purgeCommand = purgeSqlFromSpecification(specification, factTypes, roleMap, schema);
|
|
218
|
+
if (purgeCommand === null) {
|
|
219
|
+
throw new Error("The specification was not satisfiable.");
|
|
220
|
+
}
|
|
221
|
+
return purgeCommand;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildFactTypeMap(...types: string[]): FactTypeMap {
|
|
225
|
+
const factTypeMap: FactTypeMap = new Map();
|
|
226
|
+
types.forEach((type, index) => {
|
|
227
|
+
factTypeMap.set(type, index + 1);
|
|
228
|
+
});
|
|
229
|
+
return factTypeMap;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildRoleMap(factTypes: FactTypeMap, ...args: [string, string[]][]): Map<number, Map<string, number>> {
|
|
233
|
+
const roleMap = new Map<number, Map<string, number>>();
|
|
234
|
+
args.forEach(([type, roles]) => {
|
|
235
|
+
const typeId = factTypes.get(type);
|
|
236
|
+
if (typeId === undefined) {
|
|
237
|
+
throw new Error(`Unknown fact type: ${type}`);
|
|
238
|
+
}
|
|
239
|
+
const roleIdMap = new Map<string, number>();
|
|
240
|
+
roles.forEach((role, roleIndex) => {
|
|
241
|
+
roleIdMap.set(role, typeId * 10 + roleIndex + 1);
|
|
242
|
+
});
|
|
243
|
+
roleMap.set(typeId, roleIdMap);
|
|
244
|
+
});
|
|
245
|
+
return roleMap;
|
|
246
|
+
}
|