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..82a80c31 --- /dev/null +++ b/packages/web/src/lib/erd.rename.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { ERDModel } from "./erd"; + +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", isPrimaryKey: true }); + + model.addTable("posts"); + model.addColumn("posts", { name: "id", type: "integer", isPrimaryKey: true }); + model.addColumn("posts", { name: "user_id", type: "integer" }); + model.addForeignKey("posts", { + columnName: "user_id", + referenceTable: "users", + referenceColumn: "id", + }); + + // Rename users to members + model.renameTable("users", "members"); + + // 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(); + 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.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..daa9767b 100644 --- a/packages/web/src/lib/erd.ts +++ b/packages/web/src/lib/erd.ts @@ -204,6 +204,69 @@ 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; + const entries = Array.from(this.tables.entries()); + this.tables = new Map( + entries.map(([key, val]) => (key === oldName ? [newName, val] : [key, val])) + ); + + 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()) {