@sixb/pg 0.1.13 → 0.1.15
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/dist/auth-storage/auth-storage.d.ts +2 -1
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/rows.d.ts +1 -1
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +779 -357
- package/dist/index.js.map +16 -16
- package/dist/latest-run-query.d.ts +2 -0
- package/dist/latest-run-query.d.ts.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/shared.d.ts +12 -9
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/ontology-storage/source-cleanup.d.ts +7 -1
- package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
- package/dist/ontology-storage/source-roots.d.ts +8 -0
- package/dist/ontology-storage/source-roots.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +8 -3
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/pg-client.d.ts +2 -0
- package/dist/pg-client.d.ts.map +1 -1
- package/dist/pg-projection-run-storage.d.ts +3 -1
- package/dist/pg-projection-run-storage.d.ts.map +1 -1
- package/dist/storage-errors.d.ts +13 -3
- package/dist/storage-errors.d.ts.map +1 -1
- package/dist/transactions.d.ts +23 -1
- package/dist/transactions.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/auth-storage/auth-storage.ts +43 -75
- package/src/auth-storage/rows.ts +1 -1
- package/src/index.ts +8 -1
- package/src/latest-run-query.ts +10 -2
- package/src/migrations/050-directory-group-memberships.sql +5 -0
- package/src/migrations/051-projection-run-supersession.sql +43 -0
- package/src/migrations/052-compact-source-storage.sql +118 -0
- package/src/migrations.ts +30 -23
- package/src/ontology-storage/materialization-state.ts +20 -46
- package/src/ontology-storage/shared.ts +18 -12
- package/src/ontology-storage/source-cleanup.ts +66 -22
- package/src/ontology-storage/source-roots.ts +164 -102
- package/src/ontology-storage/sources.ts +150 -120
- package/src/pg-client.ts +132 -2
- package/src/pg-projection-run-storage.ts +56 -2
- package/src/storage-errors.ts +101 -15
- package/src/transactions.ts +113 -20
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- Projection runs gain the terminal status `superseded`, and a running run may carry the failure
|
|
2
|
+
-- of its last attempt in `error` while the queue retries it.
|
|
3
|
+
|
|
4
|
+
-- The status checks were unnamed. Drop exactly the two that enumerate the terminal statuses, then
|
|
5
|
+
-- replace them with named ones that admit `superseded`.
|
|
6
|
+
DO $$
|
|
7
|
+
DECLARE status_constraint RECORD;
|
|
8
|
+
BEGIN
|
|
9
|
+
FOR status_constraint IN
|
|
10
|
+
SELECT conname
|
|
11
|
+
FROM pg_constraint
|
|
12
|
+
WHERE conrelid = 'projection_runs'::regclass
|
|
13
|
+
AND contype = 'c'
|
|
14
|
+
AND pg_get_constraintdef(oid) LIKE '%''cancelled''%'
|
|
15
|
+
LOOP
|
|
16
|
+
EXECUTE format('ALTER TABLE projection_runs DROP CONSTRAINT %I', status_constraint.conname);
|
|
17
|
+
END LOOP;
|
|
18
|
+
END $$;
|
|
19
|
+
|
|
20
|
+
ALTER TABLE projection_runs
|
|
21
|
+
ADD CONSTRAINT projection_runs_status_check CHECK (
|
|
22
|
+
status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled', 'superseded')
|
|
23
|
+
),
|
|
24
|
+
ADD CONSTRAINT projection_runs_lifecycle_check CHECK (
|
|
25
|
+
(status = 'queued' AND attempt = 0 AND started_at IS NULL AND finished_at IS NULL
|
|
26
|
+
AND error IS NULL)
|
|
27
|
+
OR (status = 'running' AND attempt >= 1 AND started_at IS NOT NULL AND finished_at IS NULL)
|
|
28
|
+
OR (
|
|
29
|
+
status IN ('succeeded', 'failed', 'cancelled', 'superseded')
|
|
30
|
+
AND finished_at IS NOT NULL
|
|
31
|
+
AND (
|
|
32
|
+
(error->>'code' = 'queue.enqueue_failed'
|
|
33
|
+
AND status = 'failed' AND attempt = 0 AND started_at IS NULL)
|
|
34
|
+
OR (COALESCE(error->>'code', '') != 'queue.enqueue_failed'
|
|
35
|
+
AND attempt >= 1 AND started_at IS NOT NULL)
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
),
|
|
39
|
+
-- A run is superseded instead of materialized, so it carries no failure, and only replacement
|
|
40
|
+
-- runs can be: telemetry appends readings rather than replace a source.
|
|
41
|
+
ADD CONSTRAINT projection_runs_superseded_check CHECK (
|
|
42
|
+
status != 'superseded' OR (error IS NULL AND materialization_protocol = 'replacement')
|
|
43
|
+
);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
-- Source versions get a compact surrogate key; roots and rows stop repeating the text triple.
|
|
2
|
+
ALTER TABLE ontology_sources ADD COLUMN version_id BIGINT GENERATED ALWAYS AS IDENTITY;
|
|
3
|
+
ALTER TABLE ontology_sources ADD CONSTRAINT ontology_sources_version_id_key UNIQUE (version_id);
|
|
4
|
+
|
|
5
|
+
ALTER TABLE ontology_source_rows RENAME TO legacy_ontology_source_rows;
|
|
6
|
+
ALTER TABLE ontology_source_roots RENAME TO legacy_ontology_source_roots;
|
|
7
|
+
-- Index names are schema-wide: free them for the new tables.
|
|
8
|
+
DO $$
|
|
9
|
+
DECLARE
|
|
10
|
+
legacy record;
|
|
11
|
+
BEGIN
|
|
12
|
+
FOR legacy IN
|
|
13
|
+
SELECT index_class.relname AS name
|
|
14
|
+
FROM pg_index
|
|
15
|
+
JOIN pg_class AS index_class ON index_class.oid = pg_index.indexrelid
|
|
16
|
+
JOIN pg_class AS table_class ON table_class.oid = pg_index.indrelid
|
|
17
|
+
WHERE table_class.relnamespace = current_schema()::regnamespace
|
|
18
|
+
AND table_class.relname IN ('legacy_ontology_source_rows', 'legacy_ontology_source_roots')
|
|
19
|
+
LOOP
|
|
20
|
+
EXECUTE format('ALTER INDEX %I RENAME TO %I', legacy.name, left('legacy_' || legacy.name, 63));
|
|
21
|
+
END LOOP;
|
|
22
|
+
END $$;
|
|
23
|
+
|
|
24
|
+
-- A root is live while its version is published (active or superseded), it is not retired and
|
|
25
|
+
-- it is not a deletion. Activation therefore writes only the roots it replaces, never its own.
|
|
26
|
+
-- `root_key` is the canonical key; "C" orders it by its UTF-8 bytes.
|
|
27
|
+
CREATE TABLE ontology_source_roots (
|
|
28
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
29
|
+
version_id BIGINT NOT NULL REFERENCES ontology_sources (version_id) ON DELETE RESTRICT,
|
|
30
|
+
project_id TEXT NOT NULL,
|
|
31
|
+
root_key TEXT COLLATE "C" NOT NULL,
|
|
32
|
+
staging_ordinal BIGINT NOT NULL CHECK (staging_ordinal >= 0),
|
|
33
|
+
deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
|
34
|
+
retired_at TIMESTAMPTZ,
|
|
35
|
+
CONSTRAINT ontology_source_roots_key UNIQUE (version_id, root_key),
|
|
36
|
+
CONSTRAINT ontology_source_roots_ordinal UNIQUE (version_id, staging_ordinal)
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX idx_ontology_source_roots_live
|
|
39
|
+
ON ontology_source_roots (project_id, root_key) WHERE retired_at IS NULL AND NOT deleted;
|
|
40
|
+
CREATE INDEX idx_ontology_source_roots_retired
|
|
41
|
+
ON ontology_source_roots (project_id, retired_at) WHERE retired_at IS NOT NULL;
|
|
42
|
+
|
|
43
|
+
-- A row's identity lives in its typed columns; `payload` is the assertion without kind and ref.
|
|
44
|
+
CREATE TABLE ontology_source_rows (
|
|
45
|
+
root_id BIGINT NOT NULL REFERENCES ontology_source_roots (id) ON DELETE RESTRICT,
|
|
46
|
+
entity_kind TEXT NOT NULL CHECK (entity_kind IN ('object', 'link')),
|
|
47
|
+
object_type_id TEXT,
|
|
48
|
+
primary_id TEXT,
|
|
49
|
+
source_type_id TEXT,
|
|
50
|
+
source_primary_id TEXT,
|
|
51
|
+
link_id TEXT,
|
|
52
|
+
target_type_id TEXT,
|
|
53
|
+
target_primary_id TEXT,
|
|
54
|
+
payload JSONB,
|
|
55
|
+
CHECK (
|
|
56
|
+
(
|
|
57
|
+
entity_kind = 'object'
|
|
58
|
+
AND object_type_id IS NOT NULL AND primary_id IS NOT NULL
|
|
59
|
+
AND source_type_id IS NULL AND source_primary_id IS NULL AND link_id IS NULL
|
|
60
|
+
AND target_type_id IS NULL AND target_primary_id IS NULL
|
|
61
|
+
)
|
|
62
|
+
OR
|
|
63
|
+
(
|
|
64
|
+
entity_kind = 'link'
|
|
65
|
+
AND object_type_id IS NULL AND primary_id IS NULL
|
|
66
|
+
AND source_type_id IS NOT NULL AND source_primary_id IS NOT NULL AND link_id IS NOT NULL
|
|
67
|
+
AND target_type_id IS NOT NULL AND target_primary_id IS NOT NULL
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
CREATE INDEX idx_ontology_source_rows_root ON ontology_source_rows (root_id);
|
|
72
|
+
CREATE INDEX idx_ontology_source_rows_link_source
|
|
73
|
+
ON ontology_source_rows (source_type_id, source_primary_id, link_id)
|
|
74
|
+
WHERE entity_kind = 'link';
|
|
75
|
+
CREATE INDEX idx_ontology_source_rows_link_target
|
|
76
|
+
ON ontology_source_rows (target_type_id, target_primary_id)
|
|
77
|
+
WHERE entity_kind = 'link';
|
|
78
|
+
|
|
79
|
+
-- A published root that was not active is retired, so liveness stays exactly what it was.
|
|
80
|
+
INSERT INTO ontology_source_roots (
|
|
81
|
+
version_id, project_id, root_key, staging_ordinal, deleted, retired_at
|
|
82
|
+
)
|
|
83
|
+
SELECT versions.version_id, roots.project_id,
|
|
84
|
+
convert_from(decode(roots.root_sort_key, 'hex'), 'UTF8'), roots.staging_ordinal, roots.deleted,
|
|
85
|
+
CASE
|
|
86
|
+
WHEN roots.active THEN NULL
|
|
87
|
+
WHEN roots.retired_at IS NOT NULL THEN roots.retired_at
|
|
88
|
+
WHEN versions.status IN ('active', 'superseded') THEN versions.updated_at
|
|
89
|
+
END
|
|
90
|
+
FROM legacy_ontology_source_roots AS roots
|
|
91
|
+
JOIN ontology_sources AS versions USING (project_id, source_id, materialization_id);
|
|
92
|
+
|
|
93
|
+
INSERT INTO ontology_source_rows (
|
|
94
|
+
root_id, entity_kind, object_type_id, primary_id,
|
|
95
|
+
source_type_id, source_primary_id, link_id, target_type_id, target_primary_id, payload
|
|
96
|
+
)
|
|
97
|
+
SELECT roots.id, rows.entity_kind, rows.object_type_id, rows.primary_id,
|
|
98
|
+
rows.source_type_id, rows.source_primary_id, rows.link_id, rows.target_type_id,
|
|
99
|
+
rows.target_primary_id, NULLIF(rows.assertion - 'kind' - 'ref', '{}'::jsonb)
|
|
100
|
+
FROM legacy_ontology_source_rows AS rows
|
|
101
|
+
JOIN ontology_sources AS versions USING (project_id, source_id, materialization_id)
|
|
102
|
+
JOIN ontology_source_roots AS roots
|
|
103
|
+
ON roots.version_id = versions.version_id
|
|
104
|
+
AND roots.root_key = convert_from(decode(rows.root_sort_key, 'hex'), 'UTF8');
|
|
105
|
+
|
|
106
|
+
DO $$
|
|
107
|
+
BEGIN
|
|
108
|
+
IF (SELECT count(*) FROM legacy_ontology_source_roots)
|
|
109
|
+
<> (SELECT count(*) FROM ontology_source_roots)
|
|
110
|
+
OR (SELECT count(*) FROM legacy_ontology_source_rows)
|
|
111
|
+
<> (SELECT count(*) FROM ontology_source_rows) THEN
|
|
112
|
+
RAISE EXCEPTION 'Source roots or rows were lost while compacting source storage.';
|
|
113
|
+
END IF;
|
|
114
|
+
END $$;
|
|
115
|
+
|
|
116
|
+
DROP TABLE legacy_ontology_source_rows;
|
|
117
|
+
DROP TABLE legacy_ontology_source_roots;
|
|
118
|
+
ANALYZE ontology_sources, ontology_source_roots, ontology_source_rows;
|
package/src/migrations.ts
CHANGED
|
@@ -106,7 +106,17 @@ import ontologyCommitAttributionSql from "./migrations/047-ontology-commit-attri
|
|
|
106
106
|
type: "text",
|
|
107
107
|
}
|
|
108
108
|
import nativeSessionsSql from "./migrations/049-native-sessions.sql" with { type: "text" }
|
|
109
|
-
import
|
|
109
|
+
import directoryGroupMembershipsSql from "./migrations/050-directory-group-memberships.sql" with {
|
|
110
|
+
type: "text",
|
|
111
|
+
}
|
|
112
|
+
import projectionRunSupersessionSql from "./migrations/051-projection-run-supersession.sql" with {
|
|
113
|
+
type: "text",
|
|
114
|
+
}
|
|
115
|
+
import compactSourceStorageSql from "./migrations/052-compact-source-storage.sql" with {
|
|
116
|
+
type: "text",
|
|
117
|
+
}
|
|
118
|
+
import type { ReservedSQL, SQL, SQLClient } from "./pg-client"
|
|
119
|
+
import { runPgTransactionOn, undoOnFailure, withReservedPgConnection } from "./transactions"
|
|
110
120
|
|
|
111
121
|
export interface PostgresMigrationContext {
|
|
112
122
|
exec(sqlText: string): Promise<void>
|
|
@@ -186,7 +196,7 @@ export function quoteIdent(identifier: string): string {
|
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
function postgresMigrationSession(
|
|
189
|
-
sql:
|
|
199
|
+
sql: ReservedSQL,
|
|
190
200
|
schemaName: string
|
|
191
201
|
): { context: PostgresMigrationContext; state: MigrationHistoryStore } {
|
|
192
202
|
const schema = quoteIdent(schemaName)
|
|
@@ -253,19 +263,15 @@ function postgresMigrationSession(
|
|
|
253
263
|
},
|
|
254
264
|
async transaction(run) {
|
|
255
265
|
// `sql` is the reserved connection holding the migration advisory lock. porsager's
|
|
256
|
-
//
|
|
257
|
-
// route exec()/markStarted()/markApplied() through
|
|
266
|
+
// `.begin` would open the transaction on another pool connection, so drive it on this one
|
|
267
|
+
// and route exec()/markStarted()/markApplied() through it via `active`.
|
|
258
268
|
const previous = active
|
|
259
269
|
active = sql
|
|
260
|
-
await sql.unsafe("BEGIN")
|
|
261
270
|
try {
|
|
262
|
-
await sql
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} catch (error) {
|
|
267
|
-
await sql.unsafe("ROLLBACK")
|
|
268
|
-
throw error
|
|
271
|
+
return await runPgTransactionOn(sql, async () => {
|
|
272
|
+
await sql.unsafe(`SET LOCAL search_path TO ${schema}`)
|
|
273
|
+
return run()
|
|
274
|
+
})
|
|
269
275
|
} finally {
|
|
270
276
|
active = previous
|
|
271
277
|
}
|
|
@@ -312,23 +318,21 @@ async function withPostgresMigrationLock<T>(
|
|
|
312
318
|
readonly schemaName: string
|
|
313
319
|
readonly migrations: MigrationSet<PostgresMigrationContext>
|
|
314
320
|
},
|
|
315
|
-
run: (sql:
|
|
321
|
+
run: (sql: ReservedSQL) => Promise<T>
|
|
316
322
|
): Promise<T> {
|
|
317
|
-
const sql = await params.sql.reserve()
|
|
318
323
|
const [first, second] = advisoryLockParts(
|
|
319
324
|
`storage:migration:${params.schemaName}:${params.migrations.adapterId}`
|
|
320
325
|
)
|
|
321
326
|
|
|
322
|
-
|
|
327
|
+
// A session-level lock outlives any transaction, so it is released before the connection goes
|
|
328
|
+
// back to the pool — except when the connection was lost, which already released it.
|
|
329
|
+
return withReservedPgConnection(params.sql, async (sql) => {
|
|
330
|
+
const unlock = () => sql`SELECT pg_advisory_unlock(${first}, ${second})`
|
|
323
331
|
await sql`SELECT pg_advisory_lock(${first}, ${second})`
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
} finally {
|
|
330
|
-
sql.release()
|
|
331
|
-
}
|
|
332
|
+
const result = await undoOnFailure(() => run(sql), unlock)
|
|
333
|
+
await unlock()
|
|
334
|
+
return result
|
|
335
|
+
})
|
|
332
336
|
}
|
|
333
337
|
|
|
334
338
|
function advisoryLockParts(key: string): readonly [number, number] {
|
|
@@ -408,6 +412,9 @@ export const postgresStorageMigrations = defineMigrations<PostgresMigrationConte
|
|
|
408
412
|
pgSql("047-ontology-commit-attribution", ontologyCommitAttributionSql),
|
|
409
413
|
pgSql("048-file-upload-sessions", fileUploadSessionsSql),
|
|
410
414
|
pgSql("049-native-sessions", nativeSessionsSql),
|
|
415
|
+
pgSql("050-directory-group-memberships", directoryGroupMembershipsSql),
|
|
416
|
+
pgSql("051-projection-run-supersession", projectionRunSupersessionSql),
|
|
417
|
+
pgSql("052-compact-source-storage", compactSourceStorageSql),
|
|
411
418
|
],
|
|
412
419
|
})
|
|
413
420
|
|
|
@@ -38,15 +38,21 @@ import type {
|
|
|
38
38
|
} from "@sixb/core/storage"
|
|
39
39
|
import type { SQLClient } from "../pg-client"
|
|
40
40
|
import {
|
|
41
|
-
databaseSafeInteger,
|
|
42
41
|
jsonParameter,
|
|
43
42
|
linkRefFromColumns,
|
|
44
43
|
objectRefFromColumns,
|
|
45
44
|
type PgOntologySourceAssertionRow,
|
|
46
45
|
type PgStoredOverrideRow,
|
|
46
|
+
sourceAssertion,
|
|
47
47
|
toIsoString,
|
|
48
48
|
} from "./shared"
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
activeSourceRows,
|
|
51
|
+
liveSourceRowsJoin,
|
|
52
|
+
replacementAssertionRows,
|
|
53
|
+
replacementSourceRows,
|
|
54
|
+
sourceAssertionColumns,
|
|
55
|
+
} from "./source-roots"
|
|
50
56
|
|
|
51
57
|
export const PG_MATERIALIZATION_WORK_TABLE = "ontology_materialization_work"
|
|
52
58
|
export const PG_REPLACEMENT_WORK_TABLE = "ontology_replacement_work"
|
|
@@ -410,20 +416,14 @@ export class PgMaterializationStateReader {
|
|
|
410
416
|
scope_sort_key TEXT, source_type_id TEXT, source_id TEXT, link_id TEXT
|
|
411
417
|
)
|
|
412
418
|
)
|
|
413
|
-
SELECT
|
|
419
|
+
SELECT ${sourceAssertionColumns(this.sql)}
|
|
414
420
|
FROM ontology_source_rows AS rows
|
|
415
421
|
JOIN requested
|
|
416
422
|
ON requested.source_type_id = rows.source_type_id
|
|
417
423
|
AND requested.source_id = rows.source_primary_id
|
|
418
424
|
AND requested.link_id = rows.link_id
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
AND sources.source_id = rows.source_id
|
|
422
|
-
AND sources.materialization_id = rows.materialization_id
|
|
423
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
424
|
-
WHERE rows.project_id = ${this.projectId}
|
|
425
|
-
AND rows.entity_kind = 'link'
|
|
426
|
-
AND sources.active
|
|
425
|
+
${liveSourceRowsJoin(this.sql, this.projectId)}
|
|
426
|
+
WHERE rows.entity_kind = 'link'
|
|
427
427
|
`,
|
|
428
428
|
this.sql<LinkSlotOverrideRow[]>`
|
|
429
429
|
WITH requested AS (
|
|
@@ -582,32 +582,20 @@ export class PgMaterializationStateReader {
|
|
|
582
582
|
SELECT rows.source_type_id, rows.source_primary_id AS source_id,
|
|
583
583
|
rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
|
|
584
584
|
FROM ontology_source_rows AS rows
|
|
585
|
-
|
|
586
|
-
ON sources.project_id = rows.project_id
|
|
587
|
-
AND sources.source_id = rows.source_id
|
|
588
|
-
AND sources.materialization_id = rows.materialization_id
|
|
589
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
585
|
+
${liveSourceRowsJoin(this.sql, this.projectId)}
|
|
590
586
|
JOIN requested
|
|
591
587
|
ON requested.object_type_id = rows.source_type_id
|
|
592
588
|
AND requested.primary_id = rows.source_primary_id
|
|
593
|
-
WHERE rows.
|
|
594
|
-
AND rows.entity_kind = 'link'
|
|
595
|
-
AND sources.active
|
|
589
|
+
WHERE rows.entity_kind = 'link'
|
|
596
590
|
UNION
|
|
597
591
|
SELECT rows.source_type_id, rows.source_primary_id AS source_id,
|
|
598
592
|
rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
|
|
599
593
|
FROM ontology_source_rows AS rows
|
|
600
|
-
|
|
601
|
-
ON sources.project_id = rows.project_id
|
|
602
|
-
AND sources.source_id = rows.source_id
|
|
603
|
-
AND sources.materialization_id = rows.materialization_id
|
|
604
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
594
|
+
${liveSourceRowsJoin(this.sql, this.projectId)}
|
|
605
595
|
JOIN requested
|
|
606
596
|
ON requested.object_type_id = rows.target_type_id
|
|
607
597
|
AND requested.primary_id = rows.target_primary_id
|
|
608
|
-
WHERE rows.
|
|
609
|
-
AND rows.entity_kind = 'link'
|
|
610
|
-
AND sources.active
|
|
598
|
+
WHERE rows.entity_kind = 'link'
|
|
611
599
|
), selected AS (
|
|
612
600
|
SELECT DISTINCT links.*,
|
|
613
601
|
${this.sql.unsafe(linkSortExpression("links"))} AS sort_key
|
|
@@ -912,32 +900,20 @@ export class PgMaterializationStateReader {
|
|
|
912
900
|
SELECT rows.source_type_id, rows.source_primary_id AS source_id,
|
|
913
901
|
rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
|
|
914
902
|
FROM ontology_source_rows AS rows
|
|
915
|
-
|
|
916
|
-
ON sources.project_id = rows.project_id
|
|
917
|
-
AND sources.source_id = rows.source_id
|
|
918
|
-
AND sources.materialization_id = rows.materialization_id
|
|
919
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
903
|
+
${liveSourceRowsJoin(this.sql, this.projectId)}
|
|
920
904
|
JOIN incident_objects
|
|
921
905
|
ON incident_objects.object_type_id = rows.source_type_id
|
|
922
906
|
AND incident_objects.primary_id = rows.source_primary_id
|
|
923
|
-
WHERE rows.
|
|
924
|
-
AND rows.entity_kind = 'link'
|
|
925
|
-
AND sources.active
|
|
907
|
+
WHERE rows.entity_kind = 'link'
|
|
926
908
|
UNION
|
|
927
909
|
SELECT rows.source_type_id, rows.source_primary_id AS source_id,
|
|
928
910
|
rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
|
|
929
911
|
FROM ontology_source_rows AS rows
|
|
930
|
-
|
|
931
|
-
ON sources.project_id = rows.project_id
|
|
932
|
-
AND sources.source_id = rows.source_id
|
|
933
|
-
AND sources.materialization_id = rows.materialization_id
|
|
934
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
912
|
+
${liveSourceRowsJoin(this.sql, this.projectId)}
|
|
935
913
|
JOIN incident_objects
|
|
936
914
|
ON incident_objects.object_type_id = rows.target_type_id
|
|
937
915
|
AND incident_objects.primary_id = rows.target_primary_id
|
|
938
|
-
WHERE rows.
|
|
939
|
-
AND rows.entity_kind = 'link'
|
|
940
|
-
AND sources.active
|
|
916
|
+
WHERE rows.entity_kind = 'link'
|
|
941
917
|
), diff_links AS (
|
|
942
918
|
SELECT * FROM replacement_links
|
|
943
919
|
UNION SELECT * FROM incident_links
|
|
@@ -1104,9 +1080,7 @@ function storedSource(row: PgOntologySourceAssertionRow): StoredSourceAssertion
|
|
|
1104
1080
|
return {
|
|
1105
1081
|
source: { projectionId: row.source_id },
|
|
1106
1082
|
materializationId: row.materialization_id,
|
|
1107
|
-
|
|
1108
|
-
assertion: structuredClone(row.assertion) as StoredSourceAssertion["assertion"],
|
|
1109
|
-
stagingOrdinal: databaseSafeInteger(row.staging_ordinal, "Source staging ordinal"),
|
|
1083
|
+
...sourceAssertion(row),
|
|
1110
1084
|
} as StoredSourceAssertion
|
|
1111
1085
|
}
|
|
1112
1086
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { parseSixbFailure } from "@sixb/core/internal/errors"
|
|
2
|
-
import type { ProjectionEntityRef } from "@sixb/core/internal/materialization"
|
|
3
2
|
import { MaterializationConflictError } from "@sixb/core/internal/materialization"
|
|
3
|
+
import {
|
|
4
|
+
sourceAssertionFromColumns,
|
|
5
|
+
sourceEntityFromKey,
|
|
6
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
4
7
|
import type {
|
|
5
8
|
AssertSourceMaterializationExecutionInput,
|
|
6
9
|
OntologyCommitRecord,
|
|
@@ -50,6 +53,7 @@ export interface PgOntologyCommitRow {
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
export interface PgOntologySourceRow {
|
|
56
|
+
readonly version_id: string
|
|
53
57
|
readonly project_id: string
|
|
54
58
|
readonly source_id: string
|
|
55
59
|
readonly materialization_id: string
|
|
@@ -76,19 +80,21 @@ export interface PgOntologySourceRow {
|
|
|
76
80
|
readonly updated_at: Date | string
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
/** An assertion with its root and version, as `sourceAssertionColumns` selects it. */
|
|
79
84
|
export interface PgOntologySourceAssertionRow {
|
|
80
|
-
readonly project_id: string
|
|
81
85
|
readonly source_id: string
|
|
82
86
|
readonly materialization_id: string
|
|
83
|
-
readonly
|
|
84
|
-
readonly entity_key: unknown
|
|
85
|
-
readonly entity_sort_key: string
|
|
86
|
-
readonly root_kind: "object" | "link"
|
|
87
|
-
readonly root_key: unknown
|
|
88
|
-
readonly root_sort_key: string
|
|
87
|
+
readonly root_key: string
|
|
89
88
|
readonly staging_ordinal: number | string
|
|
90
|
-
readonly
|
|
91
|
-
readonly
|
|
89
|
+
readonly entity_kind: "object" | "link"
|
|
90
|
+
readonly object_type_id: string | null
|
|
91
|
+
readonly primary_id: string | null
|
|
92
|
+
readonly source_type_id: string | null
|
|
93
|
+
readonly source_primary_id: string | null
|
|
94
|
+
readonly link_id: string | null
|
|
95
|
+
readonly target_type_id: string | null
|
|
96
|
+
readonly target_primary_id: string | null
|
|
97
|
+
readonly payload: unknown
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
export interface PgStoredOverrideRow {
|
|
@@ -174,8 +180,8 @@ export function sourceRecord(row: PgOntologySourceRow): OntologySourceRecord {
|
|
|
174
180
|
|
|
175
181
|
export function sourceAssertion(row: PgOntologySourceAssertionRow): StageSourceAssertion {
|
|
176
182
|
return {
|
|
177
|
-
root:
|
|
178
|
-
assertion:
|
|
183
|
+
root: sourceEntityFromKey(row.root_key),
|
|
184
|
+
assertion: sourceAssertionFromColumns(row, row.payload),
|
|
179
185
|
stagingOrdinal: databaseSafeInteger(row.staging_ordinal, "Source staging ordinal"),
|
|
180
186
|
}
|
|
181
187
|
}
|
|
@@ -1,24 +1,74 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CleanupTerminalSourceMaterializationsInput,
|
|
3
3
|
CleanupTerminalSourceMaterializationsResult,
|
|
4
|
+
PurgeAbandonedSourceMaterializationsInput,
|
|
4
5
|
} from "@sixb/core/storage"
|
|
5
6
|
import type { SQLClient } from "../pg-client"
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Superseded versions can still hold live roots (a delta keeps unchanged roots where they were),
|
|
10
|
+
* so they are cleaned root by root once retired. An abandoned candidate was never published:
|
|
11
|
+
* everything it holds goes, one whole version at a time, each delete on its own version prefix.
|
|
12
|
+
*/
|
|
13
|
+
export async function purgeAbandonedSourceVersions(
|
|
14
|
+
sql: SQLClient,
|
|
15
|
+
input: PurgeAbandonedSourceMaterializationsInput
|
|
16
|
+
): Promise<CleanupTerminalSourceMaterializationsResult> {
|
|
17
|
+
let remaining = input.limit
|
|
18
|
+
let rowsDeleted = 0
|
|
19
|
+
let materializationsDeleted = 0
|
|
20
|
+
while (remaining > 0) {
|
|
21
|
+
const [candidate] = await sql<{ readonly version_id: string }[]>`
|
|
22
|
+
SELECT version_id FROM ontology_sources
|
|
23
|
+
WHERE project_id = ${input.projectId} AND status = 'abandoned'
|
|
24
|
+
ORDER BY terminal_at, source_id, materialization_id
|
|
25
|
+
LIMIT 1 FOR UPDATE SKIP LOCKED
|
|
26
|
+
`
|
|
27
|
+
if (!candidate) break
|
|
28
|
+
const rows = await sql`
|
|
29
|
+
DELETE FROM ontology_source_rows WHERE ctid IN (
|
|
30
|
+
SELECT rows.ctid FROM ontology_source_roots AS roots
|
|
31
|
+
JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
32
|
+
WHERE roots.version_id = ${candidate.version_id} LIMIT ${remaining}
|
|
33
|
+
)
|
|
34
|
+
`
|
|
35
|
+
rowsDeleted += rows.count
|
|
36
|
+
remaining -= rows.count
|
|
37
|
+
if (remaining === 0) break
|
|
38
|
+
const roots = await sql`
|
|
39
|
+
DELETE FROM ontology_source_roots WHERE ctid IN (
|
|
40
|
+
SELECT ctid FROM ontology_source_roots
|
|
41
|
+
WHERE version_id = ${candidate.version_id} LIMIT ${remaining}
|
|
42
|
+
)
|
|
43
|
+
`
|
|
44
|
+
rowsDeleted += roots.count
|
|
45
|
+
remaining -= roots.count
|
|
46
|
+
if (remaining === 0) break
|
|
47
|
+
const manifest = await sql`
|
|
48
|
+
DELETE FROM ontology_sources
|
|
49
|
+
WHERE version_id = ${candidate.version_id} AND status = 'abandoned'
|
|
50
|
+
`
|
|
51
|
+
if (manifest.count === 0) break
|
|
52
|
+
materializationsDeleted += manifest.count
|
|
53
|
+
remaining -= manifest.count
|
|
54
|
+
}
|
|
55
|
+
return { rowsDeleted, materializationsDeleted }
|
|
56
|
+
}
|
|
57
|
+
|
|
7
58
|
export async function cleanupSourceVersions(
|
|
8
59
|
sql: SQLClient,
|
|
9
60
|
input: CleanupTerminalSourceMaterializationsInput
|
|
10
61
|
): Promise<CleanupTerminalSourceMaterializationsResult> {
|
|
11
62
|
let remaining = input.limit
|
|
63
|
+
const retired = sql`roots.project_id = ${input.projectId}
|
|
64
|
+
AND roots.retired_at < ${input.terminalBefore} AND versions.status = 'superseded'`
|
|
12
65
|
const removedRows = await sql`
|
|
13
66
|
WITH selected AS (
|
|
14
67
|
SELECT rows.ctid FROM ontology_source_roots AS roots
|
|
15
|
-
JOIN ontology_sources AS
|
|
16
|
-
JOIN ontology_source_rows AS rows
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
WHERE roots.project_id = ${input.projectId} AND NOT roots.active AND roots.retired_at < ${input.terminalBefore}
|
|
20
|
-
AND sources.status IN ('superseded', 'abandoned')
|
|
21
|
-
ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key, rows.entity_sort_key
|
|
68
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
69
|
+
JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
70
|
+
WHERE ${retired}
|
|
71
|
+
ORDER BY roots.retired_at, roots.id
|
|
22
72
|
LIMIT ${remaining} FOR UPDATE OF rows SKIP LOCKED
|
|
23
73
|
) DELETE FROM ontology_source_rows AS rows USING selected WHERE rows.ctid = selected.ctid RETURNING 1
|
|
24
74
|
`
|
|
@@ -28,13 +78,10 @@ export async function cleanupSourceVersions(
|
|
|
28
78
|
const removed = await sql`
|
|
29
79
|
WITH selected AS (
|
|
30
80
|
SELECT roots.ctid FROM ontology_source_roots AS roots
|
|
31
|
-
JOIN ontology_sources AS
|
|
32
|
-
WHERE
|
|
33
|
-
AND
|
|
34
|
-
|
|
35
|
-
WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
36
|
-
AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key)
|
|
37
|
-
ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key
|
|
81
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
82
|
+
WHERE ${retired}
|
|
83
|
+
AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id)
|
|
84
|
+
ORDER BY roots.retired_at, roots.id
|
|
38
85
|
LIMIT ${remaining} FOR UPDATE OF roots SKIP LOCKED
|
|
39
86
|
) DELETE FROM ontology_source_roots AS roots USING selected WHERE roots.ctid = selected.ctid RETURNING 1
|
|
40
87
|
`
|
|
@@ -46,16 +93,13 @@ export async function cleanupSourceVersions(
|
|
|
46
93
|
? []
|
|
47
94
|
: await sql`
|
|
48
95
|
WITH selected AS (
|
|
49
|
-
SELECT
|
|
50
|
-
WHERE project_id = ${input.projectId} AND status
|
|
96
|
+
SELECT versions.ctid FROM ontology_sources AS versions
|
|
97
|
+
WHERE project_id = ${input.projectId} AND status = 'superseded'
|
|
98
|
+
AND terminal_at < ${input.terminalBefore}
|
|
51
99
|
AND NOT EXISTS (SELECT 1 FROM ontology_source_roots AS roots
|
|
52
|
-
WHERE roots.
|
|
53
|
-
AND roots.materialization_id = sources.materialization_id)
|
|
54
|
-
AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
|
|
55
|
-
WHERE rows.project_id = sources.project_id AND rows.source_id = sources.source_id
|
|
56
|
-
AND rows.materialization_id = sources.materialization_id)
|
|
100
|
+
WHERE roots.version_id = versions.version_id)
|
|
57
101
|
ORDER BY terminal_at, source_id, materialization_id LIMIT ${remaining} FOR UPDATE SKIP LOCKED
|
|
58
|
-
) DELETE FROM ontology_sources AS
|
|
102
|
+
) DELETE FROM ontology_sources AS versions USING selected WHERE versions.ctid = selected.ctid RETURNING 1
|
|
59
103
|
`
|
|
60
104
|
return { rowsDeleted, materializationsDeleted: removedManifests.length }
|
|
61
105
|
}
|