From e789d0425ef1a014f77d7522947399ce94c95cca Mon Sep 17 00:00:00 2001 From: Haoqian Li Date: Thu, 25 Jun 2026 14:02:14 +0800 Subject: [PATCH] fix: preserve composite indexes in generated SQL --- src/types/schema.ts | 1 + src/utils/sqlGenerator.ts | 50 ++++++++++++++++++++++++++++--- tests/utils/sqlGenerator.test.ts | 51 ++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/types/schema.ts b/src/types/schema.ts index 0f519cb8..1544250b 100644 --- a/src/types/schema.ts +++ b/src/types/schema.ts @@ -19,4 +19,5 @@ export interface Index { column_name: string; is_unique: boolean; is_primary: boolean; + seq_in_index?: number; } diff --git a/src/utils/sqlGenerator.ts b/src/utils/sqlGenerator.ts index c143a3fb..d61f2b2f 100644 --- a/src/utils/sqlGenerator.ts +++ b/src/utils/sqlGenerator.ts @@ -157,20 +157,62 @@ export function generateIndexStatements( quote: string ): string[] { const statements: string[] = []; + const groupedIndexes = new Map; + }>(); + + indexes.forEach((idx, position) => { + const existing = groupedIndexes.get(idx.name); + if (existing) { + existing.columns.push({ + name: idx.column_name, + seq_in_index: idx.seq_in_index, + position, + }); + return; + } + + groupedIndexes.set(idx.name, { + name: idx.name, + is_unique: idx.is_unique, + is_primary: idx.is_primary, + columns: [{ + name: idx.column_name, + seq_in_index: idx.seq_in_index, + position, + }], + }); + }); + + const renderColumns = (columns: Array<{ name: string; seq_in_index?: number; position: number }>) => + [...columns] + .sort((a, b) => { + const aSeq = a.seq_in_index ?? Number.MAX_SAFE_INTEGER; + const bSeq = b.seq_in_index ?? Number.MAX_SAFE_INTEGER; + if (aSeq !== bSeq) return aSeq - bSeq; + return a.position - b.position; + }) + .map(col => `${quote}${col.name}${quote}`) + .join(', '); + + const indexGroups = [...groupedIndexes.values()]; // Unique indexes (excluding primary keys) - const uniqueIndexes = indexes.filter(idx => idx.is_unique && !idx.is_primary); + const uniqueIndexes = indexGroups.filter(idx => idx.is_unique && !idx.is_primary); uniqueIndexes.forEach(idx => { statements.push( - `CREATE UNIQUE INDEX ${quote}${idx.name}${quote} ON ${quote}${tableName}${quote} (${quote}${idx.column_name}${quote});` + `CREATE UNIQUE INDEX ${quote}${idx.name}${quote} ON ${quote}${tableName}${quote} (${renderColumns(idx.columns)});` ); }); // Non-unique indexes (excluding primary keys) - const nonUniqueIndexes = indexes.filter(idx => !idx.is_unique && !idx.is_primary); + const nonUniqueIndexes = indexGroups.filter(idx => !idx.is_unique && !idx.is_primary); nonUniqueIndexes.forEach(idx => { statements.push( - `CREATE INDEX ${quote}${idx.name}${quote} ON ${quote}${tableName}${quote} (${quote}${idx.column_name}${quote});` + `CREATE INDEX ${quote}${idx.name}${quote} ON ${quote}${tableName}${quote} (${renderColumns(idx.columns)});` ); }); diff --git a/tests/utils/sqlGenerator.test.ts b/tests/utils/sqlGenerator.test.ts index c514b3cb..108a25cb 100644 --- a/tests/utils/sqlGenerator.test.ts +++ b/tests/utils/sqlGenerator.test.ts @@ -255,6 +255,43 @@ describe('sqlGenerator utils', () => { expect(result[0]).toBe('CREATE INDEX `idx_name` ON `users` (`name`);'); }); + it('should generate a single statement for composite indexes', () => { + const indexes: Index[] = [ + { name: 'idx_orders_lookup', column_name: 'customer_id', is_unique: false, is_primary: false, seq_in_index: 1 }, + { name: 'idx_orders_lookup', column_name: 'status', is_unique: false, is_primary: false, seq_in_index: 2 }, + { name: 'idx_orders_lookup', column_name: 'created_at', is_unique: false, is_primary: false, seq_in_index: 3 }, + { name: 'idx_orders_lookup', column_name: 'id', is_unique: false, is_primary: false, seq_in_index: 4 }, + ]; + const result = generateIndexStatements(indexes, 'orders', '`'); + expect(result).toEqual([ + 'CREATE INDEX `idx_orders_lookup` ON `orders` (`customer_id`, `status`, `created_at`, `id`);', + ]); + }); + + it('should order composite index columns by sequence', () => { + const indexes: Index[] = [ + { name: 'idx_orders_lookup', column_name: 'id', is_unique: false, is_primary: false, seq_in_index: 4 }, + { name: 'idx_orders_lookup', column_name: 'customer_id', is_unique: false, is_primary: false, seq_in_index: 1 }, + { name: 'idx_orders_lookup', column_name: 'created_at', is_unique: false, is_primary: false, seq_in_index: 3 }, + { name: 'idx_orders_lookup', column_name: 'status', is_unique: false, is_primary: false, seq_in_index: 2 }, + ]; + const result = generateIndexStatements(indexes, 'orders', '`'); + expect(result[0]).toBe( + 'CREATE INDEX `idx_orders_lookup` ON `orders` (`customer_id`, `status`, `created_at`, `id`);', + ); + }); + + it('should generate a single statement for composite unique indexes', () => { + const indexes: Index[] = [ + { name: 'uk_user_email_scope', column_name: 'email', is_unique: true, is_primary: false, seq_in_index: 1 }, + { name: 'uk_user_email_scope', column_name: 'account_id', is_unique: true, is_primary: false, seq_in_index: 2 }, + ]; + const result = generateIndexStatements(indexes, tableName, '`'); + expect(result).toEqual([ + 'CREATE UNIQUE INDEX `uk_user_email_scope` ON `users` (`email`, `account_id`);', + ]); + }); + it('should generate multiple index statements', () => { const indexes: Index[] = [ { name: 'uk_email', column_name: 'email', is_unique: true, is_primary: false }, @@ -323,6 +360,20 @@ describe('sqlGenerator utils', () => { expect(result).toContain('CREATE INDEX `idx_name` ON `users` (`name`);'); }); + it('should generate CREATE TABLE with composite indexes', () => { + const indexes: Index[] = [ + { name: 'idx_orders_lookup', column_name: 'customer_id', is_unique: false, is_primary: false, seq_in_index: 1 }, + { name: 'idx_orders_lookup', column_name: 'status', is_unique: false, is_primary: false, seq_in_index: 2 }, + { name: 'idx_orders_lookup', column_name: 'created_at', is_unique: false, is_primary: false, seq_in_index: 3 }, + { name: 'idx_orders_lookup', column_name: 'id', is_unique: false, is_primary: false, seq_in_index: 4 }, + ]; + const result = generateCreateTableSQL('orders', columns, [], indexes, 'mysql'); + expect(result).toContain( + 'CREATE INDEX `idx_orders_lookup` ON `orders` (`customer_id`, `status`, `created_at`, `id`);', + ); + expect(result).not.toContain('CREATE INDEX `idx_orders_lookup` ON `orders` (`status`);'); + }); + it('should generate complete SQL for PostgreSQL', () => { const result = generateCreateTableSQL('users', columns, [], [], 'postgresql'); expect(result).toContain('CREATE TABLE "users" (');