@rudderhq/db 0.7.2-canary.0 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backup-lib.d.ts.map +1 -1
- package/dist/backup-lib.js +60 -36
- package/dist/backup-lib.js.map +1 -1
- package/dist/backup-lib.test.js +122 -2
- package/dist/backup-lib.test.js.map +1 -1
- package/dist/client.d.ts +25 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +309 -20
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +416 -23
- package/dist/client.test.js.map +1 -1
- package/dist/embedded-postgres-recovery.d.ts +11 -0
- package/dist/embedded-postgres-recovery.d.ts.map +1 -1
- package/dist/embedded-postgres-recovery.js +50 -11
- package/dist/embedded-postgres-recovery.js.map +1 -1
- package/dist/embedded-postgres-recovery.test.js +50 -2
- package/dist/embedded-postgres-recovery.test.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/local-postgres-provider.d.ts +3 -0
- package/dist/local-postgres-provider.d.ts.map +1 -1
- package/dist/local-postgres-provider.js +92 -10
- package/dist/local-postgres-provider.js.map +1 -1
- package/dist/local-postgres-provider.test.js +24 -1
- package/dist/local-postgres-provider.test.js.map +1 -1
- package/dist/migration-manifest.d.ts +41 -0
- package/dist/migration-manifest.d.ts.map +1 -0
- package/dist/migration-manifest.js +186 -0
- package/dist/migration-manifest.js.map +1 -0
- package/dist/migration-manifest.test.d.ts +2 -0
- package/dist/migration-manifest.test.d.ts.map +1 -0
- package/dist/migration-manifest.test.js +88 -0
- package/dist/migration-manifest.test.js.map +1 -0
- package/dist/migration-runtime.d.ts.map +1 -1
- package/dist/migration-runtime.js +18 -33
- package/dist/migration-runtime.js.map +1 -1
- package/dist/migrations/0143_goal_contract.sql +97 -0
- package/dist/migrations/0144_eminent_umar.sql +16 -0
- package/dist/migrations/0145_public_nehzno.sql +96 -0
- package/dist/migrations/0146_medical_roulette.sql +1 -0
- package/dist/migrations/0147_github_mcp_provider.sql +4 -0
- package/dist/migrations/0148_chat_message_transcript_entries.sql +20 -0
- package/dist/migrations/0149_app_builder_verified_source_ready.sql +3 -0
- package/dist/migrations/0150_rudder_plugins_v1.sql +114 -0
- package/dist/migrations/0151_agent_issue_creation_requests.sql +49 -0
- package/dist/migrations/chat-transcript-storage.test.ts +211 -0
- package/dist/migrations/github-mcp-provider.test.ts +20 -0
- package/dist/migrations/meta/0143_snapshot.json +25318 -0
- package/dist/migrations/meta/0144_snapshot.json +25336 -0
- package/dist/migrations/meta/0145_snapshot.json +27010 -0
- package/dist/migrations/meta/0146_snapshot.json +27016 -0
- package/dist/migrations/meta/0147_snapshot.json +27016 -0
- package/dist/migrations/meta/_journal.json +63 -0
- package/dist/schema/{plugin_jobs.d.ts → agent_issue_creation_requests.d.ts} +134 -142
- package/dist/schema/{plugin_jobs.d.ts.map → agent_issue_creation_requests.d.ts.map} +1 -1
- package/dist/schema/agent_issue_creation_requests.js +40 -0
- package/dist/schema/agent_issue_creation_requests.js.map +1 -0
- package/dist/schema/app_builder_apps.js +1 -1
- package/dist/schema/app_builder_apps.js.map +1 -1
- package/dist/schema/chat_message_transcript_entries.d.ts +82 -0
- package/dist/schema/chat_message_transcript_entries.d.ts.map +1 -0
- package/dist/schema/chat_message_transcript_entries.js +20 -0
- package/dist/schema/chat_message_transcript_entries.js.map +1 -0
- package/dist/schema/goals.d.ts +2110 -0
- package/dist/schema/goals.d.ts.map +1 -1
- package/dist/schema/goals.js +169 -1
- package/dist/schema/goals.js.map +1 -1
- package/dist/schema/index.d.ts +4 -9
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -9
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/issues.d.ts.map +1 -1
- package/dist/schema/issues.js +4 -0
- package/dist/schema/issues.js.map +1 -1
- package/dist/schema/mcp_connections.js +2 -2
- package/dist/schema/mcp_connections.js.map +1 -1
- package/dist/schema/rudder_plugins.d.ts +978 -0
- package/dist/schema/rudder_plugins.d.ts.map +1 -0
- package/dist/schema/rudder_plugins.js +91 -0
- package/dist/schema/rudder_plugins.js.map +1 -0
- package/package.json +2 -2
- package/dist/schema/plugin_config.d.ts +0 -123
- package/dist/schema/plugin_config.d.ts.map +0 -1
- package/dist/schema/plugin_config.js +0 -26
- package/dist/schema/plugin_config.js.map +0 -1
- package/dist/schema/plugin_entities.d.ts +0 -213
- package/dist/schema/plugin_entities.d.ts.map +0 -1
- package/dist/schema/plugin_entities.js +0 -37
- package/dist/schema/plugin_entities.js.map +0 -1
- package/dist/schema/plugin_jobs.js +0 -83
- package/dist/schema/plugin_jobs.js.map +0 -1
- package/dist/schema/plugin_logs.d.ts +0 -125
- package/dist/schema/plugin_logs.d.ts.map +0 -1
- package/dist/schema/plugin_logs.js +0 -29
- package/dist/schema/plugin_logs.js.map +0 -1
- package/dist/schema/plugin_organization_settings.d.ts +0 -158
- package/dist/schema/plugin_organization_settings.d.ts.map +0 -1
- package/dist/schema/plugin_organization_settings.js +0 -34
- package/dist/schema/plugin_organization_settings.js.map +0 -1
- package/dist/schema/plugin_state.d.ts +0 -169
- package/dist/schema/plugin_state.d.ts.map +0 -1
- package/dist/schema/plugin_state.js +0 -68
- package/dist/schema/plugin_state.js.map +0 -1
- package/dist/schema/plugin_webhooks.d.ts +0 -239
- package/dist/schema/plugin_webhooks.d.ts.map +0 -1
- package/dist/schema/plugin_webhooks.js +0 -52
- package/dist/schema/plugin_webhooks.js.map +0 -1
- package/dist/schema/plugins.d.ts +0 -246
- package/dist/schema/plugins.d.ts.map +0 -1
- package/dist/schema/plugins.js +0 -31
- package/dist/schema/plugins.js.map +0 -1
package/dist/client.test.js
CHANGED
|
@@ -7,13 +7,15 @@ import os from "node:os";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import postgres from "postgres";
|
|
9
9
|
import { afterEach, describe, expect, it } from "vitest";
|
|
10
|
-
import {
|
|
10
|
+
import { runDatabaseBackup, runDatabaseRestore } from "./backup-lib.js";
|
|
11
|
+
import { applyPendingMigrations, ensurePostgresDatabase, ensurePostgresRolePassword, inspectMigrations, listLegacyColumnRenames, MIGRATION_ADVISORY_LOCK_NAME, normalizeLegacyColumnNames, reconcilePendingMigrationHistory, validatePostMigrationInvariants, } from "./client.js";
|
|
11
12
|
const tempPaths = [];
|
|
12
13
|
const runningInstances = [];
|
|
13
14
|
async function getEmbeddedPostgresCtor() {
|
|
14
15
|
const mod = await import("embedded-postgres");
|
|
15
16
|
return mod.default;
|
|
16
17
|
}
|
|
18
|
+
const migrationTestTimeout = (timeoutMs) => process.platform === "win32" ? timeoutMs * 4 : timeoutMs;
|
|
17
19
|
async function getAvailablePort() {
|
|
18
20
|
return await new Promise((resolve, reject) => {
|
|
19
21
|
const server = net.createServer();
|
|
@@ -152,20 +154,155 @@ function createCurrentMigrationsFolderThrough(maxIdx) {
|
|
|
152
154
|
return migrationsFolder;
|
|
153
155
|
}
|
|
154
156
|
afterEach(async () => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
const instances = runningInstances.splice(0).reverse();
|
|
158
|
+
const results = await Promise.allSettled(instances.map((instance) => instance.stop()));
|
|
159
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
160
|
+
if (failures.length > 0) {
|
|
161
|
+
throw new AggregateError(failures.map((failure) => failure.reason), `Failed to stop ${failures.length} embedded PostgreSQL test instance(s)`);
|
|
160
162
|
}
|
|
161
163
|
while (tempPaths.length > 0) {
|
|
162
164
|
const tempPath = tempPaths.pop();
|
|
163
165
|
if (!tempPath)
|
|
164
166
|
continue;
|
|
165
|
-
fs.
|
|
167
|
+
await fs.promises.rm(tempPath, {
|
|
168
|
+
recursive: true,
|
|
169
|
+
force: true,
|
|
170
|
+
maxRetries: 20,
|
|
171
|
+
retryDelay: 250,
|
|
172
|
+
});
|
|
166
173
|
}
|
|
167
|
-
});
|
|
174
|
+
}, migrationTestTimeout(30_000));
|
|
168
175
|
describe("applyPendingMigrations", () => {
|
|
176
|
+
it("serializes migration attempts with a database advisory lock", async () => {
|
|
177
|
+
const connectionString = await createTempDatabase();
|
|
178
|
+
const blocker = postgres(connectionString, { max: 1, onnotice: () => { } });
|
|
179
|
+
let lockReleased = false;
|
|
180
|
+
try {
|
|
181
|
+
await blocker `
|
|
182
|
+
SELECT pg_advisory_lock(
|
|
183
|
+
hashtext(current_database()),
|
|
184
|
+
hashtext(${MIGRATION_ADVISORY_LOCK_NAME})
|
|
185
|
+
)
|
|
186
|
+
`;
|
|
187
|
+
const migration = applyPendingMigrations(connectionString);
|
|
188
|
+
const initialOutcome = await Promise.race([
|
|
189
|
+
migration.then(() => "completed"),
|
|
190
|
+
new Promise((resolve) => setTimeout(() => resolve("waiting"), 150)),
|
|
191
|
+
]);
|
|
192
|
+
expect(initialOutcome).toBe("waiting");
|
|
193
|
+
await blocker `
|
|
194
|
+
SELECT pg_advisory_unlock(
|
|
195
|
+
hashtext(current_database()),
|
|
196
|
+
hashtext(${MIGRATION_ADVISORY_LOCK_NAME})
|
|
197
|
+
)
|
|
198
|
+
`;
|
|
199
|
+
lockReleased = true;
|
|
200
|
+
await migration;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
if (!lockReleased) {
|
|
204
|
+
await blocker `
|
|
205
|
+
SELECT pg_advisory_unlock(
|
|
206
|
+
hashtext(current_database()),
|
|
207
|
+
hashtext(${MIGRATION_ADVISORY_LOCK_NAME})
|
|
208
|
+
)
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
await blocker.end();
|
|
212
|
+
}
|
|
213
|
+
}, migrationTestTimeout(30_000));
|
|
214
|
+
it("reports journal, core table, foreign key, and index invariants after migration", async () => {
|
|
215
|
+
const connectionString = await createTempDatabase();
|
|
216
|
+
await applyPendingMigrations(connectionString);
|
|
217
|
+
const report = await validatePostMigrationInvariants(connectionString);
|
|
218
|
+
expect(report).toMatchObject({
|
|
219
|
+
valid: true,
|
|
220
|
+
migrationJournalSchema: "drizzle",
|
|
221
|
+
organizationsTablePresent: true,
|
|
222
|
+
organizationsPrimaryKeyValid: true,
|
|
223
|
+
unvalidatedForeignKeys: [],
|
|
224
|
+
invalidIndexes: [],
|
|
225
|
+
issues: [],
|
|
226
|
+
});
|
|
227
|
+
expect(report.manifestFingerprint).toMatch(/^[0-9a-f]{64}$/);
|
|
228
|
+
expect(report.expectedMigrationCount).toBeGreaterThan(0);
|
|
229
|
+
expect(report.migrationJournalEntryCount).toBeGreaterThan(0);
|
|
230
|
+
expect(report.foreignKeyCount).toBeGreaterThan(0);
|
|
231
|
+
const journalTamperSql = postgres(connectionString, { max: 1, onnotice: () => { } });
|
|
232
|
+
try {
|
|
233
|
+
await journalTamperSql.unsafe(`
|
|
234
|
+
INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at")
|
|
235
|
+
SELECT legacy."hash", latest."created_at" + legacy."offset"
|
|
236
|
+
FROM (VALUES
|
|
237
|
+
('31ba03166f91d84423463bf986219371786078bde80241379cab83d53a4df6d5', 1),
|
|
238
|
+
('e5c12f75cba0ee38da04e5175c762a4b3b5e9e9c523ea97f9956448b44e11570', 2),
|
|
239
|
+
('f48a179c17c3ae9b2b419a3f8d4ee8d78de6e4acec3a077fe0d4bcb9a73d57c6', 3),
|
|
240
|
+
('a531d1d8383becb9090492d1b763aeb11a4c2ade4f325a29500511900b29888d', 4),
|
|
241
|
+
('cbf2988159818d54929cda6119f3ca3b6cd6d265c08fb73c6221198ff99d070e', 5)
|
|
242
|
+
) AS legacy("hash", "offset")
|
|
243
|
+
CROSS JOIN (
|
|
244
|
+
SELECT COALESCE(MAX("created_at"), 0) AS "created_at"
|
|
245
|
+
FROM "drizzle"."__drizzle_migrations"
|
|
246
|
+
) AS latest
|
|
247
|
+
`);
|
|
248
|
+
const legacyReport = await validatePostMigrationInvariants(connectionString);
|
|
249
|
+
expect(legacyReport.valid).toBe(true);
|
|
250
|
+
expect(legacyReport.issues).toEqual([]);
|
|
251
|
+
await journalTamperSql.unsafe(`
|
|
252
|
+
UPDATE "drizzle"."__drizzle_migrations"
|
|
253
|
+
SET "hash" = repeat('0', 64)
|
|
254
|
+
WHERE "id" = (SELECT min("id") FROM "drizzle"."__drizzle_migrations")
|
|
255
|
+
`);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
await journalTamperSql.end();
|
|
259
|
+
}
|
|
260
|
+
const tamperedReport = await validatePostMigrationInvariants(connectionString);
|
|
261
|
+
expect(tamperedReport.valid).toBe(false);
|
|
262
|
+
expect(tamperedReport.issues).toContainEqual(expect.objectContaining({
|
|
263
|
+
code: "migration_journal_invalid",
|
|
264
|
+
}));
|
|
265
|
+
const unknownLegacyConnectionString = await createTempDatabase();
|
|
266
|
+
await applyPendingMigrations(unknownLegacyConnectionString);
|
|
267
|
+
const legacyJournalTamperSql = postgres(unknownLegacyConnectionString, { max: 1, onnotice: () => { } });
|
|
268
|
+
try {
|
|
269
|
+
await legacyJournalTamperSql.unsafe(`
|
|
270
|
+
INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at")
|
|
271
|
+
SELECT 'legacy-corrupt', COALESCE(MAX("created_at"), 0) + 1
|
|
272
|
+
FROM "drizzle"."__drizzle_migrations"
|
|
273
|
+
`);
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
await legacyJournalTamperSql.end();
|
|
277
|
+
}
|
|
278
|
+
const unknownLegacyReport = await validatePostMigrationInvariants(unknownLegacyConnectionString);
|
|
279
|
+
expect(unknownLegacyReport.valid).toBe(false);
|
|
280
|
+
expect(unknownLegacyReport.issues).toContainEqual(expect.objectContaining({
|
|
281
|
+
code: "migration_journal_invalid",
|
|
282
|
+
message: expect.stringContaining("does not match any current migration manifest entry"),
|
|
283
|
+
}));
|
|
284
|
+
const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
|
|
285
|
+
try {
|
|
286
|
+
await sql.unsafe(`CREATE TABLE "migration_invariant_parent" ("id" integer PRIMARY KEY)`);
|
|
287
|
+
await sql.unsafe(`CREATE TABLE "migration_invariant_child" ("parent_id" integer)`);
|
|
288
|
+
await sql.unsafe(`
|
|
289
|
+
ALTER TABLE "migration_invariant_child"
|
|
290
|
+
ADD CONSTRAINT "migration_invariant_child_parent_fk"
|
|
291
|
+
FOREIGN KEY ("parent_id") REFERENCES "migration_invariant_parent"("id") NOT VALID
|
|
292
|
+
`);
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
await sql.end();
|
|
296
|
+
}
|
|
297
|
+
const invalidReport = await validatePostMigrationInvariants(connectionString);
|
|
298
|
+
expect(invalidReport.valid).toBe(false);
|
|
299
|
+
expect(invalidReport.unvalidatedForeignKeys).toEqual([
|
|
300
|
+
"migration_invariant_child.migration_invariant_child_parent_fk",
|
|
301
|
+
]);
|
|
302
|
+
expect(invalidReport.issues).toContainEqual(expect.objectContaining({
|
|
303
|
+
code: "foreign_keys_not_validated",
|
|
304
|
+
}));
|
|
305
|
+
}, migrationTestTimeout(30_000));
|
|
169
306
|
it("normalizes legacy embedded cluster passwords back to rudder", async () => {
|
|
170
307
|
const legacyConnectionString = await createTempDatabaseWithPassword("password");
|
|
171
308
|
const legacyUrl = new URL(legacyConnectionString);
|
|
@@ -190,7 +327,7 @@ describe("applyPendingMigrations", () => {
|
|
|
190
327
|
finally {
|
|
191
328
|
await sql.end();
|
|
192
329
|
}
|
|
193
|
-
}, 20_000);
|
|
330
|
+
}, migrationTestTimeout(20_000));
|
|
194
331
|
it("rebuilds schema when migration journal exists but core tables are missing", async () => {
|
|
195
332
|
const connectionString = await createTempDatabase();
|
|
196
333
|
await applyPendingMigrations(connectionString);
|
|
@@ -223,7 +360,7 @@ describe("applyPendingMigrations", () => {
|
|
|
223
360
|
finally {
|
|
224
361
|
await verifySql.end();
|
|
225
362
|
}
|
|
226
|
-
}, 20_000);
|
|
363
|
+
}, migrationTestTimeout(20_000));
|
|
227
364
|
it("applies the organization schema on a fresh database", async () => {
|
|
228
365
|
const connectionString = await createTempDatabase();
|
|
229
366
|
await applyPendingMigrations(connectionString);
|
|
@@ -246,7 +383,184 @@ describe("applyPendingMigrations", () => {
|
|
|
246
383
|
finally {
|
|
247
384
|
await verifySql.end();
|
|
248
385
|
}
|
|
249
|
-
}, 20_000);
|
|
386
|
+
}, migrationTestTimeout(20_000));
|
|
387
|
+
it("preserves dormant legacy plugin storage across restart, upgrade, and backup restore", async () => {
|
|
388
|
+
const connectionString = await createTempDatabase();
|
|
389
|
+
await applyPendingMigrations(connectionString);
|
|
390
|
+
const pluginId = "00000000-0000-0000-0000-000000000202";
|
|
391
|
+
const jobId = "00000000-0000-0000-0000-000000000206";
|
|
392
|
+
const legacyTables = [
|
|
393
|
+
"plugin_config",
|
|
394
|
+
"plugin_entities",
|
|
395
|
+
"plugin_job_runs",
|
|
396
|
+
"plugin_jobs",
|
|
397
|
+
"plugin_logs",
|
|
398
|
+
"plugin_organization_settings",
|
|
399
|
+
"plugin_state",
|
|
400
|
+
"plugin_webhook_deliveries",
|
|
401
|
+
"plugins",
|
|
402
|
+
];
|
|
403
|
+
const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
|
|
404
|
+
try {
|
|
405
|
+
await sql.unsafe(`
|
|
406
|
+
INSERT INTO "organizations" ("id", "url_key", "name", "issue_prefix")
|
|
407
|
+
VALUES ('00000000-0000-0000-0000-000000000201', 'legacy-plugin-storage', 'Legacy Plugin Storage', 'LPS')
|
|
408
|
+
`);
|
|
409
|
+
await sql.unsafe(`
|
|
410
|
+
INSERT INTO "plugins" (
|
|
411
|
+
"id", "plugin_key", "package_name", "version", "categories",
|
|
412
|
+
"manifest_json", "status", "package_path"
|
|
413
|
+
) VALUES (
|
|
414
|
+
'${pluginId}', 'legacy-linear', '@legacy/linear', '1.2.3',
|
|
415
|
+
'["integration"]'::jsonb, '{"name":"Linear","apiVersion":1}'::jsonb,
|
|
416
|
+
'active', '/legacy/plugins/linear'
|
|
417
|
+
)
|
|
418
|
+
`);
|
|
419
|
+
await sql.unsafe(`
|
|
420
|
+
INSERT INTO "plugin_config" ("id", "plugin_id", "config_json", "last_error")
|
|
421
|
+
VALUES (
|
|
422
|
+
'00000000-0000-0000-0000-000000000203', '${pluginId}',
|
|
423
|
+
'{"token":"preserved","team":"ENG"}'::jsonb, 'historical config error'
|
|
424
|
+
)
|
|
425
|
+
`);
|
|
426
|
+
await sql.unsafe(`
|
|
427
|
+
INSERT INTO "plugin_state" (
|
|
428
|
+
"id", "plugin_id", "scope_kind", "scope_id", "namespace", "state_key", "value_json"
|
|
429
|
+
) VALUES (
|
|
430
|
+
'00000000-0000-0000-0000-000000000204', '${pluginId}',
|
|
431
|
+
'organization', '00000000-0000-0000-0000-000000000201',
|
|
432
|
+
'sync', 'cursor', '{"after":"LIN-42"}'::jsonb
|
|
433
|
+
)
|
|
434
|
+
`);
|
|
435
|
+
await sql.unsafe(`
|
|
436
|
+
INSERT INTO "plugin_entities" (
|
|
437
|
+
"id", "plugin_id", "entity_type", "scope_kind", "scope_id",
|
|
438
|
+
"external_id", "title", "status", "data"
|
|
439
|
+
) VALUES (
|
|
440
|
+
'00000000-0000-0000-0000-000000000205', '${pluginId}', 'issue',
|
|
441
|
+
'organization', '00000000-0000-0000-0000-000000000201',
|
|
442
|
+
'LIN-42', 'Preserved issue', 'closed', '{"priority":2}'::jsonb
|
|
443
|
+
)
|
|
444
|
+
`);
|
|
445
|
+
await sql.unsafe(`
|
|
446
|
+
INSERT INTO "plugin_jobs" ("id", "plugin_id", "job_key", "schedule", "status")
|
|
447
|
+
VALUES ('${jobId}', '${pluginId}', 'sync', '0 * * * *', 'paused')
|
|
448
|
+
`);
|
|
449
|
+
await sql.unsafe(`
|
|
450
|
+
INSERT INTO "plugin_job_runs" (
|
|
451
|
+
"id", "job_id", "plugin_id", "trigger", "status", "duration_ms", "logs"
|
|
452
|
+
) VALUES (
|
|
453
|
+
'00000000-0000-0000-0000-000000000207', '${jobId}', '${pluginId}',
|
|
454
|
+
'scheduled', 'succeeded', 1250, '["legacy run complete"]'::jsonb
|
|
455
|
+
)
|
|
456
|
+
`);
|
|
457
|
+
await sql.unsafe(`
|
|
458
|
+
INSERT INTO "plugin_webhook_deliveries" (
|
|
459
|
+
"id", "plugin_id", "webhook_key", "external_id", "status",
|
|
460
|
+
"duration_ms", "payload", "headers"
|
|
461
|
+
) VALUES (
|
|
462
|
+
'00000000-0000-0000-0000-000000000208', '${pluginId}', 'linear-event',
|
|
463
|
+
'delivery-42', 'succeeded', 25, '{"type":"Issue"}'::jsonb,
|
|
464
|
+
'{"x-delivery-id":"delivery-42"}'::jsonb
|
|
465
|
+
)
|
|
466
|
+
`);
|
|
467
|
+
await sql.unsafe(`
|
|
468
|
+
INSERT INTO "plugin_organization_settings" (
|
|
469
|
+
"id", "org_id", "plugin_id", "enabled", "settings_json", "last_error"
|
|
470
|
+
) VALUES (
|
|
471
|
+
'00000000-0000-0000-0000-000000000209',
|
|
472
|
+
'00000000-0000-0000-0000-000000000201', '${pluginId}', false,
|
|
473
|
+
'{"project":"ENG"}'::jsonb, 'disabled before removal'
|
|
474
|
+
)
|
|
475
|
+
`);
|
|
476
|
+
await sql.unsafe(`
|
|
477
|
+
INSERT INTO "plugin_logs" ("id", "plugin_id", "level", "message", "meta")
|
|
478
|
+
VALUES (
|
|
479
|
+
'00000000-0000-0000-0000-000000000210', '${pluginId}', 'warn',
|
|
480
|
+
'legacy log preserved', '{"attempt":3}'::jsonb
|
|
481
|
+
)
|
|
482
|
+
`);
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
await sql.end();
|
|
486
|
+
}
|
|
487
|
+
const readSnapshot = async (databaseUrl) => {
|
|
488
|
+
const readSql = postgres(databaseUrl, { max: 1, onnotice: () => { } });
|
|
489
|
+
try {
|
|
490
|
+
const tables = await readSql.unsafe(`
|
|
491
|
+
SELECT table_name
|
|
492
|
+
FROM information_schema.tables
|
|
493
|
+
WHERE table_schema = 'public'
|
|
494
|
+
AND table_name LIKE 'plugin%'
|
|
495
|
+
ORDER BY table_name
|
|
496
|
+
`);
|
|
497
|
+
const [data] = await readSql.unsafe(`
|
|
498
|
+
SELECT
|
|
499
|
+
jsonb_build_object(
|
|
500
|
+
'plugin_config', (SELECT count(*)::int FROM "plugin_config" WHERE "plugin_id" = '${pluginId}'),
|
|
501
|
+
'plugin_entities', (SELECT count(*)::int FROM "plugin_entities" WHERE "plugin_id" = '${pluginId}'),
|
|
502
|
+
'plugin_job_runs', (SELECT count(*)::int FROM "plugin_job_runs" WHERE "plugin_id" = '${pluginId}'),
|
|
503
|
+
'plugin_jobs', (SELECT count(*)::int FROM "plugin_jobs" WHERE "plugin_id" = '${pluginId}'),
|
|
504
|
+
'plugin_logs', (SELECT count(*)::int FROM "plugin_logs" WHERE "plugin_id" = '${pluginId}'),
|
|
505
|
+
'plugin_organization_settings', (SELECT count(*)::int FROM "plugin_organization_settings" WHERE "plugin_id" = '${pluginId}'),
|
|
506
|
+
'plugin_state', (SELECT count(*)::int FROM "plugin_state" WHERE "plugin_id" = '${pluginId}'),
|
|
507
|
+
'plugin_webhook_deliveries', (SELECT count(*)::int FROM "plugin_webhook_deliveries" WHERE "plugin_id" = '${pluginId}'),
|
|
508
|
+
'plugins', (SELECT count(*)::int FROM "plugins" WHERE "id" = '${pluginId}')
|
|
509
|
+
) AS counts,
|
|
510
|
+
(SELECT "manifest_json" FROM "plugins" WHERE "id" = '${pluginId}') AS manifest,
|
|
511
|
+
(SELECT "config_json" FROM "plugin_config" WHERE "plugin_id" = '${pluginId}') AS config,
|
|
512
|
+
(SELECT "value_json" FROM "plugin_state" WHERE "plugin_id" = '${pluginId}') AS state,
|
|
513
|
+
(SELECT "data" FROM "plugin_entities" WHERE "plugin_id" = '${pluginId}') AS entity,
|
|
514
|
+
(SELECT jsonb_build_object('schedule', "schedule", 'status', "status") FROM "plugin_jobs" WHERE "id" = '${jobId}') AS job,
|
|
515
|
+
(SELECT "logs" FROM "plugin_job_runs" WHERE "plugin_id" = '${pluginId}') AS job_logs,
|
|
516
|
+
(SELECT jsonb_build_object('payload', "payload", 'headers', "headers") FROM "plugin_webhook_deliveries" WHERE "plugin_id" = '${pluginId}') AS webhook,
|
|
517
|
+
(SELECT "settings_json" FROM "plugin_organization_settings" WHERE "plugin_id" = '${pluginId}') AS org_settings,
|
|
518
|
+
(SELECT jsonb_build_object('message', "message", 'meta', "meta") FROM "plugin_logs" WHERE "plugin_id" = '${pluginId}') AS log_entry
|
|
519
|
+
`);
|
|
520
|
+
const foreignKeys = await readSql.unsafe(`
|
|
521
|
+
SELECT conname
|
|
522
|
+
FROM pg_constraint
|
|
523
|
+
WHERE contype = 'f'
|
|
524
|
+
AND conrelid::regclass::text LIKE 'plugin%'
|
|
525
|
+
ORDER BY conname
|
|
526
|
+
`);
|
|
527
|
+
return {
|
|
528
|
+
tables: tables.map((row) => row.table_name),
|
|
529
|
+
data,
|
|
530
|
+
foreignKeys: foreignKeys.map((row) => row.conname),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
await readSql.end();
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
const beforeRestart = await readSnapshot(connectionString);
|
|
538
|
+
expect(beforeRestart.tables).toEqual(expect.arrayContaining(legacyTables));
|
|
539
|
+
expect(beforeRestart.data?.counts).toEqual(Object.fromEntries(legacyTables.map((table) => [table, 1])));
|
|
540
|
+
expect(beforeRestart.foreignKeys.length).toBeGreaterThanOrEqual(10);
|
|
541
|
+
const instance = runningInstances.at(-1);
|
|
542
|
+
expect(instance).toBeDefined();
|
|
543
|
+
await instance.stop();
|
|
544
|
+
await instance.start();
|
|
545
|
+
await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
|
|
546
|
+
expect(await readSnapshot(connectionString)).toEqual(beforeRestart);
|
|
547
|
+
const backupDir = fs.mkdtempSync(path.join(os.tmpdir(), "rudder-legacy-plugin-backup-"));
|
|
548
|
+
tempPaths.push(backupDir);
|
|
549
|
+
const backup = await runDatabaseBackup({
|
|
550
|
+
connectionString,
|
|
551
|
+
backupDir,
|
|
552
|
+
retentionDays: 1,
|
|
553
|
+
includeMigrationJournal: true,
|
|
554
|
+
});
|
|
555
|
+
const restoreAdminUrl = new URL(connectionString);
|
|
556
|
+
restoreAdminUrl.pathname = "/postgres";
|
|
557
|
+
await ensurePostgresDatabase(restoreAdminUrl.toString(), "rudder_restore");
|
|
558
|
+
const restoreUrl = new URL(connectionString);
|
|
559
|
+
restoreUrl.pathname = "/rudder_restore";
|
|
560
|
+
await runDatabaseRestore({ connectionString: restoreUrl.toString(), backupFile: backup.backupFile });
|
|
561
|
+
expect((await inspectMigrations(restoreUrl.toString())).status).toBe("upToDate");
|
|
562
|
+
expect(await readSnapshot(restoreUrl.toString())).toEqual(beforeRestart);
|
|
563
|
+
}, 60_000);
|
|
250
564
|
it("completes migration 0100 when an older version already created the alias table", async () => {
|
|
251
565
|
const connectionString = await createTempDatabase();
|
|
252
566
|
await applyPendingMigrations(connectionString);
|
|
@@ -333,7 +647,7 @@ describe("applyPendingMigrations", () => {
|
|
|
333
647
|
finally {
|
|
334
648
|
await verifySql.end();
|
|
335
649
|
}
|
|
336
|
-
}, 20_000);
|
|
650
|
+
}, migrationTestTimeout(20_000));
|
|
337
651
|
it("reapplies consolidated migration 0102 over the legacy 0102-0104 feature shape", async () => {
|
|
338
652
|
const connectionString = await createTempDatabase();
|
|
339
653
|
const { manifest, migrationsFolder } = createLegacyTerminalMigrationsFolder();
|
|
@@ -429,6 +743,15 @@ describe("applyPendingMigrations", () => {
|
|
|
429
743
|
"0140_faulty_kitty_pryde.sql",
|
|
430
744
|
"0141_product_analytics_revision_origin.sql",
|
|
431
745
|
"0142_product_analytics_quality_counters.sql",
|
|
746
|
+
"0143_goal_contract.sql",
|
|
747
|
+
"0144_eminent_umar.sql",
|
|
748
|
+
"0145_public_nehzno.sql",
|
|
749
|
+
"0146_medical_roulette.sql",
|
|
750
|
+
"0147_github_mcp_provider.sql",
|
|
751
|
+
"0148_chat_message_transcript_entries.sql",
|
|
752
|
+
"0149_app_builder_verified_source_ready.sql",
|
|
753
|
+
"0150_rudder_plugins_v1.sql",
|
|
754
|
+
"0151_agent_issue_creation_requests.sql",
|
|
432
755
|
],
|
|
433
756
|
reason: "pending-migrations",
|
|
434
757
|
});
|
|
@@ -488,7 +811,7 @@ describe("applyPendingMigrations", () => {
|
|
|
488
811
|
finally {
|
|
489
812
|
await verifySql.end();
|
|
490
813
|
}
|
|
491
|
-
},
|
|
814
|
+
}, migrationTestTimeout(40_000));
|
|
492
815
|
it("upgrades databases that applied the legacy 0102 chat runtime controls migration", async () => {
|
|
493
816
|
const connectionString = await createTempDatabase();
|
|
494
817
|
const { historicalHash, migrationsFolder } = createLegacyChatRuntimeControlsMigrationsFolder();
|
|
@@ -586,6 +909,15 @@ describe("applyPendingMigrations", () => {
|
|
|
586
909
|
"0140_faulty_kitty_pryde.sql",
|
|
587
910
|
"0141_product_analytics_revision_origin.sql",
|
|
588
911
|
"0142_product_analytics_quality_counters.sql",
|
|
912
|
+
"0143_goal_contract.sql",
|
|
913
|
+
"0144_eminent_umar.sql",
|
|
914
|
+
"0145_public_nehzno.sql",
|
|
915
|
+
"0146_medical_roulette.sql",
|
|
916
|
+
"0147_github_mcp_provider.sql",
|
|
917
|
+
"0148_chat_message_transcript_entries.sql",
|
|
918
|
+
"0149_app_builder_verified_source_ready.sql",
|
|
919
|
+
"0150_rudder_plugins_v1.sql",
|
|
920
|
+
"0151_agent_issue_creation_requests.sql",
|
|
589
921
|
],
|
|
590
922
|
reason: "pending-migrations",
|
|
591
923
|
});
|
|
@@ -620,7 +952,7 @@ describe("applyPendingMigrations", () => {
|
|
|
620
952
|
finally {
|
|
621
953
|
await verifySql.end();
|
|
622
954
|
}
|
|
623
|
-
},
|
|
955
|
+
}, migrationTestTimeout(40_000));
|
|
624
956
|
it("upgrades 0102 heartbeat runs with durable session lineage columns", async () => {
|
|
625
957
|
const connectionString = await createTempDatabase();
|
|
626
958
|
const migrationsThrough0102 = createCurrentMigrationsFolderThrough(102);
|
|
@@ -683,7 +1015,7 @@ describe("applyPendingMigrations", () => {
|
|
|
683
1015
|
finally {
|
|
684
1016
|
await verifySql.end();
|
|
685
1017
|
}
|
|
686
|
-
},
|
|
1018
|
+
}, migrationTestTimeout(40_000));
|
|
687
1019
|
it("reconciles legacy chat control states before creating active-generation constraints", async () => {
|
|
688
1020
|
const connectionString = await createTempDatabase();
|
|
689
1021
|
const migrationsThrough0103 = createCurrentMigrationsFolderThrough(103);
|
|
@@ -947,7 +1279,7 @@ describe("applyPendingMigrations", () => {
|
|
|
947
1279
|
finally {
|
|
948
1280
|
await verifySql.end();
|
|
949
1281
|
}
|
|
950
|
-
}, 20_000);
|
|
1282
|
+
}, migrationTestTimeout(20_000));
|
|
951
1283
|
it("rolls back migration 0100 when a legacy alias conflicts with another organization route", async () => {
|
|
952
1284
|
const connectionString = await createTempDatabase();
|
|
953
1285
|
await applyPendingMigrations(connectionString);
|
|
@@ -1021,7 +1353,7 @@ describe("applyPendingMigrations", () => {
|
|
|
1021
1353
|
finally {
|
|
1022
1354
|
await verifySql.end();
|
|
1023
1355
|
}
|
|
1024
|
-
}, 20_000);
|
|
1356
|
+
}, migrationTestTimeout(20_000));
|
|
1025
1357
|
it("replays migration 0044 safely when its schema changes already exist", async () => {
|
|
1026
1358
|
const connectionString = await createTempDatabase();
|
|
1027
1359
|
await applyPendingMigrations(connectionString);
|
|
@@ -1050,7 +1382,7 @@ describe("applyPendingMigrations", () => {
|
|
|
1050
1382
|
await applyPendingMigrations(connectionString);
|
|
1051
1383
|
const finalState = await inspectMigrations(connectionString);
|
|
1052
1384
|
expect(finalState.status).toBe("upToDate");
|
|
1053
|
-
}, 20_000);
|
|
1385
|
+
}, migrationTestTimeout(20_000));
|
|
1054
1386
|
it("replays migration 0063 safely when the experimental settings column is already absent", async () => {
|
|
1055
1387
|
const connectionString = await createTempDatabase();
|
|
1056
1388
|
await applyPendingMigrations(connectionString);
|
|
@@ -1079,7 +1411,7 @@ describe("applyPendingMigrations", () => {
|
|
|
1079
1411
|
await applyPendingMigrations(connectionString);
|
|
1080
1412
|
const finalState = await inspectMigrations(connectionString);
|
|
1081
1413
|
expect(finalState.status).toBe("upToDate");
|
|
1082
|
-
}, 20_000);
|
|
1414
|
+
}, migrationTestTimeout(20_000));
|
|
1083
1415
|
it("repairs missing migration history when the schema changes are still directly verifiable", async () => {
|
|
1084
1416
|
const connectionString = await createTempDatabase();
|
|
1085
1417
|
await applyPendingMigrations(connectionString);
|
|
@@ -1113,7 +1445,7 @@ describe("applyPendingMigrations", () => {
|
|
|
1113
1445
|
await applyPendingMigrations(connectionString);
|
|
1114
1446
|
const finalState = await inspectMigrations(connectionString);
|
|
1115
1447
|
expect(finalState.status).toBe("upToDate");
|
|
1116
|
-
}, 20_000);
|
|
1448
|
+
}, migrationTestTimeout(20_000));
|
|
1117
1449
|
it("normalizes legacy adapter column names before using an up-to-date migration journal", async () => {
|
|
1118
1450
|
const connectionString = await createTempDatabase();
|
|
1119
1451
|
await applyPendingMigrations(connectionString);
|
|
@@ -1132,6 +1464,15 @@ describe("applyPendingMigrations", () => {
|
|
|
1132
1464
|
}
|
|
1133
1465
|
const driftedState = await inspectMigrations(connectionString);
|
|
1134
1466
|
expect(driftedState.status).toBe("upToDate");
|
|
1467
|
+
await expect(listLegacyColumnRenames(connectionString)).resolves.toEqual([
|
|
1468
|
+
"agents.adapter_type->agent_runtime_type",
|
|
1469
|
+
"agents.adapter_config->agent_runtime_config",
|
|
1470
|
+
"agent_runtime_state.adapter_type->agent_runtime_type",
|
|
1471
|
+
"agent_task_sessions.adapter_type->agent_runtime_type",
|
|
1472
|
+
"join_requests.adapter_type->agent_runtime_type",
|
|
1473
|
+
"finance_events.execution_adapter_type->execution_agent_runtime_type",
|
|
1474
|
+
"issues.assignee_adapter_overrides->assignee_agent_runtime_overrides",
|
|
1475
|
+
]);
|
|
1135
1476
|
await applyPendingMigrations(connectionString);
|
|
1136
1477
|
const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
|
|
1137
1478
|
try {
|
|
@@ -1164,7 +1505,7 @@ describe("applyPendingMigrations", () => {
|
|
|
1164
1505
|
finally {
|
|
1165
1506
|
await verifySql.end();
|
|
1166
1507
|
}
|
|
1167
|
-
}, 20_000);
|
|
1508
|
+
}, migrationTestTimeout(20_000));
|
|
1168
1509
|
it("keeps a missing migration pending until the schema change really exists", async () => {
|
|
1169
1510
|
const connectionString = await createTempDatabase();
|
|
1170
1511
|
await applyPendingMigrations(connectionString);
|
|
@@ -1209,7 +1550,59 @@ describe("applyPendingMigrations", () => {
|
|
|
1209
1550
|
finally {
|
|
1210
1551
|
await verifySql.end();
|
|
1211
1552
|
}
|
|
1212
|
-
}, 20_000);
|
|
1553
|
+
}, migrationTestTimeout(20_000));
|
|
1554
|
+
it("rolls back every legacy column rename when a later rename fails", async () => {
|
|
1555
|
+
const connectionString = await createTempDatabase();
|
|
1556
|
+
await applyPendingMigrations(connectionString);
|
|
1557
|
+
const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
|
|
1558
|
+
try {
|
|
1559
|
+
await sql.unsafe(`ALTER TABLE "agents" RENAME COLUMN "agent_runtime_type" TO "adapter_type"`);
|
|
1560
|
+
await sql.unsafe(`ALTER TABLE "agents" RENAME COLUMN "agent_runtime_config" TO "adapter_config"`);
|
|
1561
|
+
await sql.unsafe(`ALTER TABLE "agent_runtime_state" RENAME COLUMN "agent_runtime_type" TO "adapter_type"`);
|
|
1562
|
+
await sql.unsafe(`
|
|
1563
|
+
CREATE FUNCTION fail_legacy_column_rename() RETURNS event_trigger
|
|
1564
|
+
LANGUAGE plpgsql AS $$
|
|
1565
|
+
BEGIN
|
|
1566
|
+
IF current_query() LIKE '%"agent_runtime_state"%' THEN
|
|
1567
|
+
RAISE EXCEPTION 'intentional legacy rename failure';
|
|
1568
|
+
END IF;
|
|
1569
|
+
END;
|
|
1570
|
+
$$
|
|
1571
|
+
`);
|
|
1572
|
+
await sql.unsafe(`
|
|
1573
|
+
CREATE EVENT TRIGGER fail_legacy_column_rename_trigger
|
|
1574
|
+
ON ddl_command_start
|
|
1575
|
+
EXECUTE FUNCTION fail_legacy_column_rename()
|
|
1576
|
+
`);
|
|
1577
|
+
}
|
|
1578
|
+
finally {
|
|
1579
|
+
await sql.end();
|
|
1580
|
+
}
|
|
1581
|
+
await expect(normalizeLegacyColumnNames(connectionString)).rejects.toThrow("intentional legacy rename failure");
|
|
1582
|
+
const cleanupSql = postgres(connectionString, { max: 1, onnotice: () => { } });
|
|
1583
|
+
try {
|
|
1584
|
+
await cleanupSql.unsafe(`DROP EVENT TRIGGER fail_legacy_column_rename_trigger`);
|
|
1585
|
+
await cleanupSql.unsafe(`DROP FUNCTION fail_legacy_column_rename()`);
|
|
1586
|
+
const columns = await cleanupSql.unsafe(`
|
|
1587
|
+
SELECT table_name, column_name
|
|
1588
|
+
FROM information_schema.columns
|
|
1589
|
+
WHERE table_schema = 'public'
|
|
1590
|
+
AND table_name IN ('agents', 'agent_runtime_state')
|
|
1591
|
+
AND column_name IN (
|
|
1592
|
+
'agent_runtime_type', 'agent_runtime_config', 'adapter_type', 'adapter_config'
|
|
1593
|
+
)
|
|
1594
|
+
ORDER BY table_name, column_name
|
|
1595
|
+
`);
|
|
1596
|
+
expect(columns).toEqual([
|
|
1597
|
+
{ table_name: "agent_runtime_state", column_name: "adapter_type" },
|
|
1598
|
+
{ table_name: "agents", column_name: "adapter_config" },
|
|
1599
|
+
{ table_name: "agents", column_name: "adapter_type" },
|
|
1600
|
+
]);
|
|
1601
|
+
}
|
|
1602
|
+
finally {
|
|
1603
|
+
await cleanupSql.end();
|
|
1604
|
+
}
|
|
1605
|
+
}, migrationTestTimeout(20_000));
|
|
1213
1606
|
it("enforces a unique board_api_keys.key_hash after migration 0044", async () => {
|
|
1214
1607
|
const connectionString = await createTempDatabase();
|
|
1215
1608
|
await applyPendingMigrations(connectionString);
|
|
@@ -1231,6 +1624,6 @@ describe("applyPendingMigrations", () => {
|
|
|
1231
1624
|
finally {
|
|
1232
1625
|
await sql.end();
|
|
1233
1626
|
}
|
|
1234
|
-
}, 20_000);
|
|
1627
|
+
}, migrationTestTimeout(20_000));
|
|
1235
1628
|
});
|
|
1236
1629
|
//# sourceMappingURL=client.test.js.map
|