metal-orm 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +17 -15
  2. package/dist/decorators/index.cjs +15 -2
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +1 -1
  5. package/dist/decorators/index.d.ts +1 -1
  6. package/dist/decorators/index.js +15 -2
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +1394 -149
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +257 -23
  11. package/dist/index.d.ts +257 -23
  12. package/dist/index.js +1376 -149
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-654m4qy8.d.cts → select-BKlr2ivY.d.cts} +141 -4
  15. package/dist/{select-654m4qy8.d.ts → select-BKlr2ivY.d.ts} +141 -4
  16. package/package.json +1 -1
  17. package/src/core/ddl/dialects/base-schema-dialect.ts +48 -0
  18. package/src/core/ddl/dialects/index.ts +5 -0
  19. package/src/core/ddl/dialects/mssql-schema-dialect.ts +97 -0
  20. package/src/core/ddl/dialects/mysql-schema-dialect.ts +109 -0
  21. package/src/core/ddl/dialects/postgres-schema-dialect.ts +99 -0
  22. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +103 -0
  23. package/src/core/ddl/introspect/mssql.ts +149 -0
  24. package/src/core/ddl/introspect/mysql.ts +99 -0
  25. package/src/core/ddl/introspect/postgres.ts +154 -0
  26. package/src/core/ddl/introspect/sqlite.ts +66 -0
  27. package/src/core/ddl/introspect/types.ts +19 -0
  28. package/src/core/ddl/introspect/utils.ts +27 -0
  29. package/src/core/ddl/schema-diff.ts +179 -0
  30. package/src/core/ddl/schema-generator.ts +229 -0
  31. package/src/core/ddl/schema-introspect.ts +32 -0
  32. package/src/core/ddl/schema-types.ts +39 -0
  33. package/src/core/dialect/base/sql-dialect.ts +161 -0
  34. package/src/core/dialect/mysql/index.ts +18 -112
  35. package/src/core/dialect/postgres/index.ts +30 -126
  36. package/src/core/dialect/sqlite/index.ts +29 -129
  37. package/src/index.ts +15 -10
  38. package/src/schema/column.ts +206 -27
  39. package/src/schema/table.ts +89 -32
  40. package/src/schema/types.ts +8 -5
@@ -0,0 +1,109 @@
1
+ import { BaseSchemaDialect } from './base-schema-dialect.js';
2
+ import {
3
+ deriveIndexName,
4
+ renderIndexColumns,
5
+ DialectName,
6
+ formatLiteral,
7
+ escapeLiteral
8
+ } from '../schema-generator.js';
9
+ import { ColumnDef } from '../../../schema/column.js';
10
+ import { IndexDef, TableDef } from '../../../schema/table.js';
11
+ import { DatabaseTable } from '../schema-types.js';
12
+
13
+ export class MySqlSchemaDialect extends BaseSchemaDialect {
14
+ name: DialectName = 'mysql';
15
+
16
+ quoteIdentifier(id: string): string {
17
+ return `\`${id}\``;
18
+ }
19
+
20
+ renderColumnType(column: ColumnDef): string {
21
+ switch (column.type) {
22
+ case 'INT':
23
+ case 'INTEGER':
24
+ case 'int':
25
+ case 'integer':
26
+ return 'INT';
27
+ case 'BIGINT':
28
+ case 'bigint':
29
+ return 'BIGINT';
30
+ case 'UUID':
31
+ case 'uuid':
32
+ return 'CHAR(36)';
33
+ case 'BOOLEAN':
34
+ case 'boolean':
35
+ return 'TINYINT(1)';
36
+ case 'JSON':
37
+ case 'json':
38
+ return 'JSON';
39
+ case 'DECIMAL':
40
+ case 'decimal':
41
+ return column.args?.length ? `DECIMAL(${column.args[0]},${column.args[1] ?? 0})` : 'DECIMAL';
42
+ case 'FLOAT':
43
+ case 'float':
44
+ return column.args?.length ? `FLOAT(${column.args[0]})` : 'FLOAT';
45
+ case 'DOUBLE':
46
+ case 'double':
47
+ return 'DOUBLE';
48
+ case 'TIMESTAMPTZ':
49
+ case 'timestamptz':
50
+ return 'TIMESTAMP';
51
+ case 'TIMESTAMP':
52
+ case 'timestamp':
53
+ return 'TIMESTAMP';
54
+ case 'DATETIME':
55
+ case 'datetime':
56
+ return 'DATETIME';
57
+ case 'DATE':
58
+ case 'date':
59
+ return 'DATE';
60
+ case 'VARCHAR':
61
+ case 'varchar':
62
+ return column.args?.length ? `VARCHAR(${column.args[0]})` : 'VARCHAR(255)';
63
+ case 'TEXT':
64
+ case 'text':
65
+ return 'TEXT';
66
+ case 'ENUM':
67
+ case 'enum':
68
+ return column.args && Array.isArray(column.args) && column.args.length
69
+ ? `ENUM(${column.args.map((v: string) => `'${escapeLiteral(v)}'`).join(',')})`
70
+ : 'ENUM';
71
+ default:
72
+ return String(column.type).toUpperCase();
73
+ }
74
+ }
75
+
76
+ renderDefault(value: unknown): string {
77
+ return formatLiteral(value, this.name);
78
+ }
79
+
80
+ renderAutoIncrement(column: ColumnDef): string | undefined {
81
+ return column.autoIncrement ? 'AUTO_INCREMENT' : undefined;
82
+ }
83
+
84
+ renderIndex(table: TableDef, index: IndexDef): string {
85
+ if (index.where) {
86
+ throw new Error('MySQL does not support partial/filtered indexes');
87
+ }
88
+ const name = index.name || deriveIndexName(table, index);
89
+ const cols = renderIndexColumns(this, index.columns);
90
+ const unique = index.unique ? 'UNIQUE ' : '';
91
+ return `CREATE ${unique}INDEX ${this.quoteIdentifier(name)} ON ${this.formatTableName(table)} (${cols});`;
92
+ }
93
+
94
+ renderTableOptions(table: TableDef): string | undefined {
95
+ const parts: string[] = [];
96
+ if (table.engine) parts.push(`ENGINE=${table.engine}`);
97
+ if (table.charset) parts.push(`DEFAULT CHARSET=${table.charset}`);
98
+ if (table.collation) parts.push(`COLLATE=${table.collation}`);
99
+ return parts.length ? parts.join(' ') : undefined;
100
+ }
101
+
102
+ dropColumnSql(table: DatabaseTable, column: string): string[] {
103
+ return [`ALTER TABLE ${this.formatTableName(table)} DROP COLUMN ${this.quoteIdentifier(column)};`];
104
+ }
105
+
106
+ dropIndexSql(table: DatabaseTable, index: string): string[] {
107
+ return [`DROP INDEX ${this.quoteIdentifier(index)} ON ${this.formatTableName(table)};`];
108
+ }
109
+ }
@@ -0,0 +1,99 @@
1
+ import { BaseSchemaDialect } from './base-schema-dialect.js';
2
+ import {
3
+ deriveIndexName,
4
+ renderIndexColumns,
5
+ DialectName
6
+ } from '../schema-generator.js';
7
+ import { ColumnDef } from '../../../schema/column.js';
8
+ import { IndexDef, TableDef } from '../../../schema/table.js';
9
+ import { DatabaseTable } from '../schema-types.js';
10
+
11
+ export class PostgresSchemaDialect extends BaseSchemaDialect {
12
+ name: DialectName = 'postgres';
13
+
14
+ quoteIdentifier(id: string): string {
15
+ return `"${id}"`;
16
+ }
17
+
18
+ renderColumnType(column: ColumnDef): string {
19
+ switch (column.type) {
20
+ case 'INT':
21
+ case 'INTEGER':
22
+ case 'int':
23
+ case 'integer':
24
+ return 'integer';
25
+ case 'BIGINT':
26
+ case 'bigint':
27
+ return 'bigint';
28
+ case 'UUID':
29
+ case 'uuid':
30
+ return 'uuid';
31
+ case 'BOOLEAN':
32
+ case 'boolean':
33
+ return 'boolean';
34
+ case 'JSON':
35
+ case 'json':
36
+ return 'jsonb';
37
+ case 'DECIMAL':
38
+ case 'decimal':
39
+ return column.args?.length ? `numeric(${column.args[0]}, ${column.args[1] ?? 0})` : 'numeric';
40
+ case 'FLOAT':
41
+ case 'float':
42
+ case 'DOUBLE':
43
+ case 'double':
44
+ return 'double precision';
45
+ case 'TIMESTAMPTZ':
46
+ case 'timestamptz':
47
+ return 'timestamptz';
48
+ case 'TIMESTAMP':
49
+ case 'timestamp':
50
+ return 'timestamp';
51
+ case 'DATE':
52
+ case 'date':
53
+ return 'date';
54
+ case 'DATETIME':
55
+ case 'datetime':
56
+ return 'timestamp';
57
+ case 'VARCHAR':
58
+ case 'varchar':
59
+ return column.args?.length ? `varchar(${column.args[0]})` : 'varchar';
60
+ case 'TEXT':
61
+ case 'text':
62
+ return 'text';
63
+ case 'ENUM':
64
+ case 'enum':
65
+ return 'text';
66
+ default:
67
+ return String(column.type).toLowerCase();
68
+ }
69
+ }
70
+
71
+ renderAutoIncrement(column: ColumnDef): string | undefined {
72
+ if (!column.autoIncrement) return undefined;
73
+ const strategy = column.generated === 'always' ? 'GENERATED ALWAYS' : 'GENERATED BY DEFAULT';
74
+ return `${strategy} AS IDENTITY`;
75
+ }
76
+
77
+ renderIndex(table: TableDef, index: IndexDef): string {
78
+ const name = index.name || deriveIndexName(table, index);
79
+ const cols = renderIndexColumns(this, index.columns);
80
+ const unique = index.unique ? 'UNIQUE ' : '';
81
+ const where = index.where ? ` WHERE ${index.where}` : '';
82
+ return `CREATE ${unique}INDEX IF NOT EXISTS ${this.quoteIdentifier(name)} ON ${this.formatTableName(table)} (${cols})${where};`;
83
+ }
84
+
85
+ supportsPartialIndexes(): boolean {
86
+ return true;
87
+ }
88
+
89
+ dropColumnSql(table: DatabaseTable, column: string): string[] {
90
+ return [`ALTER TABLE ${this.formatTableName(table)} DROP COLUMN ${this.quoteIdentifier(column)};`];
91
+ }
92
+
93
+ dropIndexSql(table: DatabaseTable, index: string): string[] {
94
+ const qualified = table.schema
95
+ ? `${this.quoteIdentifier(table.schema)}.${this.quoteIdentifier(index)}`
96
+ : this.quoteIdentifier(index);
97
+ return [`DROP INDEX IF EXISTS ${qualified};`];
98
+ }
99
+ }
@@ -0,0 +1,103 @@
1
+ import { BaseSchemaDialect } from './base-schema-dialect.js';
2
+ import {
3
+ deriveIndexName,
4
+ renderIndexColumns,
5
+ DialectName,
6
+ formatLiteral,
7
+ resolvePrimaryKey
8
+ } from '../schema-generator.js';
9
+ import { ColumnDef } from '../../../schema/column.js';
10
+ import { IndexDef, TableDef } from '../../../schema/table.js';
11
+ import { DatabaseTable } from '../schema-types.js';
12
+
13
+ export class SQLiteSchemaDialect extends BaseSchemaDialect {
14
+ name: DialectName = 'sqlite';
15
+
16
+ quoteIdentifier(id: string): string {
17
+ return `"${id}"`;
18
+ }
19
+
20
+ renderColumnType(column: ColumnDef): string {
21
+ switch (column.type) {
22
+ case 'INT':
23
+ case 'INTEGER':
24
+ case 'int':
25
+ case 'integer':
26
+ case 'BIGINT':
27
+ case 'bigint':
28
+ return 'INTEGER';
29
+ case 'BOOLEAN':
30
+ case 'boolean':
31
+ return 'INTEGER';
32
+ case 'DECIMAL':
33
+ case 'decimal':
34
+ case 'FLOAT':
35
+ case 'float':
36
+ case 'DOUBLE':
37
+ case 'double':
38
+ return 'REAL';
39
+ case 'DATE':
40
+ case 'date':
41
+ case 'DATETIME':
42
+ case 'datetime':
43
+ case 'TIMESTAMP':
44
+ case 'timestamp':
45
+ case 'TIMESTAMPTZ':
46
+ case 'timestamptz':
47
+ return 'TEXT';
48
+ case 'VARCHAR':
49
+ case 'varchar':
50
+ case 'TEXT':
51
+ case 'text':
52
+ case 'JSON':
53
+ case 'json':
54
+ case 'UUID':
55
+ case 'uuid':
56
+ return 'TEXT';
57
+ case 'ENUM':
58
+ case 'enum':
59
+ return 'TEXT';
60
+ default:
61
+ return 'TEXT';
62
+ }
63
+ }
64
+
65
+ renderAutoIncrement(column: ColumnDef, table: TableDef): string | undefined {
66
+ const pk = resolvePrimaryKey(table);
67
+ if (column.autoIncrement && pk.length === 1 && pk[0] === column.name) {
68
+ return 'PRIMARY KEY AUTOINCREMENT';
69
+ }
70
+ return undefined;
71
+ }
72
+
73
+ preferInlinePkAutoincrement(column: ColumnDef, table: TableDef, pk: string[]): boolean {
74
+ return !!(column.autoIncrement && pk.length === 1 && pk[0] === column.name);
75
+ }
76
+
77
+ renderDefault(value: unknown): string {
78
+ return formatLiteral(value, this.name);
79
+ }
80
+
81
+ renderIndex(table: TableDef, index: IndexDef): string {
82
+ if (index.where) {
83
+ throw new Error('SQLite does not support partial/filtered indexes');
84
+ }
85
+ const name = index.name || deriveIndexName(table, index);
86
+ const cols = renderIndexColumns(this, index.columns);
87
+ const unique = index.unique ? 'UNIQUE ' : '';
88
+ return `CREATE ${unique}INDEX IF NOT EXISTS ${this.quoteIdentifier(name)} ON ${this.formatTableName(table)} (${cols});`;
89
+ }
90
+
91
+ dropColumnSql(_table: DatabaseTable, _column: string): string[] {
92
+ return [];
93
+ }
94
+
95
+ dropIndexSql(_table: DatabaseTable, index: string): string[] {
96
+ return [`DROP INDEX IF EXISTS ${this.quoteIdentifier(index)};`];
97
+ }
98
+
99
+ warnDropColumn(table: DatabaseTable, column: string): string | undefined {
100
+ const key = table.schema ? `${table.schema}.${table.name}` : table.name;
101
+ return `Dropping columns on SQLite requires table rebuild (column ${column} on ${key}).`;
102
+ }
103
+ }
@@ -0,0 +1,149 @@
1
+ import { SchemaIntrospector, IntrospectOptions } from './types.js';
2
+ import { queryRows, shouldIncludeTable } from './utils.js';
3
+ import { DatabaseSchema, DatabaseTable, DatabaseIndex, DatabaseColumn } from '../schema-types.js';
4
+ import { DbExecutor } from '../../orm/db-executor.js';
5
+
6
+ export const mssqlIntrospector: SchemaIntrospector = {
7
+ async introspect(executor: DbExecutor, options: IntrospectOptions): Promise<DatabaseSchema> {
8
+ const schema = options.schema;
9
+ const filterSchema = schema ? 'sch.name = @p1' : '1=1';
10
+ const params = schema ? [schema] : [];
11
+
12
+ const columnRows = await queryRows(
13
+ executor,
14
+ `
15
+ SELECT
16
+ sch.name AS table_schema,
17
+ t.name AS table_name,
18
+ c.name AS column_name,
19
+ ty.name AS data_type,
20
+ c.is_nullable,
21
+ c.is_identity,
22
+ object_definition(c.default_object_id) AS column_default
23
+ FROM sys.columns c
24
+ JOIN sys.tables t ON t.object_id = c.object_id
25
+ JOIN sys.schemas sch ON sch.schema_id = t.schema_id
26
+ JOIN sys.types ty ON ty.user_type_id = c.user_type_id
27
+ WHERE t.is_ms_shipped = 0 AND ${filterSchema}
28
+ `,
29
+ params
30
+ );
31
+
32
+ const pkRows = await queryRows(
33
+ executor,
34
+ `
35
+ SELECT
36
+ sch.name AS table_schema,
37
+ t.name AS table_name,
38
+ c.name AS column_name,
39
+ ic.key_ordinal
40
+ FROM sys.indexes i
41
+ JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
42
+ JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
43
+ JOIN sys.tables t ON t.object_id = i.object_id
44
+ JOIN sys.schemas sch ON sch.schema_id = t.schema_id
45
+ WHERE i.is_primary_key = 1 AND ${filterSchema}
46
+ ORDER BY ic.key_ordinal
47
+ `,
48
+ params
49
+ );
50
+
51
+ const pkMap = new Map<string, string[]>();
52
+ pkRows.forEach(r => {
53
+ const key = `${r.table_schema}.${r.table_name}`;
54
+ const list = pkMap.get(key) || [];
55
+ list.push(r.column_name);
56
+ pkMap.set(key, list);
57
+ });
58
+
59
+ const indexRows = await queryRows(
60
+ executor,
61
+ `
62
+ SELECT
63
+ sch.name AS table_schema,
64
+ t.name AS table_name,
65
+ i.name AS index_name,
66
+ i.is_unique,
67
+ i.has_filter,
68
+ i.filter_definition
69
+ FROM sys.indexes i
70
+ JOIN sys.tables t ON t.object_id = i.object_id
71
+ JOIN sys.schemas sch ON sch.schema_id = t.schema_id
72
+ WHERE i.is_primary_key = 0 AND i.is_hypothetical = 0 AND ${filterSchema}
73
+ `,
74
+ params
75
+ );
76
+
77
+ const indexColsRows = await queryRows(
78
+ executor,
79
+ `
80
+ SELECT
81
+ sch.name AS table_schema,
82
+ t.name AS table_name,
83
+ i.name AS index_name,
84
+ c.name AS column_name,
85
+ ic.key_ordinal
86
+ FROM sys.index_columns ic
87
+ JOIN sys.indexes i ON i.object_id = ic.object_id AND i.index_id = ic.index_id
88
+ JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
89
+ JOIN sys.tables t ON t.object_id = i.object_id
90
+ JOIN sys.schemas sch ON sch.schema_id = t.schema_id
91
+ WHERE i.is_primary_key = 0 AND ${filterSchema}
92
+ ORDER BY ic.key_ordinal
93
+ `,
94
+ params
95
+ );
96
+
97
+ const indexColumnsMap = new Map<string, { column: string; order: number }[]>();
98
+ indexColsRows.forEach(r => {
99
+ const key = `${r.table_schema}.${r.table_name}.${r.index_name}`;
100
+ const list = indexColumnsMap.get(key) || [];
101
+ list.push({ column: r.column_name, order: r.key_ordinal });
102
+ indexColumnsMap.set(key, list);
103
+ });
104
+
105
+ const tablesByKey = new Map<string, DatabaseTable>();
106
+
107
+ columnRows.forEach(r => {
108
+ if (!shouldIncludeTable(r.table_name, options)) return;
109
+ const key = `${r.table_schema}.${r.table_name}`;
110
+ if (!tablesByKey.has(key)) {
111
+ tablesByKey.set(key, {
112
+ name: r.table_name,
113
+ schema: r.table_schema,
114
+ columns: [],
115
+ primaryKey: pkMap.get(key) || [],
116
+ indexes: []
117
+ });
118
+ }
119
+ const t = tablesByKey.get(key)!;
120
+ const column: DatabaseColumn = {
121
+ name: r.column_name,
122
+ type: r.data_type,
123
+ notNull: r.is_nullable === false || r.is_nullable === 0,
124
+ default: r.column_default ?? undefined,
125
+ autoIncrement: !!r.is_identity
126
+ };
127
+ t.columns.push(column);
128
+ });
129
+
130
+ indexRows.forEach(r => {
131
+ const key = `${r.table_schema}.${r.table_name}`;
132
+ const table = tablesByKey.get(key);
133
+ if (!table) return;
134
+ const cols = (indexColumnsMap.get(`${r.table_schema}.${r.table_name}.${r.index_name}`) || [])
135
+ .sort((a, b) => a.order - b.order)
136
+ .map(c => ({ column: c.column }));
137
+ const idx: DatabaseIndex = {
138
+ name: r.index_name,
139
+ columns: cols,
140
+ unique: !!r.is_unique,
141
+ where: r.has_filter ? r.filter_definition : undefined
142
+ };
143
+ table.indexes = table.indexes || [];
144
+ table.indexes.push(idx);
145
+ });
146
+
147
+ return { tables: Array.from(tablesByKey.values()) };
148
+ }
149
+ };
@@ -0,0 +1,99 @@
1
+ import { SchemaIntrospector, IntrospectOptions } from './types.js';
2
+ import { queryRows, shouldIncludeTable } from './utils.js';
3
+ import { DatabaseSchema, DatabaseTable, DatabaseIndex, DatabaseColumn } from '../schema-types.js';
4
+ import { DbExecutor } from '../../orm/db-executor.js';
5
+
6
+ export const mysqlIntrospector: SchemaIntrospector = {
7
+ async introspect(executor: DbExecutor, options: IntrospectOptions): Promise<DatabaseSchema> {
8
+ const schema = options.schema;
9
+ const filterClause = schema ? 'table_schema = ?' : 'table_schema = database()';
10
+ const params = schema ? [schema] : [];
11
+
12
+ const columnRows = await queryRows(
13
+ executor,
14
+ `
15
+ SELECT table_schema, table_name, column_name, data_type, is_nullable, column_default, extra
16
+ FROM information_schema.columns
17
+ WHERE ${filterClause}
18
+ ORDER BY table_name, ordinal_position
19
+ `,
20
+ params
21
+ );
22
+
23
+ const pkRows = await queryRows(
24
+ executor,
25
+ `
26
+ SELECT table_schema, table_name, column_name
27
+ FROM information_schema.key_column_usage
28
+ WHERE constraint_name = 'PRIMARY' AND ${filterClause}
29
+ ORDER BY ordinal_position
30
+ `,
31
+ params
32
+ );
33
+
34
+ const pkMap = new Map<string, string[]>();
35
+ pkRows.forEach(r => {
36
+ const key = `${r.table_schema}.${r.table_name}`;
37
+ const list = pkMap.get(key) || [];
38
+ list.push(r.column_name);
39
+ pkMap.set(key, list);
40
+ });
41
+
42
+ const indexRows = await queryRows(
43
+ executor,
44
+ `
45
+ SELECT
46
+ table_schema,
47
+ table_name,
48
+ index_name,
49
+ non_unique,
50
+ GROUP_CONCAT(column_name ORDER BY seq_in_index) AS cols
51
+ FROM information_schema.statistics
52
+ WHERE ${filterClause} AND index_name <> 'PRIMARY'
53
+ GROUP BY table_schema, table_name, index_name, non_unique
54
+ `,
55
+ params
56
+ );
57
+
58
+ const tablesByKey = new Map<string, DatabaseTable>();
59
+
60
+ columnRows.forEach(r => {
61
+ const key = `${r.table_schema}.${r.table_name}`;
62
+ if (!shouldIncludeTable(r.table_name, options)) return;
63
+ if (!tablesByKey.has(key)) {
64
+ tablesByKey.set(key, {
65
+ name: r.table_name,
66
+ schema: r.table_schema,
67
+ columns: [],
68
+ primaryKey: pkMap.get(key) || [],
69
+ indexes: []
70
+ });
71
+ }
72
+ const cols = tablesByKey.get(key)!;
73
+ const column: DatabaseColumn = {
74
+ name: r.column_name,
75
+ type: r.data_type,
76
+ notNull: r.is_nullable === 'NO',
77
+ default: r.column_default ?? undefined,
78
+ autoIncrement: typeof r.extra === 'string' && r.extra.includes('auto_increment')
79
+ };
80
+ cols.columns.push(column);
81
+ });
82
+
83
+ indexRows.forEach(r => {
84
+ const key = `${r.table_schema}.${r.table_name}`;
85
+ const table = tablesByKey.get(key);
86
+ if (!table) return;
87
+ const cols = (typeof r.cols === 'string' ? r.cols.split(',') : []).map((c: string) => ({ column: c.trim() }));
88
+ const idx: DatabaseIndex = {
89
+ name: r.index_name,
90
+ columns: cols,
91
+ unique: r.non_unique === 0
92
+ };
93
+ table.indexes = table.indexes || [];
94
+ table.indexes.push(idx);
95
+ });
96
+
97
+ return { tables: Array.from(tablesByKey.values()) };
98
+ }
99
+ };