@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
package/dist/index.js
CHANGED
|
@@ -32,20 +32,34 @@ import {
|
|
|
32
32
|
} from "@sixb/core/storage";
|
|
33
33
|
|
|
34
34
|
// src/storage-errors.ts
|
|
35
|
+
import { createSixbError } from "@sixb/core/internal/errors";
|
|
35
36
|
var UNIQUE_VIOLATION = "23505";
|
|
36
37
|
var FOREIGN_KEY_VIOLATION = "23503";
|
|
37
38
|
var RETRYABLE_TRANSACTION_CONFLICT_CODES = new Set(["40001", "40P01"]);
|
|
38
|
-
var
|
|
39
|
-
"CONNECT_TIMEOUT",
|
|
39
|
+
var CONNECTION_LOST_CODES = new Set([
|
|
40
40
|
"CONNECTION_CLOSED",
|
|
41
41
|
"CONNECTION_DESTROYED",
|
|
42
|
-
"CONNECTION_ENDED",
|
|
43
|
-
"ECONNREFUSED",
|
|
44
42
|
"ECONNRESET",
|
|
45
|
-
"
|
|
43
|
+
"EPIPE",
|
|
44
|
+
"ETIMEDOUT"
|
|
45
|
+
]);
|
|
46
|
+
var CONNECT_FAILED_CODES = new Set([
|
|
47
|
+
"CONNECT_TIMEOUT",
|
|
48
|
+
"ECONNREFUSED",
|
|
46
49
|
"ENOTFOUND",
|
|
47
|
-
"EAI_AGAIN"
|
|
50
|
+
"EAI_AGAIN",
|
|
51
|
+
"EHOSTUNREACH",
|
|
52
|
+
"ENETUNREACH"
|
|
48
53
|
]);
|
|
54
|
+
var LOCK_TABLES = [
|
|
55
|
+
{
|
|
56
|
+
setting: "max_pred_locks_per_transaction",
|
|
57
|
+
table: "predicate lock table",
|
|
58
|
+
note: " A long serializable transaction also keeps the predicate locks of every serializable" + " transaction that commits while it is open."
|
|
59
|
+
},
|
|
60
|
+
{ setting: "max_locks_per_transaction", table: "lock table", note: "" }
|
|
61
|
+
];
|
|
62
|
+
var OUT_OF_MEMORY = "53200";
|
|
49
63
|
function pgErrorCode(error) {
|
|
50
64
|
if (error instanceof Error) {
|
|
51
65
|
const code = error.code;
|
|
@@ -65,30 +79,101 @@ function isRetryableTransactionConflict(error) {
|
|
|
65
79
|
const code = pgErrorCode(error);
|
|
66
80
|
return code !== undefined && RETRYABLE_TRANSACTION_CONFLICT_CODES.has(code);
|
|
67
81
|
}
|
|
82
|
+
function isConnectionLost(error) {
|
|
83
|
+
const code = pgErrorCode(error);
|
|
84
|
+
if (code !== undefined && CONNECTION_LOST_CODES.has(code)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const severity = error instanceof Error ? error.severity : null;
|
|
88
|
+
return severity === "FATAL" || severity === "PANIC";
|
|
89
|
+
}
|
|
90
|
+
function explainPgError(error) {
|
|
91
|
+
if (isStorageUnavailable(error)) {
|
|
92
|
+
return storageUnavailable(error);
|
|
93
|
+
}
|
|
94
|
+
if (!(error instanceof Error) || pgErrorCode(error) !== OUT_OF_MEMORY) {
|
|
95
|
+
return error;
|
|
96
|
+
}
|
|
97
|
+
const hint = error.hint;
|
|
98
|
+
const lockTable = typeof hint === "string" ? LOCK_TABLES.find(({ setting }) => hint.includes(setting)) : undefined;
|
|
99
|
+
if (!lockTable) {
|
|
100
|
+
return error;
|
|
101
|
+
}
|
|
102
|
+
const { setting, table, note } = lockTable;
|
|
103
|
+
return new Error(`[SixbPg] PostgreSQL aborted the transaction because its shared ${table} is full ` + `(SQLSTATE 53200 "${error.message}"). The table holds ${setting} \xD7 (max_connections + ` + `max_prepared_transactions) entries shared by all sessions.${note} Raise ${setting} ` + "(applied at server restart) or shorten the transactions that overlap.", { cause: error });
|
|
104
|
+
}
|
|
105
|
+
function isStorageUnavailable(error) {
|
|
106
|
+
const code = pgErrorCode(error);
|
|
107
|
+
return isConnectionLost(error) || code !== undefined && CONNECT_FAILED_CODES.has(code);
|
|
108
|
+
}
|
|
109
|
+
function storageUnavailable(error) {
|
|
110
|
+
const code = pgErrorCode(error);
|
|
111
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
112
|
+
return createSixbError("storage.unavailable", `[SixbPg] PostgreSQL is unavailable: the connection was lost or could not be opened (${code ?? "no code"}: ${reason}).`, { cause: error, ...code === undefined ? {} : { details: { code } } });
|
|
113
|
+
}
|
|
68
114
|
|
|
69
115
|
// src/transactions.ts
|
|
70
116
|
import { createHash } from "crypto";
|
|
117
|
+
var BEGIN_STATEMENTS = {
|
|
118
|
+
unverifiedDefault: "BEGIN",
|
|
119
|
+
repeatableRead: "BEGIN ISOLATION LEVEL REPEATABLE READ",
|
|
120
|
+
serializable: "BEGIN ISOLATION LEVEL SERIALIZABLE"
|
|
121
|
+
};
|
|
122
|
+
var pgTransactionClients = new WeakSet;
|
|
71
123
|
var activePgTransactions = new WeakMap;
|
|
72
124
|
async function runPgTransaction(sql, run, options = {}) {
|
|
73
125
|
if (!canStartPgTransaction(sql)) {
|
|
74
126
|
return run(sql);
|
|
75
127
|
}
|
|
76
128
|
const isolation = options.isolation ?? "unverifiedDefault";
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
|
|
129
|
+
try {
|
|
130
|
+
return await withReservedPgConnection(sql, (tx) => {
|
|
131
|
+
pgTransactionClients.add(tx);
|
|
132
|
+
return runPgTransactionOn(tx, async () => {
|
|
133
|
+
activePgTransactions.set(tx, isolation);
|
|
134
|
+
try {
|
|
135
|
+
return await run(tx);
|
|
136
|
+
} finally {
|
|
137
|
+
activePgTransactions.delete(tx);
|
|
138
|
+
}
|
|
139
|
+
}, isolation);
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw explainPgError(error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function withReservedPgConnection(sql, run) {
|
|
146
|
+
const connection = await sql.reserve();
|
|
147
|
+
let usable = true;
|
|
148
|
+
try {
|
|
149
|
+
return await run(connection);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
usable = !isConnectionLost(error);
|
|
152
|
+
throw error;
|
|
153
|
+
} finally {
|
|
154
|
+
if (usable) {
|
|
155
|
+
connection.release();
|
|
83
156
|
}
|
|
84
|
-
};
|
|
85
|
-
if (isolation === "serializable") {
|
|
86
|
-
return sql.begin("isolation level serializable", trackedRun);
|
|
87
157
|
}
|
|
88
|
-
|
|
89
|
-
|
|
158
|
+
}
|
|
159
|
+
async function runPgTransactionOn(connection, run, isolation = "unverifiedDefault") {
|
|
160
|
+
await connection.unsafe(BEGIN_STATEMENTS[isolation]);
|
|
161
|
+
const result = await undoOnFailure(run, () => connection.unsafe("ROLLBACK"));
|
|
162
|
+
const commit = await connection.unsafe("COMMIT");
|
|
163
|
+
if (commit.command !== "COMMIT") {
|
|
164
|
+
throw new Error("[SixbPg] PostgreSQL rolled the transaction back instead of committing it: a statement in it" + " failed and its error was caught without being rethrown. A failed statement aborts the" + " whole transaction, so let its error propagate out of the transaction callback.");
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
async function undoOnFailure(run, undo) {
|
|
169
|
+
try {
|
|
170
|
+
return await run();
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (!isConnectionLost(error)) {
|
|
173
|
+
await undo();
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
90
176
|
}
|
|
91
|
-
return sql.begin(trackedRun);
|
|
92
177
|
}
|
|
93
178
|
async function runPgRepeatableReadTransaction(sql, run) {
|
|
94
179
|
if (canStartPgTransaction(sql)) {
|
|
@@ -121,7 +206,7 @@ function advisoryLockParts(key) {
|
|
|
121
206
|
return [hash.readInt32BE(0), hash.readInt32BE(4)];
|
|
122
207
|
}
|
|
123
208
|
function canStartPgTransaction(sql) {
|
|
124
|
-
return typeof sql.
|
|
209
|
+
return !pgTransactionClients.has(sql) && typeof sql.reserve === "function";
|
|
125
210
|
}
|
|
126
211
|
|
|
127
212
|
// src/agents/rows.ts
|
|
@@ -3055,17 +3140,6 @@ class PgAuthStorage {
|
|
|
3055
3140
|
error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`)
|
|
3056
3141
|
};
|
|
3057
3142
|
}
|
|
3058
|
-
if (shouldCreateUser && !activeInvitation && input.allowUserCreationWithoutInvitation && input.requireNoActiveUsersForUserCreation && await hasActiveUsers(tx, projectId)) {
|
|
3059
|
-
await consumeMagicLink(tx, {
|
|
3060
|
-
projectId,
|
|
3061
|
-
id: input.magicLinkId,
|
|
3062
|
-
tokenHash: input.tokenHash,
|
|
3063
|
-
consumedAt: completedAt
|
|
3064
|
-
});
|
|
3065
|
-
return {
|
|
3066
|
-
error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`)
|
|
3067
|
-
};
|
|
3068
|
-
}
|
|
3069
3143
|
await validateCompleteSessionInput(tx, projectId, input.session);
|
|
3070
3144
|
assertSignInSessionAudience(projectId, input.session.audience, magicLink.audience);
|
|
3071
3145
|
let newUserId;
|
|
@@ -3097,8 +3171,9 @@ class PgAuthStorage {
|
|
|
3097
3171
|
projectId,
|
|
3098
3172
|
user
|
|
3099
3173
|
});
|
|
3100
|
-
const groupMemberships = await this.
|
|
3174
|
+
const groupMemberships = await this.applyGroups(tx, {
|
|
3101
3175
|
completedAt,
|
|
3176
|
+
source: "manual",
|
|
3102
3177
|
existing: invitation.groupMemberships,
|
|
3103
3178
|
groupIds: manualGroupIds,
|
|
3104
3179
|
projectId,
|
|
@@ -3158,36 +3233,18 @@ class PgAuthStorage {
|
|
|
3158
3233
|
error: new AuthStorageError11("missing_user", `[Sixb] User '${identity.user_id}' not found for linked OIDC identity.`)
|
|
3159
3234
|
};
|
|
3160
3235
|
}
|
|
3161
|
-
if (!identity && userRow && (!input.autoLinkByVerifiedEmail || !input.emailVerified)) {
|
|
3162
|
-
await this.consumeOidcAttempt(input, completedAt, projectId, tx);
|
|
3163
|
-
return {
|
|
3164
|
-
error: new AuthStorageError11("email_link_not_allowed", `[Sixb] OIDC identity cannot auto-link to user '${userRow.id}' for project '${projectId}'.`)
|
|
3165
|
-
};
|
|
3166
|
-
}
|
|
3167
3236
|
if (userRow?.status === "suspended") {
|
|
3168
3237
|
await this.consumeOidcAttempt(input, completedAt, projectId, tx);
|
|
3169
3238
|
return {
|
|
3170
3239
|
error: new AuthStorageError11("suspended_user", `[Sixb] User '${userRow.id}' is suspended for project '${projectId}'.`)
|
|
3171
3240
|
};
|
|
3172
3241
|
}
|
|
3173
|
-
if (shouldCreateUser && !input.emailVerified) {
|
|
3174
|
-
await this.consumeOidcAttempt(input, completedAt, projectId, tx);
|
|
3175
|
-
return {
|
|
3176
|
-
error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
|
|
3177
|
-
};
|
|
3178
|
-
}
|
|
3179
3242
|
if (shouldCreateUser && !activeInvitation && !input.allowUserCreationWithoutInvitation) {
|
|
3180
3243
|
await this.consumeOidcAttempt(input, completedAt, projectId, tx);
|
|
3181
3244
|
return {
|
|
3182
3245
|
error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
|
|
3183
3246
|
};
|
|
3184
3247
|
}
|
|
3185
|
-
if (shouldCreateUser && !activeInvitation && input.allowUserCreationWithoutInvitation && input.requireNoActiveUsersForUserCreation && await hasActiveUsers(tx, projectId)) {
|
|
3186
|
-
await this.consumeOidcAttempt(input, completedAt, projectId, tx);
|
|
3187
|
-
return {
|
|
3188
|
-
error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
|
|
3189
|
-
};
|
|
3190
|
-
}
|
|
3191
3248
|
await validateCompleteSessionInput(tx, projectId, input.session);
|
|
3192
3249
|
assertSignInSessionAudience(projectId, input.session.audience, attempt.audience);
|
|
3193
3250
|
let newUserId;
|
|
@@ -3230,10 +3287,17 @@ class PgAuthStorage {
|
|
|
3230
3287
|
projectId,
|
|
3231
3288
|
user
|
|
3232
3289
|
});
|
|
3233
|
-
const groupMemberships = await this.
|
|
3290
|
+
const groupMemberships = await this.syncDirectoryGroups(tx, {
|
|
3234
3291
|
completedAt,
|
|
3235
|
-
existing:
|
|
3236
|
-
|
|
3292
|
+
existing: await this.applyGroups(tx, {
|
|
3293
|
+
completedAt,
|
|
3294
|
+
source: "manual",
|
|
3295
|
+
existing: invitation.groupMemberships,
|
|
3296
|
+
groupIds: manualGroupIds,
|
|
3297
|
+
projectId,
|
|
3298
|
+
userId: user.id
|
|
3299
|
+
}),
|
|
3300
|
+
groupIds: input.directoryGroupIds,
|
|
3237
3301
|
projectId,
|
|
3238
3302
|
userId: user.id
|
|
3239
3303
|
});
|
|
@@ -3362,19 +3426,33 @@ class PgAuthStorage {
|
|
|
3362
3426
|
}
|
|
3363
3427
|
return { invitation, groupMemberships };
|
|
3364
3428
|
}
|
|
3365
|
-
async
|
|
3429
|
+
async applyGroups(sql, input) {
|
|
3366
3430
|
const groupMemberships = [...input.existing];
|
|
3367
3431
|
for (const groupId of input.groupIds) {
|
|
3368
3432
|
groupMemberships.push(await upsertGroupMembership(sql, {
|
|
3369
3433
|
projectId: input.projectId,
|
|
3370
3434
|
userId: input.userId,
|
|
3371
3435
|
groupId,
|
|
3372
|
-
source:
|
|
3436
|
+
source: input.source,
|
|
3373
3437
|
createdAt: input.completedAt
|
|
3374
3438
|
}));
|
|
3375
3439
|
}
|
|
3376
3440
|
return groupMemberships;
|
|
3377
3441
|
}
|
|
3442
|
+
async syncDirectoryGroups(sql, input) {
|
|
3443
|
+
if (!input.groupIds) {
|
|
3444
|
+
return input.existing;
|
|
3445
|
+
}
|
|
3446
|
+
const groupIds = normalizeGroupIds(input.groupIds);
|
|
3447
|
+
await sql`
|
|
3448
|
+
DELETE FROM auth_group_memberships
|
|
3449
|
+
WHERE project_id = ${input.projectId}
|
|
3450
|
+
AND user_id = ${input.userId}
|
|
3451
|
+
AND source = 'directory'
|
|
3452
|
+
${groupIds.length > 0 ? sql`AND group_id NOT IN ${sql(groupIds)}` : sql``}
|
|
3453
|
+
`;
|
|
3454
|
+
return this.applyGroups(sql, { ...input, groupIds, source: "directory" });
|
|
3455
|
+
}
|
|
3378
3456
|
async createSignInSession(sql, input) {
|
|
3379
3457
|
await lockAdvisoryKeys(sql, [
|
|
3380
3458
|
authLockKey("sessions", input.projectId, input.userId),
|
|
@@ -3431,16 +3509,6 @@ class PgAuthStorage {
|
|
|
3431
3509
|
return rowToIdentityRecord(row);
|
|
3432
3510
|
}
|
|
3433
3511
|
}
|
|
3434
|
-
async function hasActiveUsers(sql, projectId) {
|
|
3435
|
-
const rows = await sql`
|
|
3436
|
-
SELECT 1 AS active
|
|
3437
|
-
FROM auth_users
|
|
3438
|
-
WHERE project_id = ${projectId}
|
|
3439
|
-
AND status = 'active'
|
|
3440
|
-
LIMIT 1
|
|
3441
|
-
`;
|
|
3442
|
-
return rows.length > 0;
|
|
3443
|
-
}
|
|
3444
3512
|
function assertSignInSessionAudience(projectId, sessionAudience, storedAudience) {
|
|
3445
3513
|
if (sessionAudience === storedAudience) {
|
|
3446
3514
|
return;
|
|
@@ -7388,6 +7456,181 @@ ALTER TABLE auth_device_authorizations
|
|
|
7388
7456
|
DROP COLUMN token_expires_at;
|
|
7389
7457
|
`;
|
|
7390
7458
|
|
|
7459
|
+
// src/migrations/050-directory-group-memberships.sql
|
|
7460
|
+
var _050_directory_group_memberships_default = `-- A membership the identity provider owns is recorded with source 'directory' and synced to the
|
|
7461
|
+
-- provider's groups on every sign-in. Service accounts never sign in, so their table is unchanged.
|
|
7462
|
+
ALTER TABLE auth_group_memberships DROP CONSTRAINT auth_group_memberships_source_check;
|
|
7463
|
+
ALTER TABLE auth_group_memberships ADD CONSTRAINT auth_group_memberships_source_check
|
|
7464
|
+
CHECK (source IN ('invitation', 'manual', 'agent', 'directory'));
|
|
7465
|
+
`;
|
|
7466
|
+
|
|
7467
|
+
// src/migrations/051-projection-run-supersession.sql
|
|
7468
|
+
var _051_projection_run_supersession_default = `-- Projection runs gain the terminal status \`superseded\`, and a running run may carry the failure
|
|
7469
|
+
-- of its last attempt in \`error\` while the queue retries it.
|
|
7470
|
+
|
|
7471
|
+
-- The status checks were unnamed. Drop exactly the two that enumerate the terminal statuses, then
|
|
7472
|
+
-- replace them with named ones that admit \`superseded\`.
|
|
7473
|
+
DO $$
|
|
7474
|
+
DECLARE status_constraint RECORD;
|
|
7475
|
+
BEGIN
|
|
7476
|
+
FOR status_constraint IN
|
|
7477
|
+
SELECT conname
|
|
7478
|
+
FROM pg_constraint
|
|
7479
|
+
WHERE conrelid = 'projection_runs'::regclass
|
|
7480
|
+
AND contype = 'c'
|
|
7481
|
+
AND pg_get_constraintdef(oid) LIKE '%''cancelled''%'
|
|
7482
|
+
LOOP
|
|
7483
|
+
EXECUTE format('ALTER TABLE projection_runs DROP CONSTRAINT %I', status_constraint.conname);
|
|
7484
|
+
END LOOP;
|
|
7485
|
+
END $$;
|
|
7486
|
+
|
|
7487
|
+
ALTER TABLE projection_runs
|
|
7488
|
+
ADD CONSTRAINT projection_runs_status_check CHECK (
|
|
7489
|
+
status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled', 'superseded')
|
|
7490
|
+
),
|
|
7491
|
+
ADD CONSTRAINT projection_runs_lifecycle_check CHECK (
|
|
7492
|
+
(status = 'queued' AND attempt = 0 AND started_at IS NULL AND finished_at IS NULL
|
|
7493
|
+
AND error IS NULL)
|
|
7494
|
+
OR (status = 'running' AND attempt >= 1 AND started_at IS NOT NULL AND finished_at IS NULL)
|
|
7495
|
+
OR (
|
|
7496
|
+
status IN ('succeeded', 'failed', 'cancelled', 'superseded')
|
|
7497
|
+
AND finished_at IS NOT NULL
|
|
7498
|
+
AND (
|
|
7499
|
+
(error->>'code' = 'queue.enqueue_failed'
|
|
7500
|
+
AND status = 'failed' AND attempt = 0 AND started_at IS NULL)
|
|
7501
|
+
OR (COALESCE(error->>'code', '') != 'queue.enqueue_failed'
|
|
7502
|
+
AND attempt >= 1 AND started_at IS NOT NULL)
|
|
7503
|
+
)
|
|
7504
|
+
)
|
|
7505
|
+
),
|
|
7506
|
+
-- A run is superseded instead of materialized, so it carries no failure, and only replacement
|
|
7507
|
+
-- runs can be: telemetry appends readings rather than replace a source.
|
|
7508
|
+
ADD CONSTRAINT projection_runs_superseded_check CHECK (
|
|
7509
|
+
status != 'superseded' OR (error IS NULL AND materialization_protocol = 'replacement')
|
|
7510
|
+
);
|
|
7511
|
+
`;
|
|
7512
|
+
|
|
7513
|
+
// src/migrations/052-compact-source-storage.sql
|
|
7514
|
+
var _052_compact_source_storage_default = `-- Source versions get a compact surrogate key; roots and rows stop repeating the text triple.
|
|
7515
|
+
ALTER TABLE ontology_sources ADD COLUMN version_id BIGINT GENERATED ALWAYS AS IDENTITY;
|
|
7516
|
+
ALTER TABLE ontology_sources ADD CONSTRAINT ontology_sources_version_id_key UNIQUE (version_id);
|
|
7517
|
+
|
|
7518
|
+
ALTER TABLE ontology_source_rows RENAME TO legacy_ontology_source_rows;
|
|
7519
|
+
ALTER TABLE ontology_source_roots RENAME TO legacy_ontology_source_roots;
|
|
7520
|
+
-- Index names are schema-wide: free them for the new tables.
|
|
7521
|
+
DO $$
|
|
7522
|
+
DECLARE
|
|
7523
|
+
legacy record;
|
|
7524
|
+
BEGIN
|
|
7525
|
+
FOR legacy IN
|
|
7526
|
+
SELECT index_class.relname AS name
|
|
7527
|
+
FROM pg_index
|
|
7528
|
+
JOIN pg_class AS index_class ON index_class.oid = pg_index.indexrelid
|
|
7529
|
+
JOIN pg_class AS table_class ON table_class.oid = pg_index.indrelid
|
|
7530
|
+
WHERE table_class.relnamespace = current_schema()::regnamespace
|
|
7531
|
+
AND table_class.relname IN ('legacy_ontology_source_rows', 'legacy_ontology_source_roots')
|
|
7532
|
+
LOOP
|
|
7533
|
+
EXECUTE format('ALTER INDEX %I RENAME TO %I', legacy.name, left('legacy_' || legacy.name, 63));
|
|
7534
|
+
END LOOP;
|
|
7535
|
+
END $$;
|
|
7536
|
+
|
|
7537
|
+
-- A root is live while its version is published (active or superseded), it is not retired and
|
|
7538
|
+
-- it is not a deletion. Activation therefore writes only the roots it replaces, never its own.
|
|
7539
|
+
-- \`root_key\` is the canonical key; "C" orders it by its UTF-8 bytes.
|
|
7540
|
+
CREATE TABLE ontology_source_roots (
|
|
7541
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
7542
|
+
version_id BIGINT NOT NULL REFERENCES ontology_sources (version_id) ON DELETE RESTRICT,
|
|
7543
|
+
project_id TEXT NOT NULL,
|
|
7544
|
+
root_key TEXT COLLATE "C" NOT NULL,
|
|
7545
|
+
staging_ordinal BIGINT NOT NULL CHECK (staging_ordinal >= 0),
|
|
7546
|
+
deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
|
7547
|
+
retired_at TIMESTAMPTZ,
|
|
7548
|
+
CONSTRAINT ontology_source_roots_key UNIQUE (version_id, root_key),
|
|
7549
|
+
CONSTRAINT ontology_source_roots_ordinal UNIQUE (version_id, staging_ordinal)
|
|
7550
|
+
);
|
|
7551
|
+
CREATE INDEX idx_ontology_source_roots_live
|
|
7552
|
+
ON ontology_source_roots (project_id, root_key) WHERE retired_at IS NULL AND NOT deleted;
|
|
7553
|
+
CREATE INDEX idx_ontology_source_roots_retired
|
|
7554
|
+
ON ontology_source_roots (project_id, retired_at) WHERE retired_at IS NOT NULL;
|
|
7555
|
+
|
|
7556
|
+
-- A row's identity lives in its typed columns; \`payload\` is the assertion without kind and ref.
|
|
7557
|
+
CREATE TABLE ontology_source_rows (
|
|
7558
|
+
root_id BIGINT NOT NULL REFERENCES ontology_source_roots (id) ON DELETE RESTRICT,
|
|
7559
|
+
entity_kind TEXT NOT NULL CHECK (entity_kind IN ('object', 'link')),
|
|
7560
|
+
object_type_id TEXT,
|
|
7561
|
+
primary_id TEXT,
|
|
7562
|
+
source_type_id TEXT,
|
|
7563
|
+
source_primary_id TEXT,
|
|
7564
|
+
link_id TEXT,
|
|
7565
|
+
target_type_id TEXT,
|
|
7566
|
+
target_primary_id TEXT,
|
|
7567
|
+
payload JSONB,
|
|
7568
|
+
CHECK (
|
|
7569
|
+
(
|
|
7570
|
+
entity_kind = 'object'
|
|
7571
|
+
AND object_type_id IS NOT NULL AND primary_id IS NOT NULL
|
|
7572
|
+
AND source_type_id IS NULL AND source_primary_id IS NULL AND link_id IS NULL
|
|
7573
|
+
AND target_type_id IS NULL AND target_primary_id IS NULL
|
|
7574
|
+
)
|
|
7575
|
+
OR
|
|
7576
|
+
(
|
|
7577
|
+
entity_kind = 'link'
|
|
7578
|
+
AND object_type_id IS NULL AND primary_id IS NULL
|
|
7579
|
+
AND source_type_id IS NOT NULL AND source_primary_id IS NOT NULL AND link_id IS NOT NULL
|
|
7580
|
+
AND target_type_id IS NOT NULL AND target_primary_id IS NOT NULL
|
|
7581
|
+
)
|
|
7582
|
+
)
|
|
7583
|
+
);
|
|
7584
|
+
CREATE INDEX idx_ontology_source_rows_root ON ontology_source_rows (root_id);
|
|
7585
|
+
CREATE INDEX idx_ontology_source_rows_link_source
|
|
7586
|
+
ON ontology_source_rows (source_type_id, source_primary_id, link_id)
|
|
7587
|
+
WHERE entity_kind = 'link';
|
|
7588
|
+
CREATE INDEX idx_ontology_source_rows_link_target
|
|
7589
|
+
ON ontology_source_rows (target_type_id, target_primary_id)
|
|
7590
|
+
WHERE entity_kind = 'link';
|
|
7591
|
+
|
|
7592
|
+
-- A published root that was not active is retired, so liveness stays exactly what it was.
|
|
7593
|
+
INSERT INTO ontology_source_roots (
|
|
7594
|
+
version_id, project_id, root_key, staging_ordinal, deleted, retired_at
|
|
7595
|
+
)
|
|
7596
|
+
SELECT versions.version_id, roots.project_id,
|
|
7597
|
+
convert_from(decode(roots.root_sort_key, 'hex'), 'UTF8'), roots.staging_ordinal, roots.deleted,
|
|
7598
|
+
CASE
|
|
7599
|
+
WHEN roots.active THEN NULL
|
|
7600
|
+
WHEN roots.retired_at IS NOT NULL THEN roots.retired_at
|
|
7601
|
+
WHEN versions.status IN ('active', 'superseded') THEN versions.updated_at
|
|
7602
|
+
END
|
|
7603
|
+
FROM legacy_ontology_source_roots AS roots
|
|
7604
|
+
JOIN ontology_sources AS versions USING (project_id, source_id, materialization_id);
|
|
7605
|
+
|
|
7606
|
+
INSERT INTO ontology_source_rows (
|
|
7607
|
+
root_id, entity_kind, object_type_id, primary_id,
|
|
7608
|
+
source_type_id, source_primary_id, link_id, target_type_id, target_primary_id, payload
|
|
7609
|
+
)
|
|
7610
|
+
SELECT roots.id, rows.entity_kind, rows.object_type_id, rows.primary_id,
|
|
7611
|
+
rows.source_type_id, rows.source_primary_id, rows.link_id, rows.target_type_id,
|
|
7612
|
+
rows.target_primary_id, NULLIF(rows.assertion - 'kind' - 'ref', '{}'::jsonb)
|
|
7613
|
+
FROM legacy_ontology_source_rows AS rows
|
|
7614
|
+
JOIN ontology_sources AS versions USING (project_id, source_id, materialization_id)
|
|
7615
|
+
JOIN ontology_source_roots AS roots
|
|
7616
|
+
ON roots.version_id = versions.version_id
|
|
7617
|
+
AND roots.root_key = convert_from(decode(rows.root_sort_key, 'hex'), 'UTF8');
|
|
7618
|
+
|
|
7619
|
+
DO $$
|
|
7620
|
+
BEGIN
|
|
7621
|
+
IF (SELECT count(*) FROM legacy_ontology_source_roots)
|
|
7622
|
+
<> (SELECT count(*) FROM ontology_source_roots)
|
|
7623
|
+
OR (SELECT count(*) FROM legacy_ontology_source_rows)
|
|
7624
|
+
<> (SELECT count(*) FROM ontology_source_rows) THEN
|
|
7625
|
+
RAISE EXCEPTION 'Source roots or rows were lost while compacting source storage.';
|
|
7626
|
+
END IF;
|
|
7627
|
+
END $$;
|
|
7628
|
+
|
|
7629
|
+
DROP TABLE legacy_ontology_source_rows;
|
|
7630
|
+
DROP TABLE legacy_ontology_source_roots;
|
|
7631
|
+
ANALYZE ontology_sources, ontology_source_roots, ontology_source_rows;
|
|
7632
|
+
`;
|
|
7633
|
+
|
|
7391
7634
|
// src/migrations/037-execution-requester-groups.sql
|
|
7392
7635
|
var _037_execution_requester_groups_default = `ALTER TABLE executions ADD COLUMN requester_group_ids JSONB NOT NULL DEFAULT '[]'::jsonb;
|
|
7393
7636
|
|
|
@@ -7573,15 +7816,11 @@ function postgresMigrationSession(sql, schemaName) {
|
|
|
7573
7816
|
async transaction(run) {
|
|
7574
7817
|
const previous = active;
|
|
7575
7818
|
active = sql;
|
|
7576
|
-
await sql.unsafe("BEGIN");
|
|
7577
7819
|
try {
|
|
7578
|
-
await sql
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
} catch (error) {
|
|
7583
|
-
await sql.unsafe("ROLLBACK");
|
|
7584
|
-
throw error;
|
|
7820
|
+
return await runPgTransactionOn(sql, async () => {
|
|
7821
|
+
await sql.unsafe(`SET LOCAL search_path TO ${schema}`);
|
|
7822
|
+
return run();
|
|
7823
|
+
});
|
|
7585
7824
|
} finally {
|
|
7586
7825
|
active = previous;
|
|
7587
7826
|
}
|
|
@@ -7601,18 +7840,14 @@ async function readPostgresHistory(sql, schemaName, adapterId) {
|
|
|
7601
7840
|
return rows.map(rowToMigrationRecord);
|
|
7602
7841
|
}
|
|
7603
7842
|
async function withPostgresMigrationLock(params, run) {
|
|
7604
|
-
const sql = await params.sql.reserve();
|
|
7605
7843
|
const [first, second] = advisoryLockParts2(`storage:migration:${params.schemaName}:${params.migrations.adapterId}`);
|
|
7606
|
-
|
|
7844
|
+
return withReservedPgConnection(params.sql, async (sql) => {
|
|
7845
|
+
const unlock = () => sql`SELECT pg_advisory_unlock(${first}, ${second})`;
|
|
7607
7846
|
await sql`SELECT pg_advisory_lock(${first}, ${second})`;
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
}
|
|
7613
|
-
} finally {
|
|
7614
|
-
sql.release();
|
|
7615
|
-
}
|
|
7847
|
+
const result = await undoOnFailure(() => run(sql), unlock);
|
|
7848
|
+
await unlock();
|
|
7849
|
+
return result;
|
|
7850
|
+
});
|
|
7616
7851
|
}
|
|
7617
7852
|
function advisoryLockParts2(key) {
|
|
7618
7853
|
const hash = createHash2("sha256").update(key).digest();
|
|
@@ -7686,7 +7921,10 @@ var postgresStorageMigrations = defineMigrations({
|
|
|
7686
7921
|
pgSql("046-workflow-intervention-principals", _046_workflow_intervention_principals_default),
|
|
7687
7922
|
pgSql("047-ontology-commit-attribution", _047_ontology_commit_attribution_default),
|
|
7688
7923
|
pgSql("048-file-upload-sessions", _048_file_upload_sessions_default),
|
|
7689
|
-
pgSql("049-native-sessions", _049_native_sessions_default)
|
|
7924
|
+
pgSql("049-native-sessions", _049_native_sessions_default),
|
|
7925
|
+
pgSql("050-directory-group-memberships", _050_directory_group_memberships_default),
|
|
7926
|
+
pgSql("051-projection-run-supersession", _051_projection_run_supersession_default),
|
|
7927
|
+
pgSql("052-compact-source-storage", _052_compact_source_storage_default)
|
|
7690
7928
|
]
|
|
7691
7929
|
});
|
|
7692
7930
|
|
|
@@ -9936,6 +10174,10 @@ function assertReconciliationPageLimit(limit) {
|
|
|
9936
10174
|
// src/ontology-storage/shared.ts
|
|
9937
10175
|
import { parseSixbFailure as parseSixbFailure2 } from "@sixb/core/internal/errors";
|
|
9938
10176
|
import { MaterializationConflictError } from "@sixb/core/internal/materialization";
|
|
10177
|
+
import {
|
|
10178
|
+
sourceAssertionFromColumns,
|
|
10179
|
+
sourceEntityFromKey
|
|
10180
|
+
} from "@sixb/core/internal/ontology-storage-provider";
|
|
9939
10181
|
import { ONTOLOGY_OUTBOX_FAILURE_CODES } from "@sixb/core/storage";
|
|
9940
10182
|
import {
|
|
9941
10183
|
assertNonblank,
|
|
@@ -10001,8 +10243,8 @@ function sourceRecord(row) {
|
|
|
10001
10243
|
}
|
|
10002
10244
|
function sourceAssertion(row) {
|
|
10003
10245
|
return {
|
|
10004
|
-
root:
|
|
10005
|
-
assertion:
|
|
10246
|
+
root: sourceEntityFromKey(row.root_key),
|
|
10247
|
+
assertion: sourceAssertionFromColumns(row, row.payload),
|
|
10006
10248
|
stagingOrdinal: databaseSafeInteger(row.staging_ordinal, "Source staging ordinal")
|
|
10007
10249
|
};
|
|
10008
10250
|
}
|
|
@@ -10214,27 +10456,80 @@ import {
|
|
|
10214
10456
|
MaterializationValidationError,
|
|
10215
10457
|
projectionEntityKey
|
|
10216
10458
|
} from "@sixb/core/internal/materialization";
|
|
10217
|
-
import {
|
|
10459
|
+
import {
|
|
10460
|
+
sourceEntityColumns,
|
|
10461
|
+
sourceStageRoots
|
|
10462
|
+
} from "@sixb/core/internal/ontology-storage-provider";
|
|
10463
|
+
var PUBLISHED = "('active', 'superseded')";
|
|
10464
|
+
function sourceAssertionColumns(sql) {
|
|
10465
|
+
return sql.unsafe(`versions.source_id, versions.materialization_id, roots.root_key,
|
|
10466
|
+
roots.staging_ordinal, rows.entity_kind, rows.object_type_id, rows.primary_id,
|
|
10467
|
+
rows.source_type_id, rows.source_primary_id, rows.link_id, rows.target_type_id,
|
|
10468
|
+
rows.target_primary_id, rows.payload`);
|
|
10469
|
+
}
|
|
10470
|
+
function liveSourceRowsJoin(sql, projectId) {
|
|
10471
|
+
return sql`
|
|
10472
|
+
JOIN ontology_source_roots AS roots ON roots.id = rows.root_id
|
|
10473
|
+
AND roots.project_id = ${projectId} AND roots.retired_at IS NULL AND NOT roots.deleted
|
|
10474
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
10475
|
+
AND versions.status IN ${sql.unsafe(PUBLISHED)}
|
|
10476
|
+
`;
|
|
10477
|
+
}
|
|
10478
|
+
function candidateVersion(sql, input) {
|
|
10479
|
+
return sql`(SELECT version_id FROM ontology_sources WHERE project_id = ${input.projectId}
|
|
10480
|
+
AND source_id = ${input.sourceId} AND materialization_id = ${input.materializationId})`;
|
|
10481
|
+
}
|
|
10482
|
+
var ENTITY_MATCH = `rows.entity_kind = requested.entity_kind
|
|
10483
|
+
AND rows.object_type_id IS NOT DISTINCT FROM requested.object_type_id
|
|
10484
|
+
AND rows.primary_id IS NOT DISTINCT FROM requested.primary_id
|
|
10485
|
+
AND rows.source_type_id IS NOT DISTINCT FROM requested.source_type_id
|
|
10486
|
+
AND rows.source_primary_id IS NOT DISTINCT FROM requested.source_primary_id
|
|
10487
|
+
AND rows.link_id IS NOT DISTINCT FROM requested.link_id
|
|
10488
|
+
AND rows.target_type_id IS NOT DISTINCT FROM requested.target_type_id
|
|
10489
|
+
AND rows.target_primary_id IS NOT DISTINCT FROM requested.target_primary_id`;
|
|
10490
|
+
var REQUESTED_ENTITY = `entity_kind TEXT, object_type_id TEXT, primary_id TEXT,
|
|
10491
|
+
source_type_id TEXT, source_primary_id TEXT, link_id TEXT, target_type_id TEXT,
|
|
10492
|
+
target_primary_id TEXT`;
|
|
10493
|
+
function requestedEntity(ref) {
|
|
10494
|
+
const columns = sourceEntityColumns(ref);
|
|
10495
|
+
const key = projectionEntityKey(ref);
|
|
10496
|
+
return {
|
|
10497
|
+
entity_kind: ref.kind,
|
|
10498
|
+
object_type_id: columns.objectTypeId,
|
|
10499
|
+
primary_id: columns.primaryId,
|
|
10500
|
+
source_type_id: columns.sourceTypeId,
|
|
10501
|
+
source_primary_id: columns.sourcePrimaryId,
|
|
10502
|
+
link_id: columns.linkId,
|
|
10503
|
+
target_type_id: columns.targetTypeId,
|
|
10504
|
+
target_primary_id: columns.targetPrimaryId,
|
|
10505
|
+
root_keys: ref.kind === "object" ? [key] : [key, projectionEntityKey({ kind: "object", ref: ref.ref.source })]
|
|
10506
|
+
};
|
|
10507
|
+
}
|
|
10218
10508
|
function replacementSourceRows(sql, input) {
|
|
10509
|
+
const live = sql`roots.retired_at IS NULL AND NOT roots.deleted
|
|
10510
|
+
AND versions.source_id = ${input.sourceId} AND versions.status IN ${sql.unsafe(PUBLISHED)}`;
|
|
10219
10511
|
const roots = input.incremental ? sql`
|
|
10220
|
-
SELECT
|
|
10512
|
+
SELECT live.id FROM ontology_source_roots AS changed
|
|
10221
10513
|
CROSS JOIN LATERAL (
|
|
10222
|
-
SELECT roots
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
` : sql`
|
|
10514
|
+
SELECT roots.id FROM ontology_source_roots AS roots
|
|
10515
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
10516
|
+
WHERE roots.project_id = ${input.projectId} AND roots.root_key = changed.root_key AND ${live}
|
|
10517
|
+
OFFSET 0
|
|
10518
|
+
) AS live
|
|
10519
|
+
WHERE changed.version_id = ${candidateVersion(sql, input)}
|
|
10520
|
+
` : sql`
|
|
10521
|
+
SELECT roots.id FROM ontology_sources AS versions
|
|
10522
|
+
JOIN ontology_source_roots AS roots ON roots.version_id = versions.version_id
|
|
10523
|
+
WHERE versions.project_id = ${input.projectId} AND ${live}
|
|
10524
|
+
`;
|
|
10229
10525
|
return sql`
|
|
10230
|
-
SELECT
|
|
10231
|
-
|
|
10526
|
+
SELECT rows.* FROM ontology_source_roots AS roots
|
|
10527
|
+
JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
10528
|
+
WHERE roots.version_id = ${candidateVersion(sql, input)}
|
|
10232
10529
|
UNION ALL
|
|
10233
10530
|
SELECT rows.* FROM (${roots}) AS roots
|
|
10234
10531
|
CROSS JOIN LATERAL (
|
|
10235
|
-
SELECT rows.* FROM ontology_source_rows AS rows WHERE rows.
|
|
10236
|
-
AND rows.source_id = roots.source_id AND rows.materialization_id = roots.materialization_id
|
|
10237
|
-
AND rows.root_sort_key = roots.root_sort_key OFFSET 0
|
|
10532
|
+
SELECT rows.* FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id OFFSET 0
|
|
10238
10533
|
) AS rows
|
|
10239
10534
|
`;
|
|
10240
10535
|
}
|
|
@@ -10246,35 +10541,28 @@ async function stageSourceRoots(sql, manifest, input) {
|
|
|
10246
10541
|
throw new MaterializationValidationError("Root deletions require a source delta base.");
|
|
10247
10542
|
}
|
|
10248
10543
|
const values = jsonParameter2(sql, roots.map((root) => ({
|
|
10249
|
-
|
|
10250
|
-
root_kind: root.root.kind,
|
|
10251
|
-
root_key: JSON.parse(root.rootKey),
|
|
10252
|
-
root: root.root,
|
|
10544
|
+
root_key: root.rootKey,
|
|
10253
10545
|
staging_ordinal: root.stagingOrdinal,
|
|
10254
10546
|
deleted: root.deleted
|
|
10255
10547
|
})));
|
|
10256
10548
|
const inserted = await sql`
|
|
10257
|
-
INSERT INTO ontology_source_roots (
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
FROM jsonb_to_recordset(${values}) AS input(
|
|
10262
|
-
root_sort_key TEXT, root_kind TEXT, root_key JSONB, root JSONB, staging_ordinal BIGINT, deleted BOOLEAN)
|
|
10263
|
-
ON CONFLICT DO NOTHING RETURNING root_sort_key
|
|
10549
|
+
INSERT INTO ontology_source_roots (version_id, project_id, root_key, staging_ordinal, deleted)
|
|
10550
|
+
SELECT ${manifest.version_id}, ${input.projectId}, root_key, staging_ordinal, deleted
|
|
10551
|
+
FROM jsonb_to_recordset(${values}) AS input(root_key TEXT, staging_ordinal BIGINT, deleted BOOLEAN)
|
|
10552
|
+
ON CONFLICT DO NOTHING RETURNING root_key
|
|
10264
10553
|
`;
|
|
10265
10554
|
if (inserted.length === roots.length)
|
|
10266
10555
|
return;
|
|
10267
|
-
const insertedKeys = new Set(inserted.map((row) => row.
|
|
10268
|
-
const skipped = roots.filter((root) => !insertedKeys.has(root.
|
|
10556
|
+
const insertedKeys = new Set(inserted.map((row) => row.root_key));
|
|
10557
|
+
const skipped = roots.filter((root) => !insertedKeys.has(root.rootKey));
|
|
10269
10558
|
const stored = await sql`
|
|
10270
|
-
SELECT
|
|
10271
|
-
WHERE
|
|
10272
|
-
AND
|
|
10273
|
-
AND root_sort_key = ANY(${sql.array(skipped.map((root) => root.sortKey))}::text[])
|
|
10559
|
+
SELECT root_key, staging_ordinal, deleted FROM ontology_source_roots
|
|
10560
|
+
WHERE version_id = ${manifest.version_id}
|
|
10561
|
+
AND root_key = ANY(${sql.array(skipped.map((root) => root.rootKey))}::text[])
|
|
10274
10562
|
`;
|
|
10275
|
-
const existing = new Map(stored.map((root) => [root.
|
|
10563
|
+
const existing = new Map(stored.map((root) => [root.root_key, root]));
|
|
10276
10564
|
if (skipped.some((root) => {
|
|
10277
|
-
const previous = existing.get(root.
|
|
10565
|
+
const previous = existing.get(root.rootKey);
|
|
10278
10566
|
return !previous || Number(previous.staging_ordinal) !== root.stagingOrdinal || previous.deleted !== root.deleted;
|
|
10279
10567
|
}))
|
|
10280
10568
|
throw new MaterializationValidationError("Source materialization repeats root or stream ordinal with different content.");
|
|
@@ -10284,94 +10572,84 @@ async function activateSourceRoots(sql, projectId, candidate, activation) {
|
|
|
10284
10572
|
throw new MaterializationConflictError2("projection-fence", "Source delta base changed before activation.");
|
|
10285
10573
|
if (candidate.base_materialization_id !== null && Number(candidate.root_count) === 0)
|
|
10286
10574
|
return;
|
|
10287
|
-
const
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
)
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
|
|
10575
|
+
const live = sql`roots.retired_at IS NULL AND NOT roots.deleted
|
|
10576
|
+
AND versions.version_id = roots.version_id AND versions.project_id = ${projectId}
|
|
10577
|
+
AND versions.source_id = ${activation.source.projectionId}
|
|
10578
|
+
AND versions.status IN ${sql.unsafe(PUBLISHED)}`;
|
|
10579
|
+
if (candidate.base_materialization_id === null) {
|
|
10580
|
+
await sql`
|
|
10581
|
+
UPDATE ontology_source_roots AS roots SET retired_at = ${activation.updatedAt}
|
|
10582
|
+
FROM ontology_sources AS versions WHERE ${live}
|
|
10583
|
+
`;
|
|
10584
|
+
} else {
|
|
10585
|
+
await sql`
|
|
10586
|
+
UPDATE ontology_source_roots AS roots SET retired_at = ${activation.updatedAt}
|
|
10587
|
+
FROM ontology_source_roots AS changed, ontology_sources AS versions
|
|
10588
|
+
WHERE changed.version_id = ${candidate.version_id} AND roots.project_id = ${projectId}
|
|
10589
|
+
AND roots.root_key = changed.root_key AND ${live}
|
|
10590
|
+
`;
|
|
10591
|
+
}
|
|
10297
10592
|
await sql`
|
|
10298
|
-
UPDATE ontology_source_roots SET
|
|
10299
|
-
|
|
10300
|
-
WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId}
|
|
10301
|
-
AND materialization_id = ${activation.materializationId}
|
|
10593
|
+
UPDATE ontology_source_roots SET retired_at = ${activation.updatedAt}
|
|
10594
|
+
WHERE version_id = ${candidate.version_id} AND deleted
|
|
10302
10595
|
`;
|
|
10303
10596
|
}
|
|
10304
10597
|
async function assertSourceRootCoverage(sql, manifest) {
|
|
10305
10598
|
const [invalid] = await sql`
|
|
10306
10599
|
SELECT 1 FROM ontology_source_roots AS roots
|
|
10307
|
-
WHERE
|
|
10308
|
-
AND materialization_id = ${manifest.materialization_id}
|
|
10600
|
+
WHERE roots.version_id = ${manifest.version_id}
|
|
10309
10601
|
AND roots.deleted = EXISTS (
|
|
10310
|
-
SELECT 1 FROM ontology_source_rows AS rows
|
|
10311
|
-
|
|
10312
|
-
|
|
10313
|
-
) LIMIT 1
|
|
10602
|
+
SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id
|
|
10603
|
+
)
|
|
10604
|
+
LIMIT 1
|
|
10314
10605
|
`;
|
|
10315
10606
|
if (invalid)
|
|
10316
10607
|
throw new MaterializationValidationError("Source roots must have complete assertions or an explicit deletion, never both.");
|
|
10317
10608
|
}
|
|
10318
10609
|
function activeSourceRows(sql, projectId, refs) {
|
|
10319
|
-
const
|
|
10320
|
-
const
|
|
10321
|
-
|
|
10322
|
-
entitySortKey,
|
|
10323
|
-
utf8SortKey(projectionEntityKey({ kind: "object", ref: entity.ref.source }))
|
|
10324
|
-
];
|
|
10325
|
-
return roots.map((rootSortKey) => ({
|
|
10326
|
-
root_sort_key: rootSortKey,
|
|
10327
|
-
entity_sort_key: entitySortKey
|
|
10328
|
-
}));
|
|
10610
|
+
const requested = refs.flatMap((ref) => {
|
|
10611
|
+
const { root_keys, ...entity } = requestedEntity(ref);
|
|
10612
|
+
return root_keys.map((root_key) => ({ ...entity, root_key }));
|
|
10329
10613
|
});
|
|
10330
10614
|
return sql`
|
|
10331
|
-
SELECT
|
|
10615
|
+
SELECT ${sourceAssertionColumns(sql)}
|
|
10616
|
+
FROM jsonb_to_recordset(${jsonParameter2(sql, requested)})
|
|
10617
|
+
AS requested(root_key TEXT, ${sql.unsafe(REQUESTED_ENTITY)})
|
|
10332
10618
|
JOIN ontology_source_roots AS roots ON roots.project_id = ${projectId}
|
|
10333
|
-
AND roots.
|
|
10334
|
-
JOIN
|
|
10335
|
-
AND
|
|
10336
|
-
|
|
10619
|
+
AND roots.root_key = requested.root_key AND roots.retired_at IS NULL AND NOT roots.deleted
|
|
10620
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
10621
|
+
AND versions.status IN ${sql.unsafe(PUBLISHED)}
|
|
10622
|
+
JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${sql.unsafe(ENTITY_MATCH)}
|
|
10337
10623
|
`;
|
|
10338
10624
|
}
|
|
10339
10625
|
function replacementAssertionRows(sql, input) {
|
|
10340
|
-
const
|
|
10341
|
-
entity_kind: ref.kind,
|
|
10342
|
-
entity_key: JSON.parse(projectionEntityKey(ref)),
|
|
10343
|
-
root_keys: ref.kind === "object" ? [utf8SortKey(projectionEntityKey(ref))] : [
|
|
10344
|
-
utf8SortKey(projectionEntityKey(ref)),
|
|
10345
|
-
utf8SortKey(projectionEntityKey({ kind: "object", ref: ref.ref.source }))
|
|
10346
|
-
]
|
|
10347
|
-
}));
|
|
10626
|
+
const version = candidateVersion(sql, input);
|
|
10348
10627
|
const previous = !input.includePrevious ? sql`` : sql`UNION ALL
|
|
10349
|
-
SELECT
|
|
10628
|
+
SELECT ${sourceAssertionColumns(sql)} FROM (
|
|
10350
10629
|
SELECT roots.* FROM ontology_source_roots AS roots
|
|
10351
|
-
|
|
10352
|
-
|
|
10630
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
10631
|
+
WHERE roots.project_id = ${input.projectId} AND roots.root_key = ANY(requested.root_keys)
|
|
10632
|
+
AND roots.retired_at IS NULL AND NOT roots.deleted
|
|
10633
|
+
AND versions.source_id = ${input.sourceId} AND versions.status IN ${sql.unsafe(PUBLISHED)}
|
|
10353
10634
|
${!input.incremental ? sql`` : sql`AND EXISTS (SELECT 1 FROM ontology_source_roots AS changed
|
|
10354
|
-
WHERE changed.
|
|
10355
|
-
AND changed.materialization_id = ${input.materializationId} AND changed.root_sort_key = roots.root_sort_key)`}
|
|
10356
|
-
|
|
10635
|
+
WHERE changed.version_id = ${version} AND changed.root_key = roots.root_key)`}
|
|
10357
10636
|
OFFSET 0
|
|
10358
10637
|
) AS roots
|
|
10638
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
10359
10639
|
CROSS JOIN LATERAL (
|
|
10360
10640
|
SELECT rows.* FROM ontology_source_rows AS rows
|
|
10361
|
-
WHERE rows.
|
|
10362
|
-
AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
|
|
10363
|
-
AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
|
|
10641
|
+
WHERE rows.root_id = roots.id AND ${sql.unsafe(ENTITY_MATCH)}
|
|
10364
10642
|
OFFSET 0
|
|
10365
10643
|
) AS rows
|
|
10366
10644
|
`;
|
|
10367
10645
|
return sql`
|
|
10368
|
-
SELECT selected.* FROM jsonb_to_recordset(${jsonParameter2(sql,
|
|
10369
|
-
AS requested(
|
|
10646
|
+
SELECT selected.* FROM jsonb_to_recordset(${jsonParameter2(sql, input.refs.map(requestedEntity))})
|
|
10647
|
+
AS requested(${sql.unsafe(REQUESTED_ENTITY)}, root_keys TEXT[])
|
|
10370
10648
|
CROSS JOIN LATERAL (
|
|
10371
|
-
SELECT
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10649
|
+
SELECT ${sourceAssertionColumns(sql)} FROM ontology_source_roots AS roots
|
|
10650
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
10651
|
+
JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${sql.unsafe(ENTITY_MATCH)}
|
|
10652
|
+
WHERE roots.version_id = ${version} AND roots.root_key = ANY(requested.root_keys)
|
|
10375
10653
|
${previous}
|
|
10376
10654
|
OFFSET 0
|
|
10377
10655
|
) AS selected
|
|
@@ -10590,20 +10868,14 @@ class PgMaterializationStateReader {
|
|
|
10590
10868
|
scope_sort_key TEXT, source_type_id TEXT, source_id TEXT, link_id TEXT
|
|
10591
10869
|
)
|
|
10592
10870
|
)
|
|
10593
|
-
SELECT
|
|
10871
|
+
SELECT ${sourceAssertionColumns(this.sql)}
|
|
10594
10872
|
FROM ontology_source_rows AS rows
|
|
10595
10873
|
JOIN requested
|
|
10596
10874
|
ON requested.source_type_id = rows.source_type_id
|
|
10597
10875
|
AND requested.source_id = rows.source_primary_id
|
|
10598
10876
|
AND requested.link_id = rows.link_id
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
AND sources.source_id = rows.source_id
|
|
10602
|
-
AND sources.materialization_id = rows.materialization_id
|
|
10603
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
10604
|
-
WHERE rows.project_id = ${this.projectId}
|
|
10605
|
-
AND rows.entity_kind = 'link'
|
|
10606
|
-
AND sources.active
|
|
10877
|
+
${liveSourceRowsJoin(this.sql, this.projectId)}
|
|
10878
|
+
WHERE rows.entity_kind = 'link'
|
|
10607
10879
|
`,
|
|
10608
10880
|
this.sql`
|
|
10609
10881
|
WITH requested AS (
|
|
@@ -10745,32 +11017,20 @@ class PgMaterializationStateReader {
|
|
|
10745
11017
|
SELECT rows.source_type_id, rows.source_primary_id AS source_id,
|
|
10746
11018
|
rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
|
|
10747
11019
|
FROM ontology_source_rows AS rows
|
|
10748
|
-
|
|
10749
|
-
ON sources.project_id = rows.project_id
|
|
10750
|
-
AND sources.source_id = rows.source_id
|
|
10751
|
-
AND sources.materialization_id = rows.materialization_id
|
|
10752
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
11020
|
+
${liveSourceRowsJoin(this.sql, this.projectId)}
|
|
10753
11021
|
JOIN requested
|
|
10754
11022
|
ON requested.object_type_id = rows.source_type_id
|
|
10755
11023
|
AND requested.primary_id = rows.source_primary_id
|
|
10756
|
-
WHERE rows.
|
|
10757
|
-
AND rows.entity_kind = 'link'
|
|
10758
|
-
AND sources.active
|
|
11024
|
+
WHERE rows.entity_kind = 'link'
|
|
10759
11025
|
UNION
|
|
10760
11026
|
SELECT rows.source_type_id, rows.source_primary_id AS source_id,
|
|
10761
11027
|
rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
|
|
10762
11028
|
FROM ontology_source_rows AS rows
|
|
10763
|
-
|
|
10764
|
-
ON sources.project_id = rows.project_id
|
|
10765
|
-
AND sources.source_id = rows.source_id
|
|
10766
|
-
AND sources.materialization_id = rows.materialization_id
|
|
10767
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
11029
|
+
${liveSourceRowsJoin(this.sql, this.projectId)}
|
|
10768
11030
|
JOIN requested
|
|
10769
11031
|
ON requested.object_type_id = rows.target_type_id
|
|
10770
11032
|
AND requested.primary_id = rows.target_primary_id
|
|
10771
|
-
WHERE rows.
|
|
10772
|
-
AND rows.entity_kind = 'link'
|
|
10773
|
-
AND sources.active
|
|
11033
|
+
WHERE rows.entity_kind = 'link'
|
|
10774
11034
|
), selected AS (
|
|
10775
11035
|
SELECT DISTINCT links.*,
|
|
10776
11036
|
${this.sql.unsafe(linkSortExpression("links"))} AS sort_key
|
|
@@ -11004,32 +11264,20 @@ class PgMaterializationStateReader {
|
|
|
11004
11264
|
SELECT rows.source_type_id, rows.source_primary_id AS source_id,
|
|
11005
11265
|
rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
|
|
11006
11266
|
FROM ontology_source_rows AS rows
|
|
11007
|
-
|
|
11008
|
-
ON sources.project_id = rows.project_id
|
|
11009
|
-
AND sources.source_id = rows.source_id
|
|
11010
|
-
AND sources.materialization_id = rows.materialization_id
|
|
11011
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
11267
|
+
${liveSourceRowsJoin(this.sql, this.projectId)}
|
|
11012
11268
|
JOIN incident_objects
|
|
11013
11269
|
ON incident_objects.object_type_id = rows.source_type_id
|
|
11014
11270
|
AND incident_objects.primary_id = rows.source_primary_id
|
|
11015
|
-
WHERE rows.
|
|
11016
|
-
AND rows.entity_kind = 'link'
|
|
11017
|
-
AND sources.active
|
|
11271
|
+
WHERE rows.entity_kind = 'link'
|
|
11018
11272
|
UNION
|
|
11019
11273
|
SELECT rows.source_type_id, rows.source_primary_id AS source_id,
|
|
11020
11274
|
rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
|
|
11021
11275
|
FROM ontology_source_rows AS rows
|
|
11022
|
-
|
|
11023
|
-
ON sources.project_id = rows.project_id
|
|
11024
|
-
AND sources.source_id = rows.source_id
|
|
11025
|
-
AND sources.materialization_id = rows.materialization_id
|
|
11026
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
11276
|
+
${liveSourceRowsJoin(this.sql, this.projectId)}
|
|
11027
11277
|
JOIN incident_objects
|
|
11028
11278
|
ON incident_objects.object_type_id = rows.target_type_id
|
|
11029
11279
|
AND incident_objects.primary_id = rows.target_primary_id
|
|
11030
|
-
WHERE rows.
|
|
11031
|
-
AND rows.entity_kind = 'link'
|
|
11032
|
-
AND sources.active
|
|
11280
|
+
WHERE rows.entity_kind = 'link'
|
|
11033
11281
|
), diff_links AS (
|
|
11034
11282
|
SELECT * FROM replacement_links
|
|
11035
11283
|
UNION SELECT * FROM incident_links
|
|
@@ -11171,9 +11419,7 @@ function storedSource(row) {
|
|
|
11171
11419
|
return {
|
|
11172
11420
|
source: { projectionId: row.source_id },
|
|
11173
11421
|
materializationId: row.materialization_id,
|
|
11174
|
-
|
|
11175
|
-
assertion: structuredClone(row.assertion),
|
|
11176
|
-
stagingOrdinal: databaseSafeInteger(row.staging_ordinal, "Source staging ordinal")
|
|
11422
|
+
...sourceAssertion(row)
|
|
11177
11423
|
};
|
|
11178
11424
|
}
|
|
11179
11425
|
function effectiveObjectSnapshot(row) {
|
|
@@ -12871,26 +13117,70 @@ import {
|
|
|
12871
13117
|
sourceEntityColumns as entityColumns,
|
|
12872
13118
|
isExactStagingManifest,
|
|
12873
13119
|
reconcileSourceStageRows,
|
|
13120
|
+
sourceAssertionPayload,
|
|
12874
13121
|
sourceConflict,
|
|
12875
13122
|
sourceMaterializationIdentity,
|
|
12876
13123
|
sourceStageRow,
|
|
12877
|
-
sourceStageRows
|
|
12878
|
-
utf8SortKey as utf8SortKey2
|
|
13124
|
+
sourceStageRows
|
|
12879
13125
|
} from "@sixb/core/internal/ontology-storage-provider";
|
|
12880
13126
|
|
|
12881
13127
|
// src/ontology-storage/source-cleanup.ts
|
|
13128
|
+
async function purgeAbandonedSourceVersions(sql, input) {
|
|
13129
|
+
let remaining = input.limit;
|
|
13130
|
+
let rowsDeleted = 0;
|
|
13131
|
+
let materializationsDeleted = 0;
|
|
13132
|
+
while (remaining > 0) {
|
|
13133
|
+
const [candidate] = await sql`
|
|
13134
|
+
SELECT version_id FROM ontology_sources
|
|
13135
|
+
WHERE project_id = ${input.projectId} AND status = 'abandoned'
|
|
13136
|
+
ORDER BY terminal_at, source_id, materialization_id
|
|
13137
|
+
LIMIT 1 FOR UPDATE SKIP LOCKED
|
|
13138
|
+
`;
|
|
13139
|
+
if (!candidate)
|
|
13140
|
+
break;
|
|
13141
|
+
const rows = await sql`
|
|
13142
|
+
DELETE FROM ontology_source_rows WHERE ctid IN (
|
|
13143
|
+
SELECT rows.ctid FROM ontology_source_roots AS roots
|
|
13144
|
+
JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
13145
|
+
WHERE roots.version_id = ${candidate.version_id} LIMIT ${remaining}
|
|
13146
|
+
)
|
|
13147
|
+
`;
|
|
13148
|
+
rowsDeleted += rows.count;
|
|
13149
|
+
remaining -= rows.count;
|
|
13150
|
+
if (remaining === 0)
|
|
13151
|
+
break;
|
|
13152
|
+
const roots = await sql`
|
|
13153
|
+
DELETE FROM ontology_source_roots WHERE ctid IN (
|
|
13154
|
+
SELECT ctid FROM ontology_source_roots
|
|
13155
|
+
WHERE version_id = ${candidate.version_id} LIMIT ${remaining}
|
|
13156
|
+
)
|
|
13157
|
+
`;
|
|
13158
|
+
rowsDeleted += roots.count;
|
|
13159
|
+
remaining -= roots.count;
|
|
13160
|
+
if (remaining === 0)
|
|
13161
|
+
break;
|
|
13162
|
+
const manifest = await sql`
|
|
13163
|
+
DELETE FROM ontology_sources
|
|
13164
|
+
WHERE version_id = ${candidate.version_id} AND status = 'abandoned'
|
|
13165
|
+
`;
|
|
13166
|
+
if (manifest.count === 0)
|
|
13167
|
+
break;
|
|
13168
|
+
materializationsDeleted += manifest.count;
|
|
13169
|
+
remaining -= manifest.count;
|
|
13170
|
+
}
|
|
13171
|
+
return { rowsDeleted, materializationsDeleted };
|
|
13172
|
+
}
|
|
12882
13173
|
async function cleanupSourceVersions(sql, input) {
|
|
12883
13174
|
let remaining = input.limit;
|
|
13175
|
+
const retired = sql`roots.project_id = ${input.projectId}
|
|
13176
|
+
AND roots.retired_at < ${input.terminalBefore} AND versions.status = 'superseded'`;
|
|
12884
13177
|
const removedRows = await sql`
|
|
12885
13178
|
WITH selected AS (
|
|
12886
13179
|
SELECT rows.ctid FROM ontology_source_roots AS roots
|
|
12887
|
-
JOIN ontology_sources AS
|
|
12888
|
-
JOIN ontology_source_rows AS rows
|
|
12889
|
-
|
|
12890
|
-
|
|
12891
|
-
WHERE roots.project_id = ${input.projectId} AND NOT roots.active AND roots.retired_at < ${input.terminalBefore}
|
|
12892
|
-
AND sources.status IN ('superseded', 'abandoned')
|
|
12893
|
-
ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key, rows.entity_sort_key
|
|
13180
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
13181
|
+
JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
13182
|
+
WHERE ${retired}
|
|
13183
|
+
ORDER BY roots.retired_at, roots.id
|
|
12894
13184
|
LIMIT ${remaining} FOR UPDATE OF rows SKIP LOCKED
|
|
12895
13185
|
) DELETE FROM ontology_source_rows AS rows USING selected WHERE rows.ctid = selected.ctid RETURNING 1
|
|
12896
13186
|
`;
|
|
@@ -12900,13 +13190,10 @@ async function cleanupSourceVersions(sql, input) {
|
|
|
12900
13190
|
const removed = await sql`
|
|
12901
13191
|
WITH selected AS (
|
|
12902
13192
|
SELECT roots.ctid FROM ontology_source_roots AS roots
|
|
12903
|
-
JOIN ontology_sources AS
|
|
12904
|
-
WHERE
|
|
12905
|
-
AND
|
|
12906
|
-
|
|
12907
|
-
WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
12908
|
-
AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key)
|
|
12909
|
-
ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key
|
|
13193
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
13194
|
+
WHERE ${retired}
|
|
13195
|
+
AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id)
|
|
13196
|
+
ORDER BY roots.retired_at, roots.id
|
|
12910
13197
|
LIMIT ${remaining} FOR UPDATE OF roots SKIP LOCKED
|
|
12911
13198
|
) DELETE FROM ontology_source_roots AS roots USING selected WHERE roots.ctid = selected.ctid RETURNING 1
|
|
12912
13199
|
`;
|
|
@@ -12915,16 +13202,13 @@ async function cleanupSourceVersions(sql, input) {
|
|
|
12915
13202
|
}
|
|
12916
13203
|
const removedManifests = remaining === 0 ? [] : await sql`
|
|
12917
13204
|
WITH selected AS (
|
|
12918
|
-
SELECT
|
|
12919
|
-
WHERE project_id = ${input.projectId} AND status
|
|
13205
|
+
SELECT versions.ctid FROM ontology_sources AS versions
|
|
13206
|
+
WHERE project_id = ${input.projectId} AND status = 'superseded'
|
|
13207
|
+
AND terminal_at < ${input.terminalBefore}
|
|
12920
13208
|
AND NOT EXISTS (SELECT 1 FROM ontology_source_roots AS roots
|
|
12921
|
-
WHERE roots.
|
|
12922
|
-
AND roots.materialization_id = sources.materialization_id)
|
|
12923
|
-
AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
|
|
12924
|
-
WHERE rows.project_id = sources.project_id AND rows.source_id = sources.source_id
|
|
12925
|
-
AND rows.materialization_id = sources.materialization_id)
|
|
13209
|
+
WHERE roots.version_id = versions.version_id)
|
|
12926
13210
|
ORDER BY terminal_at, source_id, materialization_id LIMIT ${remaining} FOR UPDATE SKIP LOCKED
|
|
12927
|
-
) DELETE FROM ontology_sources AS
|
|
13211
|
+
) DELETE FROM ontology_sources AS versions USING selected WHERE versions.ctid = selected.ctid RETURNING 1
|
|
12928
13212
|
`;
|
|
12929
13213
|
return { rowsDeleted, materializationsDeleted: removedManifests.length };
|
|
12930
13214
|
}
|
|
@@ -12951,7 +13235,7 @@ class PgOntologySourceStorage {
|
|
|
12951
13235
|
}
|
|
12952
13236
|
const candidate = await this.getRunCandidate(sql, input);
|
|
12953
13237
|
if (candidate) {
|
|
12954
|
-
throw sourceConflict(`Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization;
|
|
13238
|
+
throw sourceConflict(`Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; adopt it before beginning another.`);
|
|
12955
13239
|
}
|
|
12956
13240
|
const rows = await sql`
|
|
12957
13241
|
INSERT INTO ontology_sources (
|
|
@@ -12980,7 +13264,7 @@ class PgOntologySourceStorage {
|
|
|
12980
13264
|
return sourceRecord(racedManifest);
|
|
12981
13265
|
}
|
|
12982
13266
|
if (await this.getRunCandidate(sql, input)) {
|
|
12983
|
-
throw sourceConflict(`Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization;
|
|
13267
|
+
throw sourceConflict(`Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; adopt it before beginning another.`);
|
|
12984
13268
|
}
|
|
12985
13269
|
throw sourceConflict(`Source materialization '${input.materializationId}' already exists with different identity or state.`);
|
|
12986
13270
|
});
|
|
@@ -12995,10 +13279,10 @@ class PgOntologySourceStorage {
|
|
|
12995
13279
|
throw sourceConflict(`Source materialization '${input.materializationId}' is '${manifest.status}' and cannot accept rows.`);
|
|
12996
13280
|
}
|
|
12997
13281
|
const rows = sourceStageRows(manifest.projection_kind, input.rows);
|
|
12998
|
-
const existing = await this.findStageRows(sql,
|
|
13282
|
+
const existing = await this.findStageRows(sql, manifest, rows);
|
|
12999
13283
|
const { pending, unchanged } = reconcileSourceStageRows(rows, existing);
|
|
13000
13284
|
await stageSourceRoots(sql, manifest, input);
|
|
13001
|
-
await this.insertStageRows(sql,
|
|
13285
|
+
await this.insertStageRows(sql, manifest, pending);
|
|
13002
13286
|
return { inserted: pending.length, unchanged };
|
|
13003
13287
|
});
|
|
13004
13288
|
}
|
|
@@ -13057,13 +13341,43 @@ class PgOntologySourceStorage {
|
|
|
13057
13341
|
return row ? sourceRecord(row) : null;
|
|
13058
13342
|
});
|
|
13059
13343
|
}
|
|
13344
|
+
async adopt(input) {
|
|
13345
|
+
return this.runRootOperation(async (sql) => {
|
|
13346
|
+
assertProjectAndSource(input);
|
|
13347
|
+
assertExecutionIdentity(input.execution.projectionRunId, input.execution.executionToken);
|
|
13348
|
+
assertTimestamp(input.adoptedAt, "Source adoptedAt", true);
|
|
13349
|
+
await this.assertExecution(sql, input);
|
|
13350
|
+
const candidate = await this.lockRunCandidate(sql, input);
|
|
13351
|
+
if (!candidate)
|
|
13352
|
+
return null;
|
|
13353
|
+
const [adopted] = await sql`
|
|
13354
|
+
UPDATE ontology_sources
|
|
13355
|
+
SET execution_token = ${input.execution.executionToken},
|
|
13356
|
+
updated_at = GREATEST(updated_at, ${input.adoptedAt}::timestamptz)
|
|
13357
|
+
WHERE project_id = ${candidate.project_id} AND source_id = ${candidate.source_id}
|
|
13358
|
+
AND materialization_id = ${candidate.materialization_id}
|
|
13359
|
+
AND status IN ('staging', 'ready')
|
|
13360
|
+
RETURNING *
|
|
13361
|
+
`;
|
|
13362
|
+
if (!adopted) {
|
|
13363
|
+
throw sourceConflict(`Source materialization '${candidate.materialization_id}' changed.`);
|
|
13364
|
+
}
|
|
13365
|
+
return {
|
|
13366
|
+
record: sourceRecord(adopted),
|
|
13367
|
+
resumeStagingOrdinal: adopted.status === "ready" ? Number(adopted.root_count) : await this.lastStagedOrdinal(sql, adopted)
|
|
13368
|
+
};
|
|
13369
|
+
});
|
|
13370
|
+
}
|
|
13060
13371
|
async abandon(input) {
|
|
13061
13372
|
return this.runRootOperation(async (sql) => {
|
|
13062
13373
|
assertProjectAndSource(input);
|
|
13063
13374
|
assertExecutionIdentity(input.execution.projectionRunId, input.execution.executionToken);
|
|
13064
13375
|
assertTimestamp(input.abandonedAt, "Source abandonedAt", true);
|
|
13065
13376
|
await this.assertExecution(sql, input);
|
|
13066
|
-
|
|
13377
|
+
if (input.kind === "candidate")
|
|
13378
|
+
return this.abandonCandidate(sql, input);
|
|
13379
|
+
const candidate = await this.lockRunCandidate(sql, input);
|
|
13380
|
+
return candidate ? this.transitionToAbandoned(sql, candidate, input.abandonedAt) : null;
|
|
13067
13381
|
});
|
|
13068
13382
|
}
|
|
13069
13383
|
async cleanupTerminal(input) {
|
|
@@ -13074,6 +13388,13 @@ class PgOntologySourceStorage {
|
|
|
13074
13388
|
return cleanupSourceVersions(sql, input);
|
|
13075
13389
|
});
|
|
13076
13390
|
}
|
|
13391
|
+
async purgeAbandoned(input) {
|
|
13392
|
+
return this.runRootOperation(async (sql) => {
|
|
13393
|
+
assertNonblank(input.projectId, "Abandoned source purge project id");
|
|
13394
|
+
assertPositiveInteger(input.limit, "Abandoned source purge limit");
|
|
13395
|
+
return purgeAbandonedSourceVersions(sql, input);
|
|
13396
|
+
});
|
|
13397
|
+
}
|
|
13077
13398
|
async summarizeTerminal(input) {
|
|
13078
13399
|
return this.runRootOperation(async (sql) => {
|
|
13079
13400
|
assertNonblank(input.projectId, "Terminal source summary project id");
|
|
@@ -13100,7 +13421,7 @@ class PgOntologySourceStorage {
|
|
|
13100
13421
|
assertSourceCandidateOwner(sourceRecord(manifest), input.execution);
|
|
13101
13422
|
return this.transitionToAbandoned(sql, manifest, input.abandonedAt);
|
|
13102
13423
|
}
|
|
13103
|
-
async
|
|
13424
|
+
async lockRunCandidate(sql, input) {
|
|
13104
13425
|
const candidates = await sql`
|
|
13105
13426
|
SELECT * FROM ontology_sources
|
|
13106
13427
|
WHERE project_id = ${input.projectId}
|
|
@@ -13109,13 +13430,17 @@ class PgOntologySourceStorage {
|
|
|
13109
13430
|
AND status IN ('staging', 'ready')
|
|
13110
13431
|
FOR UPDATE
|
|
13111
13432
|
`;
|
|
13112
|
-
if (candidates.some((candidate) => candidate.execution_token === input.execution.executionToken)) {
|
|
13113
|
-
throw sourceConflict(`Projection run '${input.execution.projectionRunId}' already has a source materialization owned by the current execution.`);
|
|
13114
|
-
}
|
|
13115
13433
|
if (candidates.length > 1) {
|
|
13116
13434
|
throw sourceConflict(`Projection run '${input.execution.projectionRunId}' has multiple nonterminal source materializations.`);
|
|
13117
13435
|
}
|
|
13118
|
-
return candidates[0]
|
|
13436
|
+
return candidates[0] ?? null;
|
|
13437
|
+
}
|
|
13438
|
+
async lastStagedOrdinal(sql, manifest) {
|
|
13439
|
+
const [last] = await sql`
|
|
13440
|
+
SELECT MAX(staging_ordinal) AS ordinal FROM ontology_source_roots
|
|
13441
|
+
WHERE version_id = ${manifest.version_id}
|
|
13442
|
+
`;
|
|
13443
|
+
return last?.ordinal == null ? 0 : Number(last.ordinal);
|
|
13119
13444
|
}
|
|
13120
13445
|
async transitionToAbandoned(sql, manifest, abandonedAt) {
|
|
13121
13446
|
if (abandonedAt < toIsoString2(manifest.created_at) || manifest.ready_at !== null && abandonedAt < toIsoString2(manifest.ready_at)) {
|
|
@@ -13134,22 +13459,18 @@ class PgOntologySourceStorage {
|
|
|
13134
13459
|
if (!rows[0]) {
|
|
13135
13460
|
throw sourceConflict(`Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`);
|
|
13136
13461
|
}
|
|
13137
|
-
await sql`UPDATE ontology_source_roots SET retired_at = ${abandonedAt}
|
|
13138
|
-
WHERE project_id = ${manifest.project_id} AND source_id = ${manifest.source_id}
|
|
13139
|
-
AND materialization_id = ${manifest.materialization_id} AND NOT active`;
|
|
13140
13462
|
return sourceRecord(rows[0]);
|
|
13141
13463
|
}
|
|
13142
13464
|
async assertReadyState(sql, manifest, rootCount, assertionCount) {
|
|
13465
|
+
const version = manifest.version_id;
|
|
13143
13466
|
const [counts] = await sql`
|
|
13144
|
-
SELECT (SELECT COUNT(*) FROM
|
|
13145
|
-
|
|
13146
|
-
|
|
13147
|
-
COUNT(DISTINCT staging_ordinal) AS ordinals,
|
|
13467
|
+
SELECT (SELECT COUNT(*) FROM ontology_source_roots AS roots
|
|
13468
|
+
JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
13469
|
+
WHERE roots.version_id = ${version}) AS assertions,
|
|
13470
|
+
COUNT(*) AS roots, COUNT(DISTINCT staging_ordinal) AS ordinals,
|
|
13148
13471
|
MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
|
|
13149
13472
|
FROM ontology_source_roots
|
|
13150
|
-
WHERE
|
|
13151
|
-
AND source_id = ${manifest.source_id}
|
|
13152
|
-
AND materialization_id = ${manifest.materialization_id}
|
|
13473
|
+
WHERE version_id = ${version}
|
|
13153
13474
|
`;
|
|
13154
13475
|
const assertions = Number(counts?.assertions ?? 0);
|
|
13155
13476
|
const roots = Number(counts?.roots ?? 0);
|
|
@@ -13161,122 +13482,101 @@ class PgOntologySourceStorage {
|
|
|
13161
13482
|
}
|
|
13162
13483
|
await assertSourceRootCoverage(sql, manifest);
|
|
13163
13484
|
const [invalid] = manifest.projection_kind === "link" ? await sql`
|
|
13164
|
-
SELECT root_key
|
|
13165
|
-
FROM
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
GROUP BY root_key
|
|
13485
|
+
SELECT roots.root_key
|
|
13486
|
+
FROM ontology_source_roots AS roots
|
|
13487
|
+
CROSS JOIN LATERAL (SELECT roots.root_key::jsonb AS parts) AS root
|
|
13488
|
+
JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
13489
|
+
WHERE roots.version_id = ${version}
|
|
13490
|
+
GROUP BY roots.id, roots.root_key
|
|
13170
13491
|
HAVING COUNT(*) <> 1
|
|
13171
|
-
OR BOOL_OR(
|
|
13172
|
-
OR BOOL_OR(entity_kind <> 'link')
|
|
13173
|
-
OR BOOL_OR(
|
|
13492
|
+
OR BOOL_OR(root.parts->>0 <> 'link')
|
|
13493
|
+
OR BOOL_OR(rows.entity_kind <> 'link')
|
|
13494
|
+
OR BOOL_OR(rows.source_type_id <> root.parts->>1
|
|
13495
|
+
OR rows.source_primary_id <> root.parts->>2 OR rows.link_id <> root.parts->>3
|
|
13496
|
+
OR rows.target_type_id <> root.parts->>4 OR rows.target_primary_id <> root.parts->>5)
|
|
13174
13497
|
LIMIT 1
|
|
13175
13498
|
` : await sql`
|
|
13176
|
-
SELECT root_key
|
|
13177
|
-
FROM
|
|
13178
|
-
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
GROUP BY root_key
|
|
13182
|
-
HAVING BOOL_OR(
|
|
13499
|
+
SELECT roots.root_key
|
|
13500
|
+
FROM ontology_source_roots AS roots
|
|
13501
|
+
CROSS JOIN LATERAL (SELECT roots.root_key::jsonb AS parts) AS root
|
|
13502
|
+
JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
13503
|
+
WHERE roots.version_id = ${version}
|
|
13504
|
+
GROUP BY roots.id, roots.root_key
|
|
13505
|
+
HAVING BOOL_OR(root.parts->>0 <> 'object')
|
|
13183
13506
|
OR COUNT(*) FILTER (
|
|
13184
|
-
WHERE entity_kind = 'object' AND
|
|
13507
|
+
WHERE rows.entity_kind = 'object' AND rows.object_type_id = root.parts->>1
|
|
13508
|
+
AND rows.primary_id = root.parts->>2
|
|
13185
13509
|
) <> 1
|
|
13186
13510
|
OR COUNT(*) FILTER (
|
|
13187
|
-
WHERE (entity_kind = 'object' AND
|
|
13188
|
-
|
|
13189
|
-
|
|
13190
|
-
|
|
13191
|
-
))
|
|
13511
|
+
WHERE (rows.entity_kind = 'object' AND (rows.object_type_id <> root.parts->>1
|
|
13512
|
+
OR rows.primary_id <> root.parts->>2))
|
|
13513
|
+
OR (rows.entity_kind = 'link' AND (rows.source_type_id <> root.parts->>1
|
|
13514
|
+
OR rows.source_primary_id <> root.parts->>2))
|
|
13192
13515
|
) <> 0
|
|
13516
|
+
OR COUNT(*) FILTER (WHERE rows.entity_kind = 'link') <> COUNT(DISTINCT (
|
|
13517
|
+
rows.link_id, rows.target_type_id, rows.target_primary_id
|
|
13518
|
+
)) FILTER (WHERE rows.entity_kind = 'link')
|
|
13193
13519
|
LIMIT 1
|
|
13194
13520
|
`;
|
|
13195
13521
|
if (invalid) {
|
|
13196
13522
|
throw new MaterializationValidationError4(manifest.projection_kind === "link" ? "Link projection roots must contain exactly their matching link assertion." : "Object projection roots must contain exactly their matching object assertion plus links sourced from that root.");
|
|
13197
13523
|
}
|
|
13198
13524
|
}
|
|
13199
|
-
async findStageRows(sql,
|
|
13525
|
+
async findStageRows(sql, manifest, rows) {
|
|
13200
13526
|
if (rows.length === 0)
|
|
13201
13527
|
return [];
|
|
13202
|
-
const
|
|
13528
|
+
const rootKeys = [...new Set(rows.map((row) => row.rootKey))];
|
|
13203
13529
|
const ordinals = [...new Set(rows.map((row) => row.row.stagingOrdinal))];
|
|
13204
|
-
const entitySortKeys = [...new Set(rows.map((row) => utf8SortKey2(row.entityKey)))];
|
|
13205
13530
|
const existing = await sql`
|
|
13206
|
-
SELECT
|
|
13207
|
-
|
|
13208
|
-
|
|
13209
|
-
|
|
13531
|
+
SELECT ${sourceAssertionColumns(sql)}
|
|
13532
|
+
FROM ontology_source_roots AS roots
|
|
13533
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
13534
|
+
JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
13535
|
+
WHERE roots.version_id = ${manifest.version_id}
|
|
13210
13536
|
AND (
|
|
13211
|
-
|
|
13212
|
-
OR staging_ordinal = ANY(${sql.array(ordinals)}::bigint[])
|
|
13213
|
-
OR entity_sort_key = ANY(${sql.array(entitySortKeys)}::text[])
|
|
13537
|
+
roots.root_key = ANY(${sql.array(rootKeys)}::text[])
|
|
13538
|
+
OR roots.staging_ordinal = ANY(${sql.array(ordinals)}::bigint[])
|
|
13214
13539
|
)
|
|
13215
13540
|
`;
|
|
13216
13541
|
return existing.map((row) => sourceStageRow(sourceAssertion(row)));
|
|
13217
13542
|
}
|
|
13218
|
-
async insertStageRows(sql,
|
|
13543
|
+
async insertStageRows(sql, manifest, rows) {
|
|
13219
13544
|
if (rows.length === 0)
|
|
13220
13545
|
return;
|
|
13221
|
-
const payload = rows.map(({
|
|
13546
|
+
const payload = rows.map(({ rootKey, row }) => {
|
|
13222
13547
|
const entity = entityColumns(row.assertion);
|
|
13223
|
-
const root = entityColumns(row.root);
|
|
13224
13548
|
return {
|
|
13225
|
-
|
|
13226
|
-
|
|
13227
|
-
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
primaryId: entity.primaryId,
|
|
13236
|
-
sourceTypeId: entity.sourceTypeId,
|
|
13237
|
-
sourcePrimaryId: entity.sourcePrimaryId,
|
|
13238
|
-
linkId: entity.linkId,
|
|
13239
|
-
targetTypeId: entity.targetTypeId,
|
|
13240
|
-
targetPrimaryId: entity.targetPrimaryId,
|
|
13241
|
-
rootObjectTypeId: root.objectTypeId,
|
|
13242
|
-
rootPrimaryId: root.primaryId,
|
|
13243
|
-
rootSourceTypeId: root.sourceTypeId,
|
|
13244
|
-
rootSourcePrimaryId: root.sourcePrimaryId,
|
|
13245
|
-
rootLinkId: root.linkId,
|
|
13246
|
-
rootTargetTypeId: root.targetTypeId,
|
|
13247
|
-
rootTargetPrimaryId: root.targetPrimaryId
|
|
13549
|
+
root_key: rootKey,
|
|
13550
|
+
entity_kind: row.assertion.kind,
|
|
13551
|
+
object_type_id: entity.objectTypeId,
|
|
13552
|
+
primary_id: entity.primaryId,
|
|
13553
|
+
source_type_id: entity.sourceTypeId,
|
|
13554
|
+
source_primary_id: entity.sourcePrimaryId,
|
|
13555
|
+
link_id: entity.linkId,
|
|
13556
|
+
target_type_id: entity.targetTypeId,
|
|
13557
|
+
target_primary_id: entity.targetPrimaryId,
|
|
13558
|
+
payload: sourceAssertionPayload(row.assertion)
|
|
13248
13559
|
};
|
|
13249
13560
|
});
|
|
13250
|
-
await sql`
|
|
13251
|
-
WITH staged AS (
|
|
13252
|
-
SELECT value
|
|
13253
|
-
FROM jsonb_array_elements(${jsonParameter2(sql, payload)}::jsonb)
|
|
13254
|
-
)
|
|
13561
|
+
const inserted = await sql`
|
|
13255
13562
|
INSERT INTO ontology_source_rows (
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
root_kind, root_key, root_sort_key, staging_ordinal,
|
|
13259
|
-
root, assertion,
|
|
13260
|
-
object_type_id, primary_id,
|
|
13261
|
-
source_type_id, source_primary_id, link_id, target_type_id, target_primary_id,
|
|
13262
|
-
root_object_type_id, root_primary_id,
|
|
13263
|
-
root_source_type_id, root_source_primary_id, root_link_id,
|
|
13264
|
-
root_target_type_id, root_target_primary_id
|
|
13563
|
+
root_id, entity_kind, object_type_id, primary_id,
|
|
13564
|
+
source_type_id, source_primary_id, link_id, target_type_id, target_primary_id, payload
|
|
13265
13565
|
)
|
|
13266
|
-
SELECT
|
|
13267
|
-
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13273
|
-
|
|
13274
|
-
|
|
13275
|
-
|
|
13276
|
-
value->>'rootSourceTypeId', value->>'rootSourcePrimaryId', value->>'rootLinkId',
|
|
13277
|
-
value->>'rootTargetTypeId', value->>'rootTargetPrimaryId'
|
|
13278
|
-
FROM staged
|
|
13566
|
+
SELECT roots.id, staged.entity_kind, staged.object_type_id, staged.primary_id,
|
|
13567
|
+
staged.source_type_id, staged.source_primary_id, staged.link_id,
|
|
13568
|
+
staged.target_type_id, staged.target_primary_id, staged.payload
|
|
13569
|
+
FROM jsonb_to_recordset(${jsonParameter2(sql, payload)}) AS staged(
|
|
13570
|
+
root_key TEXT, entity_kind TEXT, object_type_id TEXT, primary_id TEXT,
|
|
13571
|
+
source_type_id TEXT, source_primary_id TEXT, link_id TEXT,
|
|
13572
|
+
target_type_id TEXT, target_primary_id TEXT, payload JSONB
|
|
13573
|
+
)
|
|
13574
|
+
JOIN ontology_source_roots AS roots
|
|
13575
|
+
ON roots.version_id = ${manifest.version_id} AND roots.root_key = staged.root_key
|
|
13279
13576
|
`;
|
|
13577
|
+
if (inserted.count !== rows.length) {
|
|
13578
|
+
throw sourceConflict("Staged source rows do not all belong to a staged root.");
|
|
13579
|
+
}
|
|
13280
13580
|
}
|
|
13281
13581
|
assertExecution(sql, input, identity) {
|
|
13282
13582
|
return assertProjectionExecution(sql, {
|
|
@@ -15553,6 +15853,8 @@ function invalidState(reservation, operation) {
|
|
|
15553
15853
|
}
|
|
15554
15854
|
|
|
15555
15855
|
// src/pg-client.ts
|
|
15856
|
+
import { isIP } from "net";
|
|
15857
|
+
import { connect as connectTls } from "tls";
|
|
15556
15858
|
import postgres from "postgres";
|
|
15557
15859
|
var DEFAULT_IDLE_TIMEOUT_SECONDS = 30;
|
|
15558
15860
|
var DEFAULT_CONNECT_TIMEOUT_SECONDS = 10;
|
|
@@ -15575,10 +15877,10 @@ function createPgClient(options) {
|
|
|
15575
15877
|
connection,
|
|
15576
15878
|
onnotice: () => {},
|
|
15577
15879
|
...options.prepare !== undefined ? { prepare: options.prepare } : {},
|
|
15578
|
-
...options
|
|
15880
|
+
...tlsOptions(options)
|
|
15579
15881
|
};
|
|
15580
15882
|
if (options.connectionString) {
|
|
15581
|
-
return postgres(options.connectionString, base);
|
|
15883
|
+
return assertDirectTlsTarget(postgres(options.connectionString, base), base);
|
|
15582
15884
|
}
|
|
15583
15885
|
return postgres({
|
|
15584
15886
|
...base,
|
|
@@ -15589,6 +15891,78 @@ function createPgClient(options) {
|
|
|
15589
15891
|
...options.password !== undefined ? { password: options.password } : {}
|
|
15590
15892
|
});
|
|
15591
15893
|
}
|
|
15894
|
+
function tlsOptions(options) {
|
|
15895
|
+
const verification = directTlsVerification(options);
|
|
15896
|
+
if (verification === null) {
|
|
15897
|
+
return options.ssl ? { ssl: options.ssl } : {};
|
|
15898
|
+
}
|
|
15899
|
+
return { ssl: false, sslnegotiation: null, socket: directTlsSocket(verification) };
|
|
15900
|
+
}
|
|
15901
|
+
function directTlsVerification(options) {
|
|
15902
|
+
const params = connectionStringParams(options.connectionString);
|
|
15903
|
+
if (params.get("sslnegotiation") !== "direct") {
|
|
15904
|
+
return null;
|
|
15905
|
+
}
|
|
15906
|
+
const sslmode = options.ssl === true ? "verify-full" : options.ssl || (params.get("sslrootcert") === "system" ? "verify-full" : params.get("sslmode") ?? params.get("ssl"));
|
|
15907
|
+
switch (sslmode) {
|
|
15908
|
+
case "require":
|
|
15909
|
+
return "none";
|
|
15910
|
+
case "verify-ca":
|
|
15911
|
+
return "ca";
|
|
15912
|
+
case "verify-full":
|
|
15913
|
+
case "true":
|
|
15914
|
+
return "full";
|
|
15915
|
+
default:
|
|
15916
|
+
throw new Error(`[SixbPg] sslnegotiation=direct requires sslmode=require, verify-ca or verify-full, got ${sslmode ? `"${sslmode}"` : "no sslmode"}: a direct TLS connection has no plaintext fallback.`);
|
|
15917
|
+
}
|
|
15918
|
+
}
|
|
15919
|
+
function connectionStringParams(connectionString) {
|
|
15920
|
+
return new URLSearchParams(connectionString?.split("?")[1] ?? "");
|
|
15921
|
+
}
|
|
15922
|
+
function directTlsSocket(verification) {
|
|
15923
|
+
let reportedRefusal = false;
|
|
15924
|
+
return ({ host: [host = "localhost"], port: [port = 5432] }) => {
|
|
15925
|
+
const socket = connectTls({
|
|
15926
|
+
host,
|
|
15927
|
+
port,
|
|
15928
|
+
servername: isIP(host) ? undefined : host,
|
|
15929
|
+
ALPNProtocols: ["postgresql"],
|
|
15930
|
+
rejectUnauthorized: verification !== "none",
|
|
15931
|
+
...verification === "ca" ? { checkServerIdentity: () => {
|
|
15932
|
+
return;
|
|
15933
|
+
} } : {}
|
|
15934
|
+
});
|
|
15935
|
+
let reachedServer = false;
|
|
15936
|
+
let secured = false;
|
|
15937
|
+
socket.once("connect", () => {
|
|
15938
|
+
reachedServer = true;
|
|
15939
|
+
});
|
|
15940
|
+
socket.once("secureConnect", () => {
|
|
15941
|
+
secured = true;
|
|
15942
|
+
if (socket.alpnProtocol !== "postgresql") {
|
|
15943
|
+
socket.destroy(new Error(`[SixbPg] ${host}:${port} accepted TLS without selecting the "postgresql" protocol` + " (ALPN), so it is not a PostgreSQL server accepting sslnegotiation=direct."));
|
|
15944
|
+
}
|
|
15945
|
+
});
|
|
15946
|
+
socket.once("error", (error) => {
|
|
15947
|
+
if (reportedRefusal || !reachedServer || secured)
|
|
15948
|
+
return;
|
|
15949
|
+
if (!["ECONNRESET", "EPIPE", "ERR_SOCKET_CLOSED"].includes(pgErrorCode(error) ?? ""))
|
|
15950
|
+
return;
|
|
15951
|
+
reportedRefusal = true;
|
|
15952
|
+
console.error(`[SixbPg] ${host}:${port} closed the connection during a direct TLS handshake` + " (sslnegotiation=direct). Direct TLS needs PostgreSQL 17 or later, reached without a" + " proxy or pooler that expects an SSLRequest; remove sslnegotiation=direct otherwise.");
|
|
15953
|
+
});
|
|
15954
|
+
return socket;
|
|
15955
|
+
};
|
|
15956
|
+
}
|
|
15957
|
+
function assertDirectTlsTarget(sql, options) {
|
|
15958
|
+
if (!("socket" in options)) {
|
|
15959
|
+
return sql;
|
|
15960
|
+
}
|
|
15961
|
+
if (sql.options.host.length !== 1 || sql.options.path) {
|
|
15962
|
+
throw new Error("[SixbPg] sslnegotiation=direct supports one TCP host:port in the connection string, not" + " several hosts or a Unix socket path.");
|
|
15963
|
+
}
|
|
15964
|
+
return sql;
|
|
15965
|
+
}
|
|
15592
15966
|
|
|
15593
15967
|
// src/pg-execution-storage.ts
|
|
15594
15968
|
import {
|
|
@@ -15853,13 +16227,19 @@ async function queryLatestRunsByOwnerId(input) {
|
|
|
15853
16227
|
return [];
|
|
15854
16228
|
}
|
|
15855
16229
|
const placeholders = ownerIds.map((_, index) => `$${index + 2}`).join(", ");
|
|
16230
|
+
const parameters = [input.projectId, ...ownerIds];
|
|
16231
|
+
let statusFilter = "";
|
|
16232
|
+
if (input.excludedStatus !== undefined) {
|
|
16233
|
+
parameters.push(input.excludedStatus);
|
|
16234
|
+
statusFilter = ` AND status <> $${parameters.length}`;
|
|
16235
|
+
}
|
|
15856
16236
|
const orderColumn = input.tableName === "sync_runs" || input.tableName === "pipeline_runs" || input.tableName === "projection_runs" ? "COALESCE(started_at, queued_at)" : "started_at";
|
|
15857
16237
|
const rows = await input.sql.unsafe(`
|
|
15858
16238
|
SELECT DISTINCT ON (${input.ownerColumn}) ${input.selectList ?? "*"}
|
|
15859
16239
|
FROM ${input.tableName}
|
|
15860
|
-
WHERE project_id = $1 AND ${input.ownerColumn} IN (${placeholders})
|
|
16240
|
+
WHERE project_id = $1 AND ${input.ownerColumn} IN (${placeholders})${statusFilter}
|
|
15861
16241
|
ORDER BY ${input.ownerColumn}, ${orderColumn} DESC, id DESC
|
|
15862
|
-
`,
|
|
16242
|
+
`, parameters);
|
|
15863
16243
|
const latestByOwnerId = new Map(rows.map((row) => [input.ownerIdFor(row), row]));
|
|
15864
16244
|
return ownerIds.flatMap((ownerId) => {
|
|
15865
16245
|
const row = latestByOwnerId.get(ownerId);
|
|
@@ -16248,12 +16628,14 @@ import {
|
|
|
16248
16628
|
failProjectionRunEnqueue,
|
|
16249
16629
|
immutableDatasetVersionConflict,
|
|
16250
16630
|
mergeProjectionRunProgress,
|
|
16631
|
+
parseProjectionAttemptFailure,
|
|
16251
16632
|
planProjectionRunFinish,
|
|
16252
16633
|
planProjectionRunReclaim,
|
|
16253
16634
|
projectionRunNotFound,
|
|
16254
16635
|
publicProjectionRunRecord,
|
|
16255
16636
|
requireTelemetryProjectionRun,
|
|
16256
16637
|
restoreProjectionRun,
|
|
16638
|
+
SUPERSEDING_PROJECTION_RUN_STATUSES,
|
|
16257
16639
|
staleProjectionRunExecution
|
|
16258
16640
|
} from "@sixb/core/internal/projection-run-storage-provider";
|
|
16259
16641
|
import { PROJECTION_RUN_FAILURE_CODES, ProjectionRunError } from "@sixb/core/storage";
|
|
@@ -16355,8 +16737,7 @@ class PgProjectionRunStorage {
|
|
|
16355
16737
|
const [updated] = await tx`
|
|
16356
16738
|
UPDATE projection_runs
|
|
16357
16739
|
SET status = ${"running"}, started_at = COALESCE(started_at, ${input.startedAt ?? new Date}),
|
|
16358
|
-
finished_at = ${null}, attempt = ${attempt}, execution_token = ${executionToken}
|
|
16359
|
-
error = ${null}
|
|
16740
|
+
finished_at = ${null}, attempt = ${attempt}, execution_token = ${executionToken}
|
|
16360
16741
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
16361
16742
|
AND (
|
|
16362
16743
|
(status = ${"queued"} AND execution_token IS NULL)
|
|
@@ -16423,6 +16804,24 @@ class PgProjectionRunStorage {
|
|
|
16423
16804
|
return rowToProjectionRunRecord(updated);
|
|
16424
16805
|
});
|
|
16425
16806
|
}
|
|
16807
|
+
async recordAttemptFailure(input) {
|
|
16808
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
16809
|
+
const existingRow = await requireMaterializationExecution(tx, input);
|
|
16810
|
+
const error = parseProjectionAttemptFailure(restoreProjectionRunRow(existingRow), input.error);
|
|
16811
|
+
const [updated] = await tx`
|
|
16812
|
+
UPDATE projection_runs
|
|
16813
|
+
SET error = ${serializeSixbFailure4(error, PROJECTION_RUN_FAILURE_CODES)}::text::jsonb
|
|
16814
|
+
WHERE project_id = ${input.projectId}
|
|
16815
|
+
AND id = ${input.id}
|
|
16816
|
+
AND status = ${"running"}
|
|
16817
|
+
AND execution_token = ${input.executionToken}
|
|
16818
|
+
RETURNING *
|
|
16819
|
+
`;
|
|
16820
|
+
if (!updated)
|
|
16821
|
+
throw staleProjectionRunExecution(input.id);
|
|
16822
|
+
return rowToProjectionRunRecord(updated);
|
|
16823
|
+
});
|
|
16824
|
+
}
|
|
16426
16825
|
async finish(input) {
|
|
16427
16826
|
return runPgTransaction(this.sql, async (tx) => {
|
|
16428
16827
|
const existingRow = await requireMaterializationExecution(tx, input);
|
|
@@ -16582,6 +16981,28 @@ class PgProjectionRunStorage {
|
|
|
16582
16981
|
const runs = rows.map(rowToProjectionRunRecord);
|
|
16583
16982
|
return { runs, hasMore: offset + runs.length < total, total };
|
|
16584
16983
|
}
|
|
16984
|
+
async findSupersedingRun(input) {
|
|
16985
|
+
const [run] = await this.sql`
|
|
16986
|
+
SELECT projection_id, materialization_protocol, dataset_version_created_at
|
|
16987
|
+
FROM projection_runs
|
|
16988
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
16989
|
+
`;
|
|
16990
|
+
if (!run)
|
|
16991
|
+
throw projectionRunNotFound(input.projectId, input.id);
|
|
16992
|
+
if (run.materialization_protocol !== "replacement")
|
|
16993
|
+
return null;
|
|
16994
|
+
const [newer] = await this.sql`
|
|
16995
|
+
SELECT * FROM projection_runs
|
|
16996
|
+
WHERE project_id = ${input.projectId}
|
|
16997
|
+
AND projection_id = ${run.projection_id}
|
|
16998
|
+
AND materialization_protocol = ${"replacement"}
|
|
16999
|
+
AND status = ANY(${this.sql.array([...SUPERSEDING_PROJECTION_RUN_STATUSES])}::text[])
|
|
17000
|
+
AND dataset_version_created_at COLLATE "C" > ${run.dataset_version_created_at} COLLATE "C"
|
|
17001
|
+
ORDER BY dataset_version_created_at COLLATE "C" DESC, id DESC
|
|
17002
|
+
LIMIT 1
|
|
17003
|
+
`;
|
|
17004
|
+
return newer ? rowToProjectionRunRecord(newer) : null;
|
|
17005
|
+
}
|
|
16585
17006
|
async listLatestByProjectionIds(input) {
|
|
16586
17007
|
const rows = await queryLatestRunsByOwnerId({
|
|
16587
17008
|
sql: this.sql,
|
|
@@ -16589,7 +17010,8 @@ class PgProjectionRunStorage {
|
|
|
16589
17010
|
ownerColumn: "projection_id",
|
|
16590
17011
|
ownerIds: input.projectionIds,
|
|
16591
17012
|
projectId: input.projectId,
|
|
16592
|
-
ownerIdFor: (row) => row.projection_id
|
|
17013
|
+
ownerIdFor: (row) => row.projection_id,
|
|
17014
|
+
excludedStatus: "superseded"
|
|
16593
17015
|
});
|
|
16594
17016
|
return { runs: rows.map(rowToProjectionRunRecord) };
|
|
16595
17017
|
}
|
|
@@ -18915,4 +19337,4 @@ export {
|
|
|
18915
19337
|
migratePostgresStorage
|
|
18916
19338
|
};
|
|
18917
19339
|
|
|
18918
|
-
//# debugId=
|
|
19340
|
+
//# debugId=4748AE3972DA287E64756E2164756E21
|