From b0e1b68d0608597161cbb551bae716afe11b976c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:05:23 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20ERD=20=EC=97=94=EC=A7=80=EB=8B=88?= =?UTF-8?q?=EC=96=B4=EB=A7=81=20=ED=88=B4=20=ED=85=8C=EC=9D=B4=EB=B8=94/?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/web/src/lib/erd.test.ts | 63 ++++++++++++++++++++++++++++++++ packages/web/src/lib/erd.ts | 61 +++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/packages/web/src/lib/erd.test.ts b/packages/web/src/lib/erd.test.ts index b40a8008..1056bc59 100644 --- a/packages/web/src/lib/erd.test.ts +++ b/packages/web/src/lib/erd.test.ts @@ -407,4 +407,67 @@ CREATE TABLE posts ( ); }); }); + + describe("Renaming", () => { + beforeEach(() => { + model.addTable("users"); + model.addColumn("users", { name: "id", type: "integer" }); + model.addTable("posts"); + model.addColumn("posts", { name: "id", type: "integer" }); + model.addColumn("posts", { name: "user_id", type: "integer" }); + model.addForeignKey("posts", { + columnName: "user_id", + referenceTable: "users", + referenceColumn: "id", + }); + }); + + it("should rename table and update foreign keys", () => { + model.renameTable("users", "members"); + expect(model.getTable("users")).toBeUndefined(); + expect(model.getTable("members")?.name).toBe("members"); + expect(model.getTable("posts")?.foreignKeys[0].referenceTable).toBe("members"); + }); + + it("should throw when renaming non-existent table", () => { + expect(() => model.renameTable("non_existent", "members")).toThrowError( + "Table 'non_existent' does not exist." + ); + }); + + it("should throw when renaming table to existing table", () => { + expect(() => model.renameTable("users", "posts")).toThrowError( + "Table 'posts' already exists." + ); + }); + + it("should rename column and update foreign keys", () => { + model.renameColumn("users", "id", "member_id"); + expect(model.getTable("users")?.columns[0].name).toBe("member_id"); + expect(model.getTable("posts")?.foreignKeys[0].referenceColumn).toBe("member_id"); + + model.renameColumn("posts", "user_id", "member_id"); + expect(model.getTable("posts")?.columns[1].name).toBe("member_id"); + expect(model.getTable("posts")?.foreignKeys[0].columnName).toBe("member_id"); + }); + + it("should throw when renaming column in non-existent table", () => { + expect(() => model.renameColumn("non_existent", "id", "new_id")).toThrowError( + "Table 'non_existent' does not exist." + ); + }); + + it("should throw when renaming non-existent column", () => { + expect(() => model.renameColumn("users", "non_existent", "new_id")).toThrowError( + "Column 'non_existent' does not exist in table 'users'." + ); + }); + + it("should throw when renaming column to existing column", () => { + model.addColumn("users", { name: "existing_col", type: "integer" }); + expect(() => model.renameColumn("users", "id", "existing_col")).toThrowError( + "Column 'existing_col' already exists in table 'users'." + ); + }); + }); }); diff --git a/packages/web/src/lib/erd.ts b/packages/web/src/lib/erd.ts index 0cc436c4..3f87a295 100644 --- a/packages/web/src/lib/erd.ts +++ b/packages/web/src/lib/erd.ts @@ -204,6 +204,67 @@ export class ERDModel { table.foreignKeys.splice(fkIndex, 1); } + renameTable(oldName: string, newName: string): void { + assertSnakeCaseIdentifier("Table", oldName); + assertSnakeCaseIdentifier("Table", newName); + + const table = this.tables.get(oldName); + if (!table) { + throw new Error(`Table '${oldName}' does not exist.`); + } + if (this.tables.has(newName)) { + throw new Error(`Table '${newName}' already exists.`); + } + + table.name = newName; + this.tables.delete(oldName); + this.tables.set(newName, table); + + for (const t of this.tables.values()) { + for (const fk of t.foreignKeys) { + if (fk.referenceTable === oldName) { + fk.referenceTable = newName; + } + } + } + } + + renameColumn(tableName: string, oldColumnName: string, newColumnName: string): void { + assertSnakeCaseIdentifier("Table", tableName); + assertSnakeCaseIdentifier("Column", oldColumnName); + assertSnakeCaseIdentifier("Column", newColumnName); + + const table = this.tables.get(tableName); + if (!table) { + throw new Error(`Table '${tableName}' does not exist.`); + } + + const col = table.columns.find((c) => c.name === oldColumnName); + if (!col) { + throw new Error(`Column '${oldColumnName}' does not exist in table '${tableName}'.`); + } + + if (table.columns.some((c) => c.name === newColumnName)) { + throw new Error(`Column '${newColumnName}' already exists in table '${tableName}'.`); + } + + col.name = newColumnName; + + for (const t of this.tables.values()) { + for (const fk of t.foreignKeys) { + if (fk.referenceTable === tableName && fk.referenceColumn === oldColumnName) { + fk.referenceColumn = newColumnName; + } + } + } + + for (const fk of table.foreignKeys) { + if (fk.columnName === oldColumnName) { + fk.columnName = newColumnName; + } + } + } + generateDDL(): string { let ddl = ""; for (const table of this.tables.values()) { From 7ca9f8f5e42bcc1a5a55cefbde8e2628e7f451eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 07:37:01 +0900 Subject: [PATCH 2/6] fix(erd): preserve table order across rename --- packages/web/src/lib/erd.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/web/src/lib/erd.ts b/packages/web/src/lib/erd.ts index 3f87a295..6232787f 100644 --- a/packages/web/src/lib/erd.ts +++ b/packages/web/src/lib/erd.ts @@ -217,8 +217,11 @@ export class ERDModel { } table.name = newName; - this.tables.delete(oldName); - this.tables.set(newName, table); + const renamedTables = new Map(); + for (const [name, currentTable] of this.tables) { + renamedTables.set(name === oldName ? newName : name, currentTable); + } + this.tables = renamedTables; for (const t of this.tables.values()) { for (const fk of t.foreignKeys) { From 1a2468e52b210bd2a85d7d0b499a8695f491d26e Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 05:34:00 +0000 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20ERD=20=EC=97=94=EC=A7=80=EB=8B=88?= =?UTF-8?q?=EC=96=B4=EB=A7=81=20=ED=88=B4=20=ED=85=8C=EC=9D=B4=EB=B8=94/?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/web/src/lib/erd.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/web/src/lib/erd.ts b/packages/web/src/lib/erd.ts index 6232787f..3f87a295 100644 --- a/packages/web/src/lib/erd.ts +++ b/packages/web/src/lib/erd.ts @@ -217,11 +217,8 @@ export class ERDModel { } table.name = newName; - const renamedTables = new Map(); - for (const [name, currentTable] of this.tables) { - renamedTables.set(name === oldName ? newName : name, currentTable); - } - this.tables = renamedTables; + this.tables.delete(oldName); + this.tables.set(newName, table); for (const t of this.tables.values()) { for (const fk of t.foreignKeys) { From f919a3aa05286f91e313b1a0b8d4b66eae948c77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:03:33 +0900 Subject: [PATCH 4/6] test(erd): preserve table order across rename --- packages/web/src/lib/erd.rename.test.ts | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 packages/web/src/lib/erd.rename.test.ts diff --git a/packages/web/src/lib/erd.rename.test.ts b/packages/web/src/lib/erd.rename.test.ts new file mode 100644 index 00000000..7b38c4d9 --- /dev/null +++ b/packages/web/src/lib/erd.rename.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { ERDModel } from "./erd"; + +describe("ERDModel rename stability", () => { + it("preserves dependency-safe table order when renaming a referenced table", () => { + const model = new ERDModel(); + model.addTable("users"); + model.addColumn("users", { name: "id", type: "integer" }); + model.addTable("posts"); + model.addColumn("posts", { name: "id", type: "integer" }); + model.addColumn("posts", { name: "user_id", type: "integer" }); + model.addForeignKey("posts", { + columnName: "user_id", + referenceTable: "users", + referenceColumn: "id", + }); + + model.renameTable("users", "members"); + + expect(model.getTables().map((table) => table.name)).toEqual([ + "members", + "posts", + ]); + + const ddl = model.generateDDL(); + expect(ddl.indexOf("CREATE TABLE members")).toBeLessThan( + ddl.indexOf("CREATE TABLE posts"), + ); + expect(ddl).toContain("REFERENCES members(id)"); + }); +}); From c0b5abc531226939d6a85309308008c09fb662ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:04:23 +0900 Subject: [PATCH 5/6] fix(erd): preserve table order when renaming --- packages/web/src/lib/erd.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/web/src/lib/erd.ts b/packages/web/src/lib/erd.ts index 3f87a295..53e8ac0c 100644 --- a/packages/web/src/lib/erd.ts +++ b/packages/web/src/lib/erd.ts @@ -217,8 +217,12 @@ export class ERDModel { } table.name = newName; - this.tables.delete(oldName); - this.tables.set(newName, table); + this.tables = new Map( + Array.from(this.tables.entries(), ([name, currentTable]) => [ + name === oldName ? newName : name, + currentTable, + ]), + ); for (const t of this.tables.values()) { for (const fk of t.foreignKeys) { From 42f814b3fdb086d68254c30575395d08e71153f8 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 06:14:17 +0000 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20ERD=20=EC=97=94=EC=A7=80=EB=8B=88?= =?UTF-8?q?=EC=96=B4=EB=A7=81=20=ED=88=B4=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=88=9C=EC=84=9C=20=EB=B3=B4=EC=A1=B4=20=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/web/src/lib/erd.rename.test.ts | 32 +++++++++++++++---------- packages/web/src/lib/erd.ts | 6 ++--- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/web/src/lib/erd.rename.test.ts b/packages/web/src/lib/erd.rename.test.ts index 7b38c4d9..82a80c31 100644 --- a/packages/web/src/lib/erd.rename.test.ts +++ b/packages/web/src/lib/erd.rename.test.ts @@ -1,13 +1,14 @@ import { describe, expect, it } from "vitest"; import { ERDModel } from "./erd"; -describe("ERDModel rename stability", () => { - it("preserves dependency-safe table order when renaming a referenced table", () => { +describe("ERDModel renameTable ordering regression", () => { + it("maintains table order and updates FKs when renaming users to members", () => { const model = new ERDModel(); model.addTable("users"); - model.addColumn("users", { name: "id", type: "integer" }); + model.addColumn("users", { name: "id", type: "integer", isPrimaryKey: true }); + model.addTable("posts"); - model.addColumn("posts", { name: "id", type: "integer" }); + model.addColumn("posts", { name: "id", type: "integer", isPrimaryKey: true }); model.addColumn("posts", { name: "user_id", type: "integer" }); model.addForeignKey("posts", { columnName: "user_id", @@ -15,17 +16,24 @@ describe("ERDModel rename stability", () => { referenceColumn: "id", }); + // Rename users to members model.renameTable("users", "members"); - expect(model.getTables().map((table) => table.name)).toEqual([ - "members", - "posts", - ]); + // The order of tables should be [members, posts], not [posts, members] + const tables = model.getTables(); + expect(tables.map(t => t.name)).toStrictEqual(["members", "posts"]); + // DDL should also generate members before posts and update REFERENCES const ddl = model.generateDDL(); - expect(ddl.indexOf("CREATE TABLE members")).toBeLessThan( - ddl.indexOf("CREATE TABLE posts"), - ); - expect(ddl).toContain("REFERENCES members(id)"); + const expectedDdl = `CREATE TABLE members ( + id integer PRIMARY KEY +); + +CREATE TABLE posts ( + id integer PRIMARY KEY, + user_id integer, + FOREIGN KEY (user_id) REFERENCES members(id) +);`; + expect(ddl).toBe(expectedDdl); }); }); diff --git a/packages/web/src/lib/erd.ts b/packages/web/src/lib/erd.ts index 53e8ac0c..daa9767b 100644 --- a/packages/web/src/lib/erd.ts +++ b/packages/web/src/lib/erd.ts @@ -217,11 +217,9 @@ export class ERDModel { } table.name = newName; + const entries = Array.from(this.tables.entries()); this.tables = new Map( - Array.from(this.tables.entries(), ([name, currentTable]) => [ - name === oldName ? newName : name, - currentTable, - ]), + entries.map(([key, val]) => (key === oldName ? [newName, val] : [key, val])) ); for (const t of this.tables.values()) {