import assert from "node:assert/strict"; import test from "node:test"; import { buildCreateTableSql, buildTableStructureChangeSql, type EditableStructureColumn, type EditableStructureIndex, } from "../../apps/desktop/src/lib/tableStructureEditorSql.ts"; function column(overrides: Partial): EditableStructureColumn { return { id: overrides.id ?? "col", name: overrides.name ?? "name", dataType: overrides.dataType ?? "varchar(255)", isNullable: overrides.isNullable ?? true, defaultValue: overrides.defaultValue ?? "", comment: overrides.comment ?? "", isPrimaryKey: overrides.isPrimaryKey ?? false, original: overrides.original, markedForDrop: overrides.markedForDrop ?? false, }; } function index(overrides: Partial): EditableStructureIndex { return { id: overrides.id ?? "idx", name: overrides.name ?? "idx_name", columns: overrides.columns ?? ["name"], isUnique: overrides.isUnique ?? false, isPrimary: overrides.isPrimary ?? false, filter: overrides.filter ?? "", indexType: overrides.indexType ?? "", includedColumns: overrides.includedColumns ?? [], comment: overrides.comment ?? "", original: overrides.original, markedForDrop: overrides.markedForDrop ?? false, }; } test("builds MySQL column and index change statements", () => { const result = buildTableStructureChangeSql({ databaseType: "mysql", tableName: "users", columns: [ column({ id: "name", name: "display_name", dataType: "varchar(120)", isNullable: false, defaultValue: "'guest'", comment: "Shown name", original: { name: "name", data_type: "varchar(80)", is_nullable: true, column_default: null, is_primary_key: false, extra: null, comment: "", }, }), column({ id: "email", name: "email", dataType: "varchar(255)", isNullable: false }), column({ id: "legacy", name: "legacy", markedForDrop: true, original: { name: "legacy", data_type: "text", is_nullable: true, column_default: null, is_primary_key: false, extra: null, }, }), ], indexes: [ index({ id: "old", name: "idx_old", markedForDrop: true, original: { name: "idx_old", columns: ["name"], is_unique: false, is_primary: false }, }), index({ id: "email_idx", name: "uniq_users_email", columns: ["email"], isUnique: true }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ "ALTER TABLE `users` CHANGE COLUMN `name` `display_name` varchar(120) NOT NULL DEFAULT 'guest' COMMENT 'Shown name';", "ALTER TABLE `users` ADD COLUMN `email` varchar(255) NOT NULL;", "ALTER TABLE `users` DROP COLUMN `legacy`;", "DROP INDEX `idx_old` ON `users`;", "CREATE UNIQUE INDEX `uniq_users_email` ON `users` (`email`);", ]); }); test("builds PostgreSQL rename, type, default, comment, and index statements", () => { const result = buildTableStructureChangeSql({ databaseType: "postgres", schema: "public", tableName: "accounts", columns: [ column({ id: "status", name: "account_status", dataType: "text", isNullable: false, defaultValue: "'active'", comment: "Current status", original: { name: "status", data_type: "varchar", is_nullable: true, column_default: null, is_primary_key: false, extra: null, comment: "", }, }), ], indexes: [ index({ id: "new", name: "idx_accounts_status", columns: ["account_status"] }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ "ALTER TABLE \"public\".\"accounts\" RENAME COLUMN \"status\" TO \"account_status\";", "ALTER TABLE \"public\".\"accounts\" ALTER COLUMN \"account_status\" TYPE text;", "ALTER TABLE \"public\".\"accounts\" ALTER COLUMN \"account_status\" SET NOT NULL;", "ALTER TABLE \"public\".\"accounts\" ALTER COLUMN \"account_status\" SET DEFAULT 'active';", "COMMENT ON COLUMN \"public\".\"accounts\".\"account_status\" IS 'Current status';", "CREATE INDEX \"idx_accounts_status\" ON \"public\".\"accounts\" (\"account_status\");", ]); }); test("warns when SQLite cannot safely alter existing column attributes", () => { const result = buildTableStructureChangeSql({ databaseType: "sqlite", tableName: "notes", columns: [ column({ id: "title", name: "title", dataType: "text", isNullable: false, original: { name: "title", data_type: "varchar(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null, }, }), column({ id: "body", name: "body", dataType: "text", isNullable: true }), ], indexes: [], }); assert.deepEqual(result.statements, [ "ALTER TABLE \"notes\" ADD COLUMN \"body\" text;", ]); assert.deepEqual(result.warnings, [ "SQLite cannot safely alter existing column \"title\" without rebuilding the table.", ]); }); test("quotes SQL Server table, column, and index names with brackets", () => { const result = buildTableStructureChangeSql({ databaseType: "sqlserver", schema: "dbo", tableName: "users", columns: [ column({ id: "email", name: "email", dataType: "nvarchar(255)", isNullable: false }), ], indexes: [ index({ id: "idx", name: "idx_users_email", columns: ["email"] }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ "ALTER TABLE [dbo].[users] ADD [email] nvarchar(255) NOT NULL;", "CREATE INDEX [idx_users_email] ON [dbo].[users] ([email]);", ]); }); test("builds DuckDB create table statements", () => { const result = buildCreateTableSql({ databaseType: "duckdb", tableName: "events", columns: [ column({ id: "name", name: "name", dataType: "VARCHAR", isNullable: false }), column({ id: "created_at", name: "created_at", dataType: "TIMESTAMP", defaultValue: "current_timestamp" }), ], indexes: [index({ id: "idx_name", name: "idx_events_name", columns: ["name"] })], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ 'CREATE TABLE "events" (\n "name" VARCHAR NOT NULL,\n "created_at" TIMESTAMP DEFAULT current_timestamp\n);', 'CREATE INDEX "idx_events_name" ON "events" ("name");', ]); }); test("PostgreSQL index with INCLUDE clause", () => { const result = buildTableStructureChangeSql({ databaseType: "postgres", schema: "public", tableName: "orders", columns: [], indexes: [ index({ id: "new", name: "idx_orders_status", columns: ["status"], includedColumns: ["total", "created_at"], }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ 'CREATE INDEX "idx_orders_status" ON "public"."orders" ("status") INCLUDE ("total", "created_at");', ]); }); test("PostgreSQL index with USING clause (index type)", () => { const result = buildTableStructureChangeSql({ databaseType: "postgres", schema: "public", tableName: "docs", columns: [], indexes: [ index({ id: "new", name: "idx_docs_body", columns: ["body"], indexType: "GIN", }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ 'CREATE INDEX "idx_docs_body" ON "public"."docs" USING GIN ("body");', ]); }); test("PostgreSQL index with WHERE filter", () => { const result = buildTableStructureChangeSql({ databaseType: "postgres", schema: "public", tableName: "users", columns: [], indexes: [ index({ id: "new", name: "idx_users_active", columns: ["email"], filter: "deleted_at IS NULL", }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ 'CREATE INDEX "idx_users_active" ON "public"."users" ("email") WHERE deleted_at IS NULL;', ]); }); test("PostgreSQL index with COMMENT", () => { const result = buildTableStructureChangeSql({ databaseType: "postgres", schema: "public", tableName: "users", columns: [], indexes: [ index({ id: "new", name: "idx_users_email", columns: ["email"], comment: "Fast lookup by email", }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ 'CREATE INDEX "idx_users_email" ON "public"."users" ("email");', "COMMENT ON INDEX \"idx_users_email\" IS 'Fast lookup by email';", ]); }); test("PostgreSQL index with single quote in comment is escaped", () => { const result = buildTableStructureChangeSql({ databaseType: "postgres", schema: "public", tableName: "users", columns: [], indexes: [ index({ id: "new", name: "idx_users_email", columns: ["email"], comment: "User's primary email", }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ 'CREATE INDEX "idx_users_email" ON "public"."users" ("email");', "COMMENT ON INDEX \"idx_users_email\" IS 'User''s primary email';", ]); }); test("PostgreSQL index with all options combined (unique + type + include + filter + comment)", () => { const result = buildTableStructureChangeSql({ databaseType: "postgres", schema: "public", tableName: "orders", columns: [], indexes: [ index({ id: "new", name: "idx_orders_covering", columns: ["user_id", "status"], isUnique: true, indexType: "BTREE", includedColumns: ["total"], filter: "status = 'active'", comment: "Covering index for active orders", }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ 'CREATE UNIQUE INDEX "idx_orders_covering" ON "public"."orders" USING BTREE ("user_id", "status") INCLUDE ("total") WHERE status = \'active\';', "COMMENT ON INDEX \"idx_orders_covering\" IS 'Covering index for active orders';", ]); }); test("SQL Server index with type prefix", () => { const result = buildTableStructureChangeSql({ databaseType: "sqlserver", schema: "dbo", tableName: "logs", columns: [], indexes: [ index({ id: "new", name: "idx_logs_message", columns: ["message"], indexType: "CLUSTERED", }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ "CREATE CLUSTERED INDEX [idx_logs_message] ON [dbo].[logs] ([message]);", ]); }); test("SQL Server index with INCLUDE clause", () => { const result = buildTableStructureChangeSql({ databaseType: "sqlserver", schema: "dbo", tableName: "orders", columns: [], indexes: [ index({ id: "new", name: "idx_orders_status", columns: ["status"], includedColumns: ["total", "created_at"], }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ "CREATE INDEX [idx_orders_status] ON [dbo].[orders] ([status]) INCLUDE ([total], [created_at]);", ]); }); test("SQL Server index with type + include combined", () => { const result = buildTableStructureChangeSql({ databaseType: "sqlserver", schema: "dbo", tableName: "orders", columns: [], indexes: [ index({ id: "new", name: "idx_orders_covering", columns: ["user_id"], indexType: "NONCLUSTERED", includedColumns: ["total", "status"], }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ "CREATE NONCLUSTERED INDEX [idx_orders_covering] ON [dbo].[orders] ([user_id]) INCLUDE ([total], [status]);", ]); }); test("MySQL index omits USING, type prefix, INCLUDE, and WHERE (unsupported)", () => { const result = buildTableStructureChangeSql({ databaseType: "mysql", tableName: "orders", columns: [], indexes: [ index({ id: "new", name: "idx_orders_status", columns: ["status"], indexType: "BTREE", includedColumns: ["total"], filter: "deleted = 0", comment: "Some comment", }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ "CREATE INDEX `idx_orders_status` ON `orders` (`status`);", ]); }); test("SQLite index with filter (partial index)", () => { const result = buildTableStructureChangeSql({ databaseType: "sqlite", tableName: "users", columns: [], indexes: [ index({ id: "new", name: "idx_users_active", columns: ["email"], filter: "deleted_at IS NULL", }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ 'CREATE INDEX "idx_users_active" ON "users" ("email") WHERE deleted_at IS NULL;', ]); }); test("index with empty name and columns produces warnings and no statements", () => { const result = buildTableStructureChangeSql({ databaseType: "postgres", schema: "public", tableName: "users", columns: [], indexes: [ index({ id: "empty", name: "", columns: [] }), ], }); assert.deepEqual(result.warnings, [ 'Index name cannot be empty.', 'Index "(new)" needs at least one column.', ]); assert.deepEqual(result.statements, []); }); test("builds Oracle column, comment, and index change statements", () => { const result = buildTableStructureChangeSql({ databaseType: "oracle", schema: "HR", tableName: "EMPLOYEES", columns: [ column({ id: "status", name: "EMP_STATUS", dataType: "VARCHAR2(20)", isNullable: false, defaultValue: "'ACTIVE'", comment: "Employment status", original: { name: "STATUS", data_type: "VARCHAR2(10)", is_nullable: true, column_default: null, is_primary_key: false, extra: null, comment: "", }, }), column({ id: "email", name: "EMAIL", dataType: "VARCHAR2(255)", isNullable: true, comment: "Work email" }), column({ id: "legacy", name: "LEGACY_CODE", markedForDrop: true, original: { name: "LEGACY_CODE", data_type: "VARCHAR2(20)", is_nullable: true, column_default: null, is_primary_key: false, extra: null, }, }), ], indexes: [ index({ id: "old", name: "IDX_EMP_OLD", markedForDrop: true, original: { name: "IDX_EMP_OLD", columns: ["STATUS"], is_unique: false, is_primary: false }, }), index({ id: "new", name: "IDX_EMP_STATUS", columns: ["EMP_STATUS"], isUnique: true }), ], }); assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ 'ALTER TABLE "HR"."EMPLOYEES" RENAME COLUMN "STATUS" TO "EMP_STATUS";', 'ALTER TABLE "HR"."EMPLOYEES" MODIFY ("EMP_STATUS" VARCHAR2(20));', 'ALTER TABLE "HR"."EMPLOYEES" MODIFY ("EMP_STATUS" NOT NULL);', 'ALTER TABLE "HR"."EMPLOYEES" MODIFY ("EMP_STATUS" DEFAULT \'ACTIVE\');', 'COMMENT ON COLUMN "HR"."EMPLOYEES"."EMP_STATUS" IS \'Employment status\';', 'ALTER TABLE "HR"."EMPLOYEES" ADD ("EMAIL" VARCHAR2(255));', 'COMMENT ON COLUMN "HR"."EMPLOYEES"."EMAIL" IS \'Work email\';', 'ALTER TABLE "HR"."EMPLOYEES" DROP COLUMN "LEGACY_CODE";', 'DROP INDEX "HR"."IDX_EMP_OLD";', 'CREATE UNIQUE INDEX "IDX_EMP_STATUS" ON "HR"."EMPLOYEES" ("EMP_STATUS");', ]); }); test("builds Dameng existing column and create table statements", () => { const change = buildTableStructureChangeSql({ databaseType: "dameng", schema: "SYSDBA", tableName: "USERS", columns: [ column({ id: "name", name: "DISPLAY_NAME", dataType: "VARCHAR(120)", isNullable: true, defaultValue: "", comment: "", original: { name: "NAME", data_type: "VARCHAR(80)", is_nullable: false, column_default: "'guest'", is_primary_key: false, extra: null, comment: "Old name", }, }), ], indexes: [], }); assert.deepEqual(change.warnings, []); assert.deepEqual(change.statements, [ 'ALTER TABLE "SYSDBA"."USERS" RENAME COLUMN "NAME" TO "DISPLAY_NAME";', 'ALTER TABLE "SYSDBA"."USERS" MODIFY ("DISPLAY_NAME" VARCHAR(120));', 'ALTER TABLE "SYSDBA"."USERS" MODIFY ("DISPLAY_NAME" NULL);', 'ALTER TABLE "SYSDBA"."USERS" MODIFY ("DISPLAY_NAME" DEFAULT NULL);', 'COMMENT ON COLUMN "SYSDBA"."USERS"."DISPLAY_NAME" IS NULL;', ]); const create = buildCreateTableSql({ databaseType: "dameng", schema: "SYSDBA", tableName: "USERS", columns: [ column({ id: "id", name: "ID", dataType: "NUMBER", isNullable: false, isPrimaryKey: true }), column({ id: "name", name: "NAME", dataType: "VARCHAR(120)", isNullable: false, comment: "Display name" }), ], indexes: [index({ id: "idx", name: "IDX_USERS_NAME", columns: ["NAME"] })], }); assert.deepEqual(create.warnings, []); assert.deepEqual(create.statements, [ 'CREATE TABLE "SYSDBA"."USERS" (\n "ID" NUMBER,\n "NAME" VARCHAR(120) NOT NULL,\n PRIMARY KEY ("ID")\n);', 'COMMENT ON COLUMN "SYSDBA"."USERS"."NAME" IS \'Display name\';', 'CREATE INDEX "IDX_USERS_NAME" ON "SYSDBA"."USERS" ("NAME");', ]); });