jinaga-server 3.7.5 → 3.8.1
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/.claude/skills/degrees-of-freedom/.openskills.json +7 -0
- package/.claude/skills/degrees-of-freedom/SKILL.md +18 -0
- package/.claude/skills/degrees-of-freedom/degrees-of-freedom-constitution.md +142 -0
- package/.claude/skills/night-shift-setup/.openskills.json +7 -0
- package/.claude/skills/night-shift-setup/SKILL.md +95 -0
- package/.claude/skills/night-shift-setup/configuring-a-repository.md +111 -0
- package/.claude/skills/night-shift-setup/creating-the-log.md +59 -0
- package/.claude/skills/night-shift-setup/creating-the-routines.md +80 -0
- package/.claude/skills/night-shift-setup/log-model.factual +971 -0
- package/.claude/skills/night-shift-worker/.openskills.json +7 -0
- package/.claude/skills/night-shift-worker/SKILL.md +199 -0
- package/.claude/skills/night-shift-worker/night-shift-log.md +72 -0
- package/.claude/skills/night-shift-worker/scripts/register-stack.sh +173 -0
- package/.claude/skills/night-shift-worker/stacking.md +57 -0
- package/.claude/skills/night-shift-worker/verifying-a-merged-fix.md +16 -0
- package/.claude/skills/refining-issues/.openskills.json +7 -0
- package/.claude/skills/refining-issues/SKILL.md +327 -0
- package/.claude/skills/refining-issues/references/worked-example.md +234 -0
- package/.github/workflows/main.yml +2 -2
- package/.night-shift/config.md +84 -0
- package/.night-shift/skills.md +17 -0
- package/README.md +21 -0
- package/dist/http/router.js +19 -23
- package/dist/http/router.js.map +1 -1
- package/dist/http/stream.d.ts +0 -1
- package/dist/http/stream.js +6 -17
- package/dist/http/stream.js.map +1 -1
- package/dist/jinaga-server.js +74 -7
- package/dist/jinaga-server.js.map +1 -1
- package/dist/postgres/postgres-store.js +18 -4
- package/dist/postgres/postgres-store.js.map +1 -1
- package/dist/postgres/purge-sql.d.ts +10 -0
- package/dist/postgres/purge-sql.js +77 -50
- package/dist/postgres/purge-sql.js.map +1 -1
- package/integration-test/jinaga-test/package-lock.json +4 -4
- package/integration-test/jinaga-test/package.json +1 -1
- package/integration-test/jinaga-test/postgres-keystore-race.test.js +9 -7
- package/integration-test/jinaga-test/purge.test.js +52 -1
- package/package.json +2 -2
- package/src/http/router.ts +31 -39
- package/src/http/stream.ts +21 -36
- package/src/jinaga-server.ts +92 -4
- package/src/postgres/postgres-store.ts +19 -5
- package/src/postgres/purge-sql.ts +89 -55
- package/test/http/streamLogVolumeSpec.ts +284 -0
- package/test/jinaga-server/poolConfigurationSpec.ts +147 -0
- package/test/jinaga-server/tracePoolSpec.ts +368 -0
- package/test/postgres/purgeSqlSpec.ts +123 -22
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { Trace, Tracer } from "jinaga";
|
|
3
|
+
import { Pool } from "pg";
|
|
4
|
+
|
|
5
|
+
import { tracePool } from "../../src/jinaga-server";
|
|
6
|
+
|
|
7
|
+
const windowMilliseconds = 10000;
|
|
8
|
+
|
|
9
|
+
// Jest's own fake timers cannot be installed on this version of Node, because
|
|
10
|
+
// they try to replace the read-only global `performance`. These stand in for
|
|
11
|
+
// the interval timer alone, which is all tracePool schedules, and they record
|
|
12
|
+
// whether it was unreferenced and whether it was cleared.
|
|
13
|
+
class FakeInterval {
|
|
14
|
+
public dueAt: number;
|
|
15
|
+
public cleared = false;
|
|
16
|
+
private referenced = true;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly callback: () => void,
|
|
20
|
+
public readonly delay: number,
|
|
21
|
+
now: number
|
|
22
|
+
) {
|
|
23
|
+
this.dueAt = now + delay;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ref(): this {
|
|
27
|
+
this.referenced = true;
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
unref(): this {
|
|
32
|
+
this.referenced = false;
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
hasRef(): boolean {
|
|
37
|
+
return this.referenced;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fire() {
|
|
41
|
+
this.callback();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class FakeClock {
|
|
46
|
+
private now = 0;
|
|
47
|
+
private readonly created: FakeInterval[] = [];
|
|
48
|
+
|
|
49
|
+
install() {
|
|
50
|
+
jest.spyOn(global, 'setInterval').mockImplementation(((callback: () => void, delay?: number) => {
|
|
51
|
+
const interval = new FakeInterval(callback, delay ?? 0, this.now);
|
|
52
|
+
this.created.push(interval);
|
|
53
|
+
return interval;
|
|
54
|
+
}) as any);
|
|
55
|
+
jest.spyOn(global, 'clearInterval').mockImplementation(((handle: FakeInterval) => {
|
|
56
|
+
handle.cleared = true;
|
|
57
|
+
}) as any);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
advance(milliseconds: number) {
|
|
61
|
+
const end = this.now + milliseconds;
|
|
62
|
+
for (;;) {
|
|
63
|
+
const due = this.live()
|
|
64
|
+
.filter(interval => interval.dueAt <= end)
|
|
65
|
+
.sort((a, b) => a.dueAt - b.dueAt)[0];
|
|
66
|
+
if (!due) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
this.now = due.dueAt;
|
|
70
|
+
due.dueAt = this.now + due.delay;
|
|
71
|
+
due.fire();
|
|
72
|
+
}
|
|
73
|
+
this.now = end;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
live(): FakeInterval[] {
|
|
77
|
+
return this.created.filter(interval => !interval.cleared);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
only(): FakeInterval {
|
|
81
|
+
expect(this.created).toHaveLength(1);
|
|
82
|
+
return this.created[0];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface Counts {
|
|
87
|
+
total: number;
|
|
88
|
+
idle: number;
|
|
89
|
+
waiting: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface MetricLine {
|
|
93
|
+
message: string;
|
|
94
|
+
measurements: { [key: string]: number };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class RecordingTracer implements Tracer {
|
|
98
|
+
public readonly info_: string[] = [];
|
|
99
|
+
public readonly warnings: string[] = [];
|
|
100
|
+
public readonly errors: any[] = [];
|
|
101
|
+
public readonly metrics: MetricLine[] = [];
|
|
102
|
+
|
|
103
|
+
info(message: string): void {
|
|
104
|
+
this.info_.push(message);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
warn(message: string): void {
|
|
108
|
+
this.warnings.push(message);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
error(error: any): void {
|
|
112
|
+
this.errors.push(error);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
dependency<T>(name: string, data: string, operation: () => Promise<T>): Promise<T> {
|
|
116
|
+
return operation();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
metric(message: string, measurements: { [key: string]: number }): void {
|
|
120
|
+
this.metrics.push({ message, measurements });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
counter(name: string, value: number): void {
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
linesNamed(message: string): MetricLine[] {
|
|
127
|
+
return this.metrics.filter(m => m.message === message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Stands in for a pg Pool. It reports the counts a test chooses and emits the
|
|
132
|
+
// events a real pool emits, so tracePool can be driven through a storm without
|
|
133
|
+
// a database.
|
|
134
|
+
class PoolDouble extends EventEmitter {
|
|
135
|
+
public totalCount = 0;
|
|
136
|
+
public idleCount = 0;
|
|
137
|
+
public waitingCount = 0;
|
|
138
|
+
public ended = false;
|
|
139
|
+
|
|
140
|
+
asPool(): Pool {
|
|
141
|
+
return this as unknown as Pool;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
acquire(counts: Counts) {
|
|
145
|
+
this.report(counts);
|
|
146
|
+
this.emit('acquire', {});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
release(counts: Counts) {
|
|
150
|
+
this.report(counts);
|
|
151
|
+
this.emit('release', undefined, {});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
connect(counts: Counts) {
|
|
155
|
+
this.report(counts);
|
|
156
|
+
this.emit('connect', {});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
remove(counts: Counts) {
|
|
160
|
+
this.report(counts);
|
|
161
|
+
this.emit('remove', {});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fail(error: Error) {
|
|
165
|
+
this.emit('error', error, {});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private report(counts: Counts) {
|
|
169
|
+
this.totalCount = counts.total;
|
|
170
|
+
this.idleCount = counts.idle;
|
|
171
|
+
this.waitingCount = counts.waiting;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
describe('tracePool', () => {
|
|
176
|
+
let tracer: RecordingTracer;
|
|
177
|
+
let pool: PoolDouble;
|
|
178
|
+
let clock: FakeClock;
|
|
179
|
+
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
clock = new FakeClock();
|
|
182
|
+
clock.install();
|
|
183
|
+
tracer = new RecordingTracer();
|
|
184
|
+
Trace.configure(tracer);
|
|
185
|
+
pool = new PoolDouble();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
afterEach(() => {
|
|
189
|
+
jest.restoreAllMocks();
|
|
190
|
+
Trace.off();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should write one line for a window of a thousand acquires', () => {
|
|
194
|
+
tracePool(pool.asPool());
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < 1000; i++) {
|
|
197
|
+
pool.acquire({ total: 10, idle: 0, waiting: 0 });
|
|
198
|
+
pool.release({ total: 10, idle: 1, waiting: 0 });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
expect(tracer.metrics).toHaveLength(0);
|
|
202
|
+
|
|
203
|
+
clock.advance(windowMilliseconds);
|
|
204
|
+
|
|
205
|
+
expect(tracer.linesNamed('Postgres pool')).toHaveLength(1);
|
|
206
|
+
expect(tracer.metrics).toHaveLength(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should write no line per acquire', () => {
|
|
210
|
+
tracePool(pool.asPool());
|
|
211
|
+
|
|
212
|
+
pool.acquire({ total: 10, idle: 0, waiting: 0 });
|
|
213
|
+
pool.acquire({ total: 10, idle: 0, waiting: 1 });
|
|
214
|
+
|
|
215
|
+
expect(tracer.metrics).toHaveLength(0);
|
|
216
|
+
expect(tracer.linesNamed('Postgres acquired')).toHaveLength(0);
|
|
217
|
+
|
|
218
|
+
clock.advance(windowMilliseconds);
|
|
219
|
+
|
|
220
|
+
expect(tracer.linesNamed('Postgres acquired')).toHaveLength(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should carry the peak waiting count of a spike that came and went', () => {
|
|
224
|
+
tracePool(pool.asPool());
|
|
225
|
+
|
|
226
|
+
pool.acquire({ total: 10, idle: 0, waiting: 0 });
|
|
227
|
+
pool.acquire({ total: 10, idle: 0, waiting: 349 });
|
|
228
|
+
pool.release({ total: 10, idle: 0, waiting: 120 });
|
|
229
|
+
pool.release({ total: 10, idle: 3, waiting: 0 });
|
|
230
|
+
|
|
231
|
+
clock.advance(windowMilliseconds);
|
|
232
|
+
|
|
233
|
+
const lines = tracer.linesNamed('Postgres pool');
|
|
234
|
+
expect(lines).toHaveLength(1);
|
|
235
|
+
expect(lines[0].measurements).toEqual({
|
|
236
|
+
total: 10,
|
|
237
|
+
idle: 3,
|
|
238
|
+
waiting: 0,
|
|
239
|
+
peakWaiting: 349
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should warn about a window whose peak waiting count was above zero', () => {
|
|
244
|
+
tracePool(pool.asPool());
|
|
245
|
+
|
|
246
|
+
pool.acquire({ total: 10, idle: 0, waiting: 349 });
|
|
247
|
+
pool.release({ total: 10, idle: 3, waiting: 0 });
|
|
248
|
+
|
|
249
|
+
clock.advance(windowMilliseconds);
|
|
250
|
+
|
|
251
|
+
expect(tracer.warnings).toHaveLength(1);
|
|
252
|
+
expect(tracer.warnings[0]).toContain('349');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should not warn about a window that never had a request waiting', () => {
|
|
256
|
+
tracePool(pool.asPool());
|
|
257
|
+
|
|
258
|
+
pool.acquire({ total: 10, idle: 2, waiting: 0 });
|
|
259
|
+
|
|
260
|
+
clock.advance(windowMilliseconds);
|
|
261
|
+
|
|
262
|
+
expect(tracer.linesNamed('Postgres pool')).toHaveLength(1);
|
|
263
|
+
expect(tracer.warnings).toHaveLength(0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should write nothing for a window with no acquires', () => {
|
|
267
|
+
tracePool(pool.asPool());
|
|
268
|
+
|
|
269
|
+
clock.advance(windowMilliseconds * 3);
|
|
270
|
+
|
|
271
|
+
expect(tracer.metrics).toHaveLength(0);
|
|
272
|
+
expect(tracer.warnings).toHaveLength(0);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should write nothing for a window in which only a release happened', () => {
|
|
276
|
+
tracePool(pool.asPool());
|
|
277
|
+
|
|
278
|
+
pool.release({ total: 10, idle: 10, waiting: 0 });
|
|
279
|
+
|
|
280
|
+
clock.advance(windowMilliseconds);
|
|
281
|
+
|
|
282
|
+
expect(tracer.metrics).toHaveLength(0);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should report a storm that stopped in the middle of a window', () => {
|
|
286
|
+
tracePool(pool.asPool());
|
|
287
|
+
|
|
288
|
+
for (let i = 0; i < 200; i++) {
|
|
289
|
+
pool.acquire({ total: 10, idle: 0, waiting: 349 });
|
|
290
|
+
}
|
|
291
|
+
pool.release({ total: 10, idle: 0, waiting: 0 });
|
|
292
|
+
|
|
293
|
+
// The storm is over, and nothing touches the pool again. Only the
|
|
294
|
+
// window's own close can report it.
|
|
295
|
+
clock.advance(windowMilliseconds);
|
|
296
|
+
|
|
297
|
+
const lines = tracer.linesNamed('Postgres pool');
|
|
298
|
+
expect(lines).toHaveLength(1);
|
|
299
|
+
expect(lines[0].measurements.peakWaiting).toBe(349);
|
|
300
|
+
|
|
301
|
+
clock.advance(windowMilliseconds * 2);
|
|
302
|
+
|
|
303
|
+
expect(tracer.linesNamed('Postgres pool')).toHaveLength(1);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should start each window with a fresh peak', () => {
|
|
307
|
+
tracePool(pool.asPool());
|
|
308
|
+
|
|
309
|
+
pool.acquire({ total: 10, idle: 0, waiting: 349 });
|
|
310
|
+
clock.advance(windowMilliseconds);
|
|
311
|
+
|
|
312
|
+
pool.acquire({ total: 10, idle: 4, waiting: 0 });
|
|
313
|
+
clock.advance(windowMilliseconds);
|
|
314
|
+
|
|
315
|
+
const lines = tracer.linesNamed('Postgres pool');
|
|
316
|
+
expect(lines).toHaveLength(2);
|
|
317
|
+
expect(lines[0].measurements.peakWaiting).toBe(349);
|
|
318
|
+
expect(lines[1].measurements.peakWaiting).toBe(0);
|
|
319
|
+
expect(tracer.warnings).toHaveLength(1);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should not hold the process open with the timer it schedules', () => {
|
|
323
|
+
tracePool(pool.asPool());
|
|
324
|
+
|
|
325
|
+
expect(clock.only().hasRef()).toBe(false);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should stop reporting once the pool has ended', () => {
|
|
329
|
+
tracePool(pool.asPool());
|
|
330
|
+
|
|
331
|
+
pool.acquire({ total: 10, idle: 0, waiting: 0 });
|
|
332
|
+
clock.advance(windowMilliseconds);
|
|
333
|
+
expect(tracer.linesNamed('Postgres pool')).toHaveLength(1);
|
|
334
|
+
|
|
335
|
+
pool.acquire({ total: 10, idle: 0, waiting: 0 });
|
|
336
|
+
pool.ended = true;
|
|
337
|
+
|
|
338
|
+
clock.advance(windowMilliseconds * 5);
|
|
339
|
+
|
|
340
|
+
expect(tracer.linesNamed('Postgres pool')).toHaveLength(1);
|
|
341
|
+
expect(clock.live()).toHaveLength(0);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should still report each physical connection as it is made and removed', () => {
|
|
345
|
+
tracePool(pool.asPool());
|
|
346
|
+
|
|
347
|
+
pool.connect({ total: 1, idle: 0, waiting: 0 });
|
|
348
|
+
pool.remove({ total: 0, idle: 0, waiting: 0 });
|
|
349
|
+
|
|
350
|
+
expect(tracer.linesNamed('Postgres connected')).toEqual([{
|
|
351
|
+
message: 'Postgres connected',
|
|
352
|
+
measurements: { total: 1, idle: 0, waiting: 0 }
|
|
353
|
+
}]);
|
|
354
|
+
expect(tracer.linesNamed('Postgres disconnected')).toEqual([{
|
|
355
|
+
message: 'Postgres disconnected',
|
|
356
|
+
measurements: { total: 0, idle: 0, waiting: 0 }
|
|
357
|
+
}]);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should still report a pool error', () => {
|
|
361
|
+
tracePool(pool.asPool());
|
|
362
|
+
|
|
363
|
+
const error = new Error('connection terminated unexpectedly');
|
|
364
|
+
pool.fail(error);
|
|
365
|
+
|
|
366
|
+
expect(tracer.errors).toEqual([error]);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Specification } from "jinaga";
|
|
2
2
|
import { ensureGetFactTypeId, FactTypeMap, getRoleId } from "../../src/postgres/maps";
|
|
3
|
-
import { purgeSqlFromSpecification } from "../../src/postgres/purge-sql";
|
|
3
|
+
import { purgeSqlFromSpecification, purgeSqlFromSpecifications } from "../../src/postgres/purge-sql";
|
|
4
4
|
import { model, Site, SiteDeleted, SitePurged } from "../models/blog";
|
|
5
5
|
|
|
6
6
|
describe("Purge SQL", () => {
|
|
@@ -21,7 +21,7 @@ describe("Purge SQL", () => {
|
|
|
21
21
|
const { sql, parameters } = whenGeneratePurgeSql(specification, factTypes, roleMap, schema);
|
|
22
22
|
|
|
23
23
|
const expected =
|
|
24
|
-
`WITH
|
|
24
|
+
`WITH candidates1 AS (
|
|
25
25
|
SELECT
|
|
26
26
|
f1.fact_id as purge_root,
|
|
27
27
|
f2.fact_id as trigger1
|
|
@@ -32,20 +32,24 @@ describe("Purge SQL", () => {
|
|
|
32
32
|
JOIN public.fact f2
|
|
33
33
|
ON f2.fact_id = e1.successor_fact_id
|
|
34
34
|
WHERE f1.fact_type_id = $1
|
|
35
|
+
), roots AS (
|
|
36
|
+
SELECT purge_root FROM candidates1
|
|
37
|
+
), triggers AS (
|
|
38
|
+
SELECT trigger1 AS fact_id FROM candidates1
|
|
35
39
|
), targets AS (
|
|
36
40
|
SELECT a.fact_id
|
|
37
41
|
FROM public.ancestor a
|
|
38
|
-
JOIN
|
|
42
|
+
JOIN roots r ON r.purge_root = a.ancestor_fact_id
|
|
39
43
|
WHERE NOT EXISTS (
|
|
40
44
|
SELECT 1
|
|
41
|
-
FROM
|
|
42
|
-
WHERE a.fact_id =
|
|
45
|
+
FROM triggers t
|
|
46
|
+
WHERE a.fact_id = t.fact_id
|
|
43
47
|
)
|
|
44
48
|
AND NOT EXISTS (
|
|
45
49
|
SELECT 1
|
|
46
|
-
FROM
|
|
50
|
+
FROM triggers t
|
|
47
51
|
JOIN public.ancestor a2
|
|
48
|
-
ON a2.fact_id =
|
|
52
|
+
ON a2.fact_id = t.fact_id
|
|
49
53
|
WHERE a.fact_id = a2.ancestor_fact_id
|
|
50
54
|
)
|
|
51
55
|
), facts AS (
|
|
@@ -83,7 +87,7 @@ SELECT fact_id FROM facts
|
|
|
83
87
|
const { sql, parameters } = whenGeneratePurgeSql(specification, factTypes, roleMap, schema);
|
|
84
88
|
|
|
85
89
|
const expected =
|
|
86
|
-
`WITH
|
|
90
|
+
`WITH candidates1 AS (
|
|
87
91
|
SELECT
|
|
88
92
|
f1.fact_id as purge_root,
|
|
89
93
|
f3.fact_id as trigger1
|
|
@@ -99,20 +103,24 @@ SELECT fact_id FROM facts
|
|
|
99
103
|
JOIN public.fact f3
|
|
100
104
|
ON f3.fact_id = e2.successor_fact_id
|
|
101
105
|
WHERE f1.fact_type_id = $1
|
|
106
|
+
), roots AS (
|
|
107
|
+
SELECT purge_root FROM candidates1
|
|
108
|
+
), triggers AS (
|
|
109
|
+
SELECT trigger1 AS fact_id FROM candidates1
|
|
102
110
|
), targets AS (
|
|
103
111
|
SELECT a.fact_id
|
|
104
112
|
FROM public.ancestor a
|
|
105
|
-
JOIN
|
|
113
|
+
JOIN roots r ON r.purge_root = a.ancestor_fact_id
|
|
106
114
|
WHERE NOT EXISTS (
|
|
107
115
|
SELECT 1
|
|
108
|
-
FROM
|
|
109
|
-
WHERE a.fact_id =
|
|
116
|
+
FROM triggers t
|
|
117
|
+
WHERE a.fact_id = t.fact_id
|
|
110
118
|
)
|
|
111
119
|
AND NOT EXISTS (
|
|
112
120
|
SELECT 1
|
|
113
|
-
FROM
|
|
121
|
+
FROM triggers t
|
|
114
122
|
JOIN public.ancestor a2
|
|
115
|
-
ON a2.fact_id =
|
|
123
|
+
ON a2.fact_id = t.fact_id
|
|
116
124
|
WHERE a.fact_id = a2.ancestor_fact_id
|
|
117
125
|
)
|
|
118
126
|
), facts AS (
|
|
@@ -154,7 +162,7 @@ SELECT fact_id FROM facts
|
|
|
154
162
|
const { sql, parameters } = whenGeneratePurgeSql(specification, factTypes, roleMap, schema);
|
|
155
163
|
|
|
156
164
|
const expected =
|
|
157
|
-
`WITH
|
|
165
|
+
`WITH candidates1 AS (
|
|
158
166
|
SELECT
|
|
159
167
|
f1.fact_id as purge_root,
|
|
160
168
|
f2.fact_id as trigger1,
|
|
@@ -171,28 +179,119 @@ SELECT fact_id FROM facts
|
|
|
171
179
|
JOIN public.fact f3
|
|
172
180
|
ON f3.fact_id = e2.successor_fact_id
|
|
173
181
|
WHERE f1.fact_type_id = $1
|
|
182
|
+
), roots AS (
|
|
183
|
+
SELECT purge_root FROM candidates1
|
|
184
|
+
), triggers AS (
|
|
185
|
+
SELECT trigger1 AS fact_id FROM candidates1
|
|
186
|
+
UNION
|
|
187
|
+
SELECT trigger2 AS fact_id FROM candidates1
|
|
174
188
|
), targets AS (
|
|
175
189
|
SELECT a.fact_id
|
|
176
190
|
FROM public.ancestor a
|
|
177
|
-
JOIN
|
|
191
|
+
JOIN roots r ON r.purge_root = a.ancestor_fact_id
|
|
178
192
|
WHERE NOT EXISTS (
|
|
179
193
|
SELECT 1
|
|
180
|
-
FROM
|
|
181
|
-
WHERE a.fact_id =
|
|
182
|
-
OR a.fact_id = c2.trigger2
|
|
194
|
+
FROM triggers t
|
|
195
|
+
WHERE a.fact_id = t.fact_id
|
|
183
196
|
)
|
|
184
197
|
AND NOT EXISTS (
|
|
185
198
|
SELECT 1
|
|
186
|
-
FROM
|
|
199
|
+
FROM triggers t
|
|
187
200
|
JOIN public.ancestor a2
|
|
188
|
-
ON a2.fact_id =
|
|
201
|
+
ON a2.fact_id = t.fact_id
|
|
189
202
|
WHERE a.fact_id = a2.ancestor_fact_id
|
|
190
203
|
)
|
|
204
|
+
), facts AS (
|
|
205
|
+
DELETE
|
|
206
|
+
FROM public.fact f
|
|
207
|
+
USING targets t WHERE t.fact_id = f.fact_id
|
|
208
|
+
RETURNING f.fact_id
|
|
209
|
+
)
|
|
210
|
+
SELECT fact_id FROM facts
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
expect(sql).toBe(expected);
|
|
214
|
+
expect(parameters).toEqual([
|
|
215
|
+
ensureGetFactTypeId(factTypes, Site.Type),
|
|
216
|
+
getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site'),
|
|
217
|
+
getRoleId(roleMap, ensureGetFactTypeId(factTypes, SitePurged.Type), 'deleted')
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
it("should spare the triggers of every condition on one given type", () => {
|
|
221
|
+
const deletedSpecification = model.given(Site).match((site, facts) =>
|
|
222
|
+
facts.ofType(SiteDeleted)
|
|
223
|
+
.join(deleted => deleted.site, site)
|
|
224
|
+
).specification;
|
|
225
|
+
const purgedSpecification = model.given(Site).match((site, facts) =>
|
|
226
|
+
facts.ofType(SitePurged)
|
|
227
|
+
.join(purged => purged.deleted.site, site)
|
|
228
|
+
).specification;
|
|
229
|
+
const factTypes = buildFactTypeMap(
|
|
230
|
+
Site.Type,
|
|
231
|
+
SiteDeleted.Type,
|
|
232
|
+
SitePurged.Type
|
|
233
|
+
);
|
|
234
|
+
const roleMap = buildRoleMap(factTypes,
|
|
235
|
+
[Site.Type, ['creator']],
|
|
236
|
+
[SiteDeleted.Type, ['site']],
|
|
237
|
+
[SitePurged.Type, ['deleted']]
|
|
238
|
+
);
|
|
239
|
+
const purgeCommand = purgeSqlFromSpecifications([deletedSpecification, purgedSpecification], factTypes, roleMap, 'public');
|
|
240
|
+
if (purgeCommand === null) {
|
|
241
|
+
throw new Error("The specifications were not satisfiable.");
|
|
242
|
+
}
|
|
243
|
+
const { sql, parameters } = purgeCommand;
|
|
244
|
+
|
|
245
|
+
const expected =
|
|
246
|
+
`WITH candidates1 AS (
|
|
247
|
+
SELECT
|
|
248
|
+
f1.fact_id as purge_root,
|
|
249
|
+
f2.fact_id as trigger1
|
|
250
|
+
FROM public.fact f1
|
|
251
|
+
JOIN public.edge e1
|
|
252
|
+
ON e1.predecessor_fact_id = f1.fact_id
|
|
253
|
+
AND e1.role_id = $2
|
|
254
|
+
JOIN public.fact f2
|
|
255
|
+
ON f2.fact_id = e1.successor_fact_id
|
|
256
|
+
WHERE f1.fact_type_id = $1
|
|
257
|
+
), candidates2 AS (
|
|
258
|
+
SELECT
|
|
259
|
+
f1.fact_id as purge_root,
|
|
260
|
+
f3.fact_id as trigger1
|
|
261
|
+
FROM public.fact f1
|
|
262
|
+
JOIN public.edge e1
|
|
263
|
+
ON e1.predecessor_fact_id = f1.fact_id
|
|
264
|
+
AND e1.role_id = $4
|
|
265
|
+
JOIN public.fact f2
|
|
266
|
+
ON f2.fact_id = e1.successor_fact_id
|
|
267
|
+
JOIN public.edge e2
|
|
268
|
+
ON e2.predecessor_fact_id = f2.fact_id
|
|
269
|
+
AND e2.role_id = $5
|
|
270
|
+
JOIN public.fact f3
|
|
271
|
+
ON f3.fact_id = e2.successor_fact_id
|
|
272
|
+
WHERE f1.fact_type_id = $3
|
|
273
|
+
), roots AS (
|
|
274
|
+
SELECT purge_root FROM candidates1
|
|
275
|
+
UNION
|
|
276
|
+
SELECT purge_root FROM candidates2
|
|
277
|
+
), triggers AS (
|
|
278
|
+
SELECT trigger1 AS fact_id FROM candidates1
|
|
279
|
+
UNION
|
|
280
|
+
SELECT trigger1 AS fact_id FROM candidates2
|
|
281
|
+
), targets AS (
|
|
282
|
+
SELECT a.fact_id
|
|
283
|
+
FROM public.ancestor a
|
|
284
|
+
JOIN roots r ON r.purge_root = a.ancestor_fact_id
|
|
285
|
+
WHERE NOT EXISTS (
|
|
286
|
+
SELECT 1
|
|
287
|
+
FROM triggers t
|
|
288
|
+
WHERE a.fact_id = t.fact_id
|
|
289
|
+
)
|
|
191
290
|
AND NOT EXISTS (
|
|
192
291
|
SELECT 1
|
|
193
|
-
FROM
|
|
292
|
+
FROM triggers t
|
|
194
293
|
JOIN public.ancestor a2
|
|
195
|
-
ON a2.fact_id =
|
|
294
|
+
ON a2.fact_id = t.fact_id
|
|
196
295
|
WHERE a.fact_id = a2.ancestor_fact_id
|
|
197
296
|
)
|
|
198
297
|
), facts AS (
|
|
@@ -206,6 +305,8 @@ SELECT fact_id FROM facts
|
|
|
206
305
|
|
|
207
306
|
expect(sql).toBe(expected);
|
|
208
307
|
expect(parameters).toEqual([
|
|
308
|
+
ensureGetFactTypeId(factTypes, Site.Type),
|
|
309
|
+
getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site'),
|
|
209
310
|
ensureGetFactTypeId(factTypes, Site.Type),
|
|
210
311
|
getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site'),
|
|
211
312
|
getRoleId(roleMap, ensureGetFactTypeId(factTypes, SitePurged.Type), 'deleted')
|