Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions packages/web/src/lib/erd.rename.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
63 changes: 63 additions & 0 deletions packages/web/src/lib/erd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'."
);
});
});
});
63 changes: 63 additions & 0 deletions packages/web/src/lib/erd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
Loading