History

backup

Rhys Sullivan committed 8 months ago

Commit c8efcda1 parent 7ce9b9b2 files · +120 −15Browse files

Unified · Split

2 files
  1. packages/control-plane/src/runtime/legacy-postgres-migration.test.ts +29 −4
  2. packages/control-plane/src/runtime/legacy-postgres-migration.ts +91 −11
packages/control-plane/src/runtime/legacy-postgres-migration.test.ts+29−4
@@ -1,6 +1,6 @@
1import { existsSync, mkdtempSync } from "node:fs";1import { existsSync, mkdtempSync, readdirSync } from "node:fs";
2import { tmpdir } from "node:os";2import { tmpdir } from "node:os";
3import { join } from "node:path";3import { basename, dirname, join } from "node:path";
4 4
5import { PGlite } from "@electric-sql/pglite";5import { PGlite } from "@electric-sql/pglite";
6import { NodeFileSystem } from "@effect/platform-node";6import { NodeFileSystem } from "@effect/platform-node";
@@ -39,6 +39,16 @@ const UPDATED_AT = 1_700_000_000_500; · 32 unmodified lines
39const makeTempDir = (prefix: string) =>39const makeTempDir = (prefix: string) =>
40 mkdtempSync(join(tmpdir(), prefix));40 mkdtempSync(join(tmpdir(), prefix));
41 41
42const findLegacyBackupPath = (legacyLocalDataDir: string): string | null => {
43 const parentDirectory = dirname(legacyLocalDataDir);
44 const backupPrefix = `${basename(legacyLocalDataDir)}.migrated-backup-`;
45 const backupEntry = readdirSync(parentDirectory).find((entry) =>
46 entry.startsWith(backupPrefix),
47 );
48
49 return backupEntry ? join(parentDirectory, backupEntry) : null;
50};
51
42const createLegacySchema = async (db: PGlite) => {52const createLegacySchema = async (db: PGlite) => {
43 await db.exec(`53 await db.exec(`
44 create table local_installations (54 create table local_installations (
@@ -470,11 +480,20 @@ const seedInitialLegacyWorkspace = async (legacyLocalDataDir: string) => { · 425 unmodified lines
470 }480 }
471};481};
472 482
473const appendUnmigratedLegacySource = async (legacyLocalDataDir: string) => {483const seedFreshLegacyWorkspaceWithSecondSource = async (legacyLocalDataDir: string) => {
474 const db = new PGlite(legacyLocalDataDir);484 const db = new PGlite(legacyLocalDataDir);
475 await db.waitReady;485 await db.waitReady;
476 486
477 try {487 try {
488 await createLegacySchema(db);
489 await db.query(
490 "insert into local_installations (id, account_id, workspace_id) values ($1, $2, $3)",
491 [LEGACY_INSTALLATION_ID, LEGACY_ACCOUNT_ID, LEGACY_WORKSPACE_ID],
492 );
493 await db.query(
494 "insert into workspaces (id, name) values ($1, $2)",
495 [LEGACY_WORKSPACE_ID, "Replacement Legacy Workspace"],
496 );
478 await db.query(497 await db.query(
479 `insert into sources (498 `insert into sources (
480 workspace_id,499 workspace_id,
@@ -538,6 +557,7 @@ describe("legacy-postgres-migration", () => { · 57 unmodified lines
538 homeStateDirectory,557 homeStateDirectory,
539 localDataDir: legacyLocalDataDir,558 localDataDir: legacyLocalDataDir,
540 });559 });
560 const legacyBackupPath = findLegacyBackupPath(legacyLocalDataDir);
541 561
542 const context = yield* resolveLocalWorkspaceContext({562 const context = yield* resolveLocalWorkspaceContext({
543 workspaceRoot,563 workspaceRoot,
@@ -555,6 +575,9 @@ describe("legacy-postgres-migration", () => { · 11 unmodified lines
555 expect(existsSync(context.projectConfigPath)).toBe(true);575 expect(existsSync(context.projectConfigPath)).toBe(true);
556 expect(existsSync(localWorkspaceStatePath(context))).toBe(true);576 expect(existsSync(localWorkspaceStatePath(context))).toBe(true);
557 expect(existsSync(localControlPlaneStatePath(context))).toBe(true);577 expect(existsSync(localControlPlaneStatePath(context))).toBe(true);
578 expect(existsSync(legacyLocalDataDir)).toBe(false);
579 expect(legacyBackupPath).not.toBeNull();
580 expect(legacyBackupPath !== null ? existsSync(legacyBackupPath) : false).toBe(true);
558 581
559 expect(loadedConfig.projectConfig?.sources?.[LEGACY_SOURCE_ID]?.kind).toBe("mcp");582 expect(loadedConfig.projectConfig?.sources?.[LEGACY_SOURCE_ID]?.kind).toBe("mcp");
560 expect(583 expect(
@@ -616,7 +639,9 @@ describe("legacy-postgres-migration", () => { · 55 unmodified lines
616 639
617 yield* Effect.promise(() => firstRuntime.close());640 yield* Effect.promise(() => firstRuntime.close());
618 641
619 yield* Effect.promise(() => appendUnmigratedLegacySource(legacyLocalDataDir));642 yield* Effect.promise(() =>
643 seedFreshLegacyWorkspaceWithSecondSource(legacyLocalDataDir),
644 );
620 645
621 const secondRuntime = yield* createControlPlaneRuntime({646 const secondRuntime = yield* createControlPlaneRuntime({
622 workspaceRoot,647 workspaceRoot,
packages/control-plane/src/runtime/legacy-postgres-migration.ts+91−11
@@ -1,5 +1,5 @@
1import { existsSync, readFileSync } from "node:fs";1import { existsSync, readFileSync } from "node:fs";
2import { mkdir, unlink } from "node:fs/promises";2import { mkdir, rename, unlink } from "node:fs/promises";
3import { join, resolve } from "node:path";3import { join, resolve } from "node:path";
4 4
5import { FileSystem } from "@effect/platform";5import { FileSystem } from "@effect/platform";
@@ -243,6 +243,21 @@ type LegacyWorkspaceSnapshot = { · 237 unmodified lines
243 toolArtifacts: LegacyToolArtifactRow[];243 toolArtifacts: LegacyToolArtifactRow[];
244};244};
245 245
246type LegacySnapshotSource =
247 | {
248 kind: "pglite";
249 localDataDir: string;
250 }
251 | {
252 kind: "postgres";
253 databaseUrl: string;
254 };
255
256type LoadedLegacyWorkspaceSnapshot = {
257 snapshot: LegacyWorkspaceSnapshot;
258 source: LegacySnapshotSource;
259};
260
246type LegacyQueryClient = {261type LegacyQueryClient = {
247 label: string;262 label: string;
248 query: (sql: string) => Promise<LegacyRow[]>;263 query: (sql: string) => Promise<LegacyRow[]>;
@@ -726,31 +741,71 @@ const loadLegacyWorkspaceSnapshot = async ( · 477 unmodified lines
726 };741 };
727};742};
728 743
744const backupLegacyPGliteDataDir = async (
745 localDataDir: string,
746): Promise<string | null> => {
747 if (localDataDir.trim() === ":memory:") {
748 return null;
749 }
750
751 const resolvedDataDir = resolve(localDataDir);
752 if (!existsSync(resolvedDataDir)) {
753 return null;
754 }
755
756 const backupPath = `${resolvedDataDir}.migrated-backup-${Date.now()}`;
757 await rename(resolvedDataDir, backupPath);
758 return backupPath;
759};
760
729const tryLoadLegacyWorkspaceSnapshot = async (input: {761const tryLoadLegacyWorkspaceSnapshot = async (input: {
730 localDataDir?: string;762 localDataDir?: string;
731 databaseUrl?: string;763 databaseUrl?: string;
732}): Promise<LegacyWorkspaceSnapshot | null> => {764}): Promise<LoadedLegacyWorkspaceSnapshot | null> => {
733 const clients: LegacyQueryClient[] = [];765 const clients: Array<{
766 client: LegacyQueryClient;
767 source: LegacySnapshotSource;
768 }> = [];
734 769
735 try {770 try {
736 if (trimOrNull(input.localDataDir) !== null) {771 if (trimOrNull(input.localDataDir) !== null) {
737 const localClient = await openLegacyPGliteClient(input.localDataDir!);772 const localClient = await openLegacyPGliteClient(input.localDataDir!);
738 if (localClient !== null) {773 if (localClient !== null) {
739 clients.push(localClient);774 clients.push({
775 client: localClient,
776 source: {
777 kind: "pglite",
778 localDataDir: input.localDataDir!,
779 },
780 });
740 }781 }
741 }782 }
742 783
743 if (clients.length === 0 && isPostgresUrl(input.databaseUrl)) {784 if (clients.length === 0 && isPostgresUrl(input.databaseUrl)) {
744 clients.push(openLegacyPostgresClient(input.databaseUrl!));785 clients.push({
786 client: openLegacyPostgresClient(input.databaseUrl!),
787 source: {
788 kind: "postgres",
789 databaseUrl: input.databaseUrl!,
790 },
791 });
745 }792 }
746 793
747 for (const client of clients) {794 for (const { client, source } of clients) {
748 try {795 try {
749 if (!(await hasLegacySchema(client))) {796 if (!(await hasLegacySchema(client))) {
750 continue;797 continue;
751 }798 }
752 799
753 return await loadLegacyWorkspaceSnapshot(client);800 const snapshot = await loadLegacyWorkspaceSnapshot(client);
801 if (snapshot === null) {
802 continue;
803 }
804
805 return {
806 snapshot,
807 source,
808 };
754 } catch {809 } catch {
755 continue;810 continue;
756 }811 }
@@ -759,7 +814,7 @@ const tryLoadLegacyWorkspaceSnapshot = async (input: { · 2 unmodified lines
759 return null;814 return null;
760 } finally {815 } finally {
761 await Promise.all(816 await Promise.all(
762 clients.map((client) =>817 clients.map(({ client }) =>
763 client.close().catch(() => undefined)),818 client.close().catch(() => undefined)),
764 );819 );
765 }820 }
@@ -1526,7 +1581,7 @@ export const migrateLegacyPostgresWorkspaceIfNeeded = (input: { · 760 unmodified lines
1526 return;1581 return;
1527 }1582 }
1528 1583
1529 const snapshot = yield* Effect.tryPromise({1584 const loadedSnapshot = yield* Effect.tryPromise({
1530 try: () =>1585 try: () =>
1531 tryLoadLegacyWorkspaceSnapshot({1586 tryLoadLegacyWorkspaceSnapshot({
1532 localDataDir: input.legacyLocalDataDir,1587 localDataDir: input.legacyLocalDataDir,
@@ -1534,7 +1589,7 @@ export const migrateLegacyPostgresWorkspaceIfNeeded = (input: { · 1 unmodified line
1534 }),1589 }),
1535 catch: toError,1590 catch: toError,
1536 });1591 });
1537 if (snapshot === null) {1592 if (loadedSnapshot === null) {
1538 return;1593 return;
1539 }1594 }
1540 1595
@@ -1542,7 +1597,7 @@ export const migrateLegacyPostgresWorkspaceIfNeeded = (input: { · 1 unmodified line
1542 try: () =>1597 try: () =>
1543 migrateLegacyWorkspace({1598 migrateLegacyWorkspace({
1544 context: input.context,1599 context: input.context,
1545 snapshot,1600 snapshot: loadedSnapshot.snapshot,
1546 }),1601 }),
1547 catch: toError,1602 catch: toError,
1548 });1603 });
@@ -1570,6 +1625,31 @@ export const migrateLegacyPostgresWorkspaceIfNeeded = (input: { · 21 unmodified lines
1570 { discard: true },1625 { discard: true },
1571 );1626 );
1572 1627
1628 if (loadedSnapshot.source.kind === "pglite") {
1629 const legacyLocalDataDir = loadedSnapshot.source.localDataDir;
1630 const backupExit = yield* Effect.exit(
1631 Effect.tryPromise({
1632 try: () => backupLegacyPGliteDataDir(legacyLocalDataDir),
1633 catch: toError,
1634 }),
1635 );
1636
1637 yield* Effect.sync(() => {
1638 if (backupExit._tag === "Success" && backupExit.value !== null) {
1639 console.warn(
1640 `[executor] Backed up legacy local data dir to: ${backupExit.value}`,
1641 );
1642 return;
1643 }
1644
1645 if (backupExit._tag === "Failure") {
1646 console.warn(
1647 `[executor] Migrated workspace files but failed to back up legacy local data dir: ${String(backupExit.cause)}`,
1648 );
1649 }
1650 });
1651 }
1652
1573 yield* Effect.sync(() => {1653 yield* Effect.sync(() => {
1574 console.warn(1654 console.warn(
1575 `[executor] Migrated ${String(migrated.sourceCount)} legacy source(s) and ${String(migrated.policyCount)} policy/policies into ${input.context.projectConfigPath}`,1655 `[executor] Migrated ${String(migrated.sourceCount)} legacy source(s) and ${String(migrated.policyCount)} policy/policies into ${input.context.projectConfigPath}`,

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t