@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.
Files changed (113) hide show
  1. package/dist/backup-lib.d.ts.map +1 -1
  2. package/dist/backup-lib.js +60 -36
  3. package/dist/backup-lib.js.map +1 -1
  4. package/dist/backup-lib.test.js +122 -2
  5. package/dist/backup-lib.test.js.map +1 -1
  6. package/dist/client.d.ts +25 -1
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +309 -20
  9. package/dist/client.js.map +1 -1
  10. package/dist/client.test.js +416 -23
  11. package/dist/client.test.js.map +1 -1
  12. package/dist/embedded-postgres-recovery.d.ts +11 -0
  13. package/dist/embedded-postgres-recovery.d.ts.map +1 -1
  14. package/dist/embedded-postgres-recovery.js +50 -11
  15. package/dist/embedded-postgres-recovery.js.map +1 -1
  16. package/dist/embedded-postgres-recovery.test.js +50 -2
  17. package/dist/embedded-postgres-recovery.test.js.map +1 -1
  18. package/dist/index.d.ts +3 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/local-postgres-provider.d.ts +3 -0
  23. package/dist/local-postgres-provider.d.ts.map +1 -1
  24. package/dist/local-postgres-provider.js +92 -10
  25. package/dist/local-postgres-provider.js.map +1 -1
  26. package/dist/local-postgres-provider.test.js +24 -1
  27. package/dist/local-postgres-provider.test.js.map +1 -1
  28. package/dist/migration-manifest.d.ts +41 -0
  29. package/dist/migration-manifest.d.ts.map +1 -0
  30. package/dist/migration-manifest.js +186 -0
  31. package/dist/migration-manifest.js.map +1 -0
  32. package/dist/migration-manifest.test.d.ts +2 -0
  33. package/dist/migration-manifest.test.d.ts.map +1 -0
  34. package/dist/migration-manifest.test.js +88 -0
  35. package/dist/migration-manifest.test.js.map +1 -0
  36. package/dist/migration-runtime.d.ts.map +1 -1
  37. package/dist/migration-runtime.js +18 -33
  38. package/dist/migration-runtime.js.map +1 -1
  39. package/dist/migrations/0143_goal_contract.sql +97 -0
  40. package/dist/migrations/0144_eminent_umar.sql +16 -0
  41. package/dist/migrations/0145_public_nehzno.sql +96 -0
  42. package/dist/migrations/0146_medical_roulette.sql +1 -0
  43. package/dist/migrations/0147_github_mcp_provider.sql +4 -0
  44. package/dist/migrations/0148_chat_message_transcript_entries.sql +20 -0
  45. package/dist/migrations/0149_app_builder_verified_source_ready.sql +3 -0
  46. package/dist/migrations/0150_rudder_plugins_v1.sql +114 -0
  47. package/dist/migrations/0151_agent_issue_creation_requests.sql +49 -0
  48. package/dist/migrations/chat-transcript-storage.test.ts +211 -0
  49. package/dist/migrations/github-mcp-provider.test.ts +20 -0
  50. package/dist/migrations/meta/0143_snapshot.json +25318 -0
  51. package/dist/migrations/meta/0144_snapshot.json +25336 -0
  52. package/dist/migrations/meta/0145_snapshot.json +27010 -0
  53. package/dist/migrations/meta/0146_snapshot.json +27016 -0
  54. package/dist/migrations/meta/0147_snapshot.json +27016 -0
  55. package/dist/migrations/meta/_journal.json +63 -0
  56. package/dist/schema/{plugin_jobs.d.ts → agent_issue_creation_requests.d.ts} +134 -142
  57. package/dist/schema/{plugin_jobs.d.ts.map → agent_issue_creation_requests.d.ts.map} +1 -1
  58. package/dist/schema/agent_issue_creation_requests.js +40 -0
  59. package/dist/schema/agent_issue_creation_requests.js.map +1 -0
  60. package/dist/schema/app_builder_apps.js +1 -1
  61. package/dist/schema/app_builder_apps.js.map +1 -1
  62. package/dist/schema/chat_message_transcript_entries.d.ts +82 -0
  63. package/dist/schema/chat_message_transcript_entries.d.ts.map +1 -0
  64. package/dist/schema/chat_message_transcript_entries.js +20 -0
  65. package/dist/schema/chat_message_transcript_entries.js.map +1 -0
  66. package/dist/schema/goals.d.ts +2110 -0
  67. package/dist/schema/goals.d.ts.map +1 -1
  68. package/dist/schema/goals.js +169 -1
  69. package/dist/schema/goals.js.map +1 -1
  70. package/dist/schema/index.d.ts +4 -9
  71. package/dist/schema/index.d.ts.map +1 -1
  72. package/dist/schema/index.js +4 -9
  73. package/dist/schema/index.js.map +1 -1
  74. package/dist/schema/issues.d.ts.map +1 -1
  75. package/dist/schema/issues.js +4 -0
  76. package/dist/schema/issues.js.map +1 -1
  77. package/dist/schema/mcp_connections.js +2 -2
  78. package/dist/schema/mcp_connections.js.map +1 -1
  79. package/dist/schema/rudder_plugins.d.ts +978 -0
  80. package/dist/schema/rudder_plugins.d.ts.map +1 -0
  81. package/dist/schema/rudder_plugins.js +91 -0
  82. package/dist/schema/rudder_plugins.js.map +1 -0
  83. package/package.json +2 -2
  84. package/dist/schema/plugin_config.d.ts +0 -123
  85. package/dist/schema/plugin_config.d.ts.map +0 -1
  86. package/dist/schema/plugin_config.js +0 -26
  87. package/dist/schema/plugin_config.js.map +0 -1
  88. package/dist/schema/plugin_entities.d.ts +0 -213
  89. package/dist/schema/plugin_entities.d.ts.map +0 -1
  90. package/dist/schema/plugin_entities.js +0 -37
  91. package/dist/schema/plugin_entities.js.map +0 -1
  92. package/dist/schema/plugin_jobs.js +0 -83
  93. package/dist/schema/plugin_jobs.js.map +0 -1
  94. package/dist/schema/plugin_logs.d.ts +0 -125
  95. package/dist/schema/plugin_logs.d.ts.map +0 -1
  96. package/dist/schema/plugin_logs.js +0 -29
  97. package/dist/schema/plugin_logs.js.map +0 -1
  98. package/dist/schema/plugin_organization_settings.d.ts +0 -158
  99. package/dist/schema/plugin_organization_settings.d.ts.map +0 -1
  100. package/dist/schema/plugin_organization_settings.js +0 -34
  101. package/dist/schema/plugin_organization_settings.js.map +0 -1
  102. package/dist/schema/plugin_state.d.ts +0 -169
  103. package/dist/schema/plugin_state.d.ts.map +0 -1
  104. package/dist/schema/plugin_state.js +0 -68
  105. package/dist/schema/plugin_state.js.map +0 -1
  106. package/dist/schema/plugin_webhooks.d.ts +0 -239
  107. package/dist/schema/plugin_webhooks.d.ts.map +0 -1
  108. package/dist/schema/plugin_webhooks.js +0 -52
  109. package/dist/schema/plugin_webhooks.js.map +0 -1
  110. package/dist/schema/plugins.d.ts +0 -246
  111. package/dist/schema/plugins.d.ts.map +0 -1
  112. package/dist/schema/plugins.js +0 -31
  113. package/dist/schema/plugins.js.map +0 -1
@@ -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 { applyPendingMigrations, ensurePostgresDatabase, ensurePostgresRolePassword, inspectMigrations, reconcilePendingMigrationHistory, } from "./client.js";
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
- while (runningInstances.length > 0) {
156
- const instance = runningInstances.pop();
157
- if (!instance)
158
- continue;
159
- await instance.stop();
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.rmSync(tempPath, { recursive: true, force: true });
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
- }, 20_000);
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
- }, 20_000);
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
- }, 20_000);
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