bunql 1.0.1-dev.4 → 1.0.1-dev.5
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.
- package/README.md +91 -3
- package/package.json +1 -1
- package/src/index.test.ts +4 -0
- package/src/index.ts +408 -171
- package/src/schema-new.ts +563 -0
- package/src/schema.test.ts +2 -3
- package/src/schema.ts +214 -618
package/src/schema.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
export interface DatabaseInfo {
|
|
4
|
-
type: 'sqlite' | 'postgresql' | 'mysql';
|
|
5
|
-
version?: string;
|
|
6
|
-
}
|
|
1
|
+
// PostgreSQL-only schema management for BunQL
|
|
7
2
|
|
|
8
3
|
export interface ColumnDefinition {
|
|
9
4
|
name: string;
|
|
@@ -54,31 +49,10 @@ export class SchemaBuilder {
|
|
|
54
49
|
private indexes: IndexInfo[] = [];
|
|
55
50
|
private foreignKeys: ForeignKeyInfo[] = [];
|
|
56
51
|
private options: CreateTableOptions = {};
|
|
57
|
-
private databaseType: 'sqlite' | 'postgresql' | 'mysql' = 'sqlite';
|
|
58
52
|
|
|
59
53
|
constructor(db: any, tableName: string) {
|
|
60
54
|
this.db = db;
|
|
61
55
|
this.tableName = tableName;
|
|
62
|
-
this.detectDatabaseType();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Detect the database type
|
|
67
|
-
*/
|
|
68
|
-
private async detectDatabaseType(): Promise<void> {
|
|
69
|
-
try {
|
|
70
|
-
const connectionString = process.env.DATABASE_URL || '';
|
|
71
|
-
|
|
72
|
-
if (connectionString.includes('postgresql://') || connectionString.includes('postgres://')) {
|
|
73
|
-
this.databaseType = 'postgresql';
|
|
74
|
-
} else if (connectionString.includes('mysql://') || connectionString.includes('mariadb://')) {
|
|
75
|
-
this.databaseType = 'mysql';
|
|
76
|
-
} else {
|
|
77
|
-
this.databaseType = 'sqlite';
|
|
78
|
-
}
|
|
79
|
-
} catch (error) {
|
|
80
|
-
this.databaseType = 'sqlite';
|
|
81
|
-
}
|
|
82
56
|
}
|
|
83
57
|
|
|
84
58
|
/**
|
|
@@ -125,48 +99,28 @@ export class SchemaBuilder {
|
|
|
125
99
|
* Execute the table creation
|
|
126
100
|
*/
|
|
127
101
|
async execute(): Promise<void> {
|
|
128
|
-
if (this.columns.length === 0) {
|
|
129
|
-
throw new Error('At least one column must be defined');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
await this.detectDatabaseType();
|
|
133
102
|
const sql = this.buildCreateTableSQL();
|
|
134
103
|
await this.db.run(sql);
|
|
135
|
-
|
|
136
|
-
// Create indexes
|
|
104
|
+
|
|
105
|
+
// Create indexes separately
|
|
137
106
|
for (const index of this.indexes) {
|
|
138
107
|
await this.createIndex(index);
|
|
139
108
|
}
|
|
140
109
|
}
|
|
141
110
|
|
|
142
111
|
private buildCreateTableSQL(): string {
|
|
143
|
-
|
|
144
112
|
const ifNotExists = this.options.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
145
113
|
const temporary = this.options.temporary ? 'TEMPORARY ' : '';
|
|
146
114
|
|
|
147
|
-
let sql = `CREATE ${temporary}TABLE ${ifNotExists}`;
|
|
148
|
-
|
|
149
|
-
// Handle different quoting for different databases
|
|
150
|
-
switch (this.databaseType) {
|
|
151
|
-
case 'postgresql':
|
|
152
|
-
sql += `"${this.tableName}" (`;
|
|
153
|
-
break;
|
|
154
|
-
case 'mysql':
|
|
155
|
-
sql += `\`${this.tableName}\` (`;
|
|
156
|
-
break;
|
|
157
|
-
case 'sqlite':
|
|
158
|
-
default:
|
|
159
|
-
sql += `"${this.tableName}" (`;
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
115
|
+
let sql = `CREATE ${temporary}TABLE ${ifNotExists}"${this.tableName}" (`;
|
|
162
116
|
|
|
163
117
|
const columnDefinitions = this.columns.map(col => {
|
|
164
|
-
let def =
|
|
118
|
+
let def = `"${col.name}" ${col.type}`;
|
|
165
119
|
|
|
166
120
|
if (col.primaryKey) {
|
|
167
121
|
def += ' PRIMARY KEY';
|
|
168
|
-
if (col.autoIncrement
|
|
169
|
-
def +=
|
|
122
|
+
if (col.autoIncrement) {
|
|
123
|
+
def += ' GENERATED ALWAYS AS IDENTITY';
|
|
170
124
|
}
|
|
171
125
|
}
|
|
172
126
|
|
|
@@ -179,7 +133,11 @@ export class SchemaBuilder {
|
|
|
179
133
|
}
|
|
180
134
|
|
|
181
135
|
if (col.defaultValue !== undefined) {
|
|
182
|
-
|
|
136
|
+
if (typeof col.defaultValue === 'string') {
|
|
137
|
+
def += ` DEFAULT '${col.defaultValue}'`;
|
|
138
|
+
} else {
|
|
139
|
+
def += ` DEFAULT ${col.defaultValue}`;
|
|
140
|
+
}
|
|
183
141
|
}
|
|
184
142
|
|
|
185
143
|
return def;
|
|
@@ -187,10 +145,10 @@ export class SchemaBuilder {
|
|
|
187
145
|
|
|
188
146
|
// Add foreign key constraints
|
|
189
147
|
const foreignKeyDefinitions = this.foreignKeys.map(fk => {
|
|
190
|
-
const columns = fk.columns.map(col =>
|
|
191
|
-
const referencedColumns = fk.referencedColumns.map(col =>
|
|
148
|
+
const columns = fk.columns.map(col => `"${col}"`).join(', ');
|
|
149
|
+
const referencedColumns = fk.referencedColumns.map(col => `"${col}"`).join(', ');
|
|
192
150
|
|
|
193
|
-
let fkDef = `FOREIGN KEY (${columns}) REFERENCES ${
|
|
151
|
+
let fkDef = `FOREIGN KEY (${columns}) REFERENCES "${fk.referencedTable}" (${referencedColumns})`;
|
|
194
152
|
|
|
195
153
|
if (fk.onDelete) {
|
|
196
154
|
fkDef += ` ON DELETE ${fk.onDelete}`;
|
|
@@ -210,120 +168,10 @@ export class SchemaBuilder {
|
|
|
210
168
|
return sql;
|
|
211
169
|
}
|
|
212
170
|
|
|
213
|
-
/**
|
|
214
|
-
* Quote column names based on database type
|
|
215
|
-
*/
|
|
216
|
-
private quoteColumn(columnName: string): string {
|
|
217
|
-
switch (this.databaseType) {
|
|
218
|
-
case 'postgresql':
|
|
219
|
-
return `"${columnName}"`;
|
|
220
|
-
case 'mysql':
|
|
221
|
-
return `\`${columnName}\``;
|
|
222
|
-
case 'sqlite':
|
|
223
|
-
default:
|
|
224
|
-
return `"${columnName}"`;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Quote table names based on database type
|
|
230
|
-
*/
|
|
231
|
-
private quoteTable(tableName: string): string {
|
|
232
|
-
switch (this.databaseType) {
|
|
233
|
-
case 'postgresql':
|
|
234
|
-
return `"${tableName}"`;
|
|
235
|
-
case 'mysql':
|
|
236
|
-
return `\`${tableName}\``;
|
|
237
|
-
case 'sqlite':
|
|
238
|
-
default:
|
|
239
|
-
return `"${tableName}"`;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Map generic types to database-specific types
|
|
245
|
-
*/
|
|
246
|
-
private mapColumnType(type: string): string {
|
|
247
|
-
const typeMap: Record<string, Record<string, string>> = {
|
|
248
|
-
sqlite: {
|
|
249
|
-
'INTEGER': 'INTEGER',
|
|
250
|
-
'TEXT': 'TEXT',
|
|
251
|
-
'REAL': 'REAL',
|
|
252
|
-
'BLOB': 'BLOB',
|
|
253
|
-
'BOOLEAN': 'INTEGER',
|
|
254
|
-
'DATE': 'TEXT',
|
|
255
|
-
'DATETIME': 'TEXT',
|
|
256
|
-
'TIMESTAMP': 'TEXT'
|
|
257
|
-
},
|
|
258
|
-
postgresql: {
|
|
259
|
-
'INTEGER': 'INTEGER',
|
|
260
|
-
'TEXT': 'TEXT',
|
|
261
|
-
'REAL': 'REAL',
|
|
262
|
-
'BLOB': 'BYTEA',
|
|
263
|
-
'BOOLEAN': 'BOOLEAN',
|
|
264
|
-
'DATE': 'DATE',
|
|
265
|
-
'DATETIME': 'TIMESTAMP',
|
|
266
|
-
'TIMESTAMP': 'TIMESTAMP'
|
|
267
|
-
},
|
|
268
|
-
mysql: {
|
|
269
|
-
'INTEGER': 'INT',
|
|
270
|
-
'TEXT': 'TEXT',
|
|
271
|
-
'REAL': 'DOUBLE',
|
|
272
|
-
'BLOB': 'BLOB',
|
|
273
|
-
'BOOLEAN': 'BOOLEAN',
|
|
274
|
-
'DATE': 'DATE',
|
|
275
|
-
'DATETIME': 'DATETIME',
|
|
276
|
-
'TIMESTAMP': 'TIMESTAMP'
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
return typeMap[this.databaseType]?.[type.toUpperCase()] || type;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Get auto-increment keyword based on database type
|
|
285
|
-
*/
|
|
286
|
-
private getAutoIncrementKeyword(): string {
|
|
287
|
-
switch (this.databaseType) {
|
|
288
|
-
case 'mysql':
|
|
289
|
-
return ' AUTO_INCREMENT';
|
|
290
|
-
case 'sqlite':
|
|
291
|
-
return ' AUTOINCREMENT';
|
|
292
|
-
case 'postgresql':
|
|
293
|
-
return ''; // PostgreSQL uses SERIAL or IDENTITY
|
|
294
|
-
default:
|
|
295
|
-
return ' AUTOINCREMENT';
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Format default values based on database type
|
|
301
|
-
*/
|
|
302
|
-
private formatDefaultValue(value: any): string {
|
|
303
|
-
if (typeof value === 'string') {
|
|
304
|
-
return ` DEFAULT '${value}'`;
|
|
305
|
-
} else if (value === null) {
|
|
306
|
-
return ' DEFAULT NULL';
|
|
307
|
-
} else if (typeof value === 'boolean') {
|
|
308
|
-
switch (this.databaseType) {
|
|
309
|
-
case 'postgresql':
|
|
310
|
-
return ` DEFAULT ${value}`;
|
|
311
|
-
case 'mysql':
|
|
312
|
-
return ` DEFAULT ${value ? 1 : 0}`;
|
|
313
|
-
case 'sqlite':
|
|
314
|
-
default:
|
|
315
|
-
return ` DEFAULT ${value ? 1 : 0}`;
|
|
316
|
-
}
|
|
317
|
-
} else {
|
|
318
|
-
return ` DEFAULT ${value}`;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
171
|
private async createIndex(index: IndexInfo): Promise<void> {
|
|
323
172
|
const unique = index.unique ? 'UNIQUE ' : '';
|
|
324
173
|
const indexName = index.name || `idx_${this.tableName}_${index.columns.join('_')}`;
|
|
325
174
|
const columns = index.columns.map(col => `"${col}"`).join(', ');
|
|
326
|
-
|
|
327
175
|
const sql = `CREATE ${unique}INDEX "${indexName}" ON "${this.tableName}" (${columns})`;
|
|
328
176
|
await this.db.run(sql);
|
|
329
177
|
}
|
|
@@ -347,10 +195,7 @@ export class AlterTableBuilder {
|
|
|
347
195
|
* Add a column to the table
|
|
348
196
|
*/
|
|
349
197
|
addColumn(column: ColumnDefinition): this {
|
|
350
|
-
this.operations.push({
|
|
351
|
-
type: 'addColumn',
|
|
352
|
-
data: column
|
|
353
|
-
});
|
|
198
|
+
this.operations.push({ type: 'addColumn', data: column });
|
|
354
199
|
return this;
|
|
355
200
|
}
|
|
356
201
|
|
|
@@ -358,10 +203,7 @@ export class AlterTableBuilder {
|
|
|
358
203
|
* Drop a column from the table
|
|
359
204
|
*/
|
|
360
205
|
dropColumn(columnName: string): this {
|
|
361
|
-
this.operations.push({
|
|
362
|
-
type: 'dropColumn',
|
|
363
|
-
data: { columnName }
|
|
364
|
-
});
|
|
206
|
+
this.operations.push({ type: 'dropColumn', data: columnName });
|
|
365
207
|
return this;
|
|
366
208
|
}
|
|
367
209
|
|
|
@@ -369,21 +211,15 @@ export class AlterTableBuilder {
|
|
|
369
211
|
* Rename a column
|
|
370
212
|
*/
|
|
371
213
|
renameColumn(oldName: string, newName: string): this {
|
|
372
|
-
this.operations.push({
|
|
373
|
-
type: 'renameColumn',
|
|
374
|
-
data: { oldName, newName }
|
|
375
|
-
});
|
|
214
|
+
this.operations.push({ type: 'renameColumn', data: { oldName, newName } });
|
|
376
215
|
return this;
|
|
377
216
|
}
|
|
378
217
|
|
|
379
218
|
/**
|
|
380
|
-
* Alter a column
|
|
219
|
+
* Alter a column
|
|
381
220
|
*/
|
|
382
|
-
alterColumn(columnName: string,
|
|
383
|
-
this.operations.push({
|
|
384
|
-
type: 'alterColumn',
|
|
385
|
-
data: { columnName, newDefinition }
|
|
386
|
-
});
|
|
221
|
+
alterColumn(columnName: string, changes: Partial<ColumnDefinition>): this {
|
|
222
|
+
this.operations.push({ type: 'alterColumn', data: { columnName, changes } });
|
|
387
223
|
return this;
|
|
388
224
|
}
|
|
389
225
|
|
|
@@ -391,10 +227,7 @@ export class AlterTableBuilder {
|
|
|
391
227
|
* Add an index
|
|
392
228
|
*/
|
|
393
229
|
addIndex(index: IndexInfo): this {
|
|
394
|
-
this.operations.push({
|
|
395
|
-
type: 'addIndex',
|
|
396
|
-
data: index
|
|
397
|
-
});
|
|
230
|
+
this.operations.push({ type: 'addIndex', data: index });
|
|
398
231
|
return this;
|
|
399
232
|
}
|
|
400
233
|
|
|
@@ -402,10 +235,7 @@ export class AlterTableBuilder {
|
|
|
402
235
|
* Drop an index
|
|
403
236
|
*/
|
|
404
237
|
dropIndex(indexName: string): this {
|
|
405
|
-
this.operations.push({
|
|
406
|
-
type: 'dropIndex',
|
|
407
|
-
data: { indexName }
|
|
408
|
-
});
|
|
238
|
+
this.operations.push({ type: 'dropIndex', data: indexName });
|
|
409
239
|
return this;
|
|
410
240
|
}
|
|
411
241
|
|
|
@@ -413,10 +243,7 @@ export class AlterTableBuilder {
|
|
|
413
243
|
* Add a foreign key constraint
|
|
414
244
|
*/
|
|
415
245
|
addForeignKey(foreignKey: ForeignKeyInfo): this {
|
|
416
|
-
this.operations.push({
|
|
417
|
-
type: 'addForeignKey',
|
|
418
|
-
data: foreignKey
|
|
419
|
-
});
|
|
246
|
+
this.operations.push({ type: 'addForeignKey', data: foreignKey });
|
|
420
247
|
return this;
|
|
421
248
|
}
|
|
422
249
|
|
|
@@ -424,10 +251,7 @@ export class AlterTableBuilder {
|
|
|
424
251
|
* Drop a foreign key constraint
|
|
425
252
|
*/
|
|
426
253
|
dropForeignKey(constraintName: string): this {
|
|
427
|
-
this.operations.push({
|
|
428
|
-
type: 'dropForeignKey',
|
|
429
|
-
data: { constraintName }
|
|
430
|
-
});
|
|
254
|
+
this.operations.push({ type: 'dropForeignKey', data: constraintName });
|
|
431
255
|
return this;
|
|
432
256
|
}
|
|
433
257
|
|
|
@@ -449,173 +273,78 @@ export class AlterTableBuilder {
|
|
|
449
273
|
}
|
|
450
274
|
|
|
451
275
|
private async executeOperation(operation: any): Promise<void> {
|
|
276
|
+
const ifExists = this.options.ifExists ? 'IF EXISTS ' : '';
|
|
277
|
+
|
|
452
278
|
switch (operation.type) {
|
|
453
279
|
case 'addColumn':
|
|
454
|
-
|
|
280
|
+
const column = operation.data as ColumnDefinition;
|
|
281
|
+
let columnDef = `"${column.name}" ${column.type}`;
|
|
282
|
+
|
|
283
|
+
if (column.notNull) {
|
|
284
|
+
columnDef += ' NOT NULL';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (column.defaultValue !== undefined) {
|
|
288
|
+
if (typeof column.defaultValue === 'string') {
|
|
289
|
+
columnDef += ` DEFAULT '${column.defaultValue}'`;
|
|
290
|
+
} else {
|
|
291
|
+
columnDef += ` DEFAULT ${column.defaultValue}`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
await this.db.run(`ALTER TABLE "${this.tableName}" ADD COLUMN ${columnDef}`);
|
|
455
296
|
break;
|
|
297
|
+
|
|
456
298
|
case 'dropColumn':
|
|
457
|
-
await this.
|
|
299
|
+
await this.db.run(`ALTER TABLE "${this.tableName}" DROP COLUMN ${ifExists}"${operation.data}"`);
|
|
458
300
|
break;
|
|
301
|
+
|
|
459
302
|
case 'renameColumn':
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
case 'alterColumn':
|
|
463
|
-
await this.alterColumnOperation(operation.data);
|
|
303
|
+
const { oldName, newName } = operation.data;
|
|
304
|
+
await this.db.run(`ALTER TABLE "${this.tableName}" RENAME COLUMN "${oldName}" TO "${newName}"`);
|
|
464
305
|
break;
|
|
306
|
+
|
|
465
307
|
case 'addIndex':
|
|
466
|
-
|
|
308
|
+
const index = operation.data as IndexInfo;
|
|
309
|
+
const unique = index.unique ? 'UNIQUE ' : '';
|
|
310
|
+
const indexName = index.name || `idx_${this.tableName}_${index.columns.join('_')}`;
|
|
311
|
+
const columns = index.columns.map(col => `"${col}"`).join(', ');
|
|
312
|
+
await this.db.run(`CREATE ${unique}INDEX "${indexName}" ON "${this.tableName}" (${columns})`);
|
|
467
313
|
break;
|
|
314
|
+
|
|
468
315
|
case 'dropIndex':
|
|
469
|
-
await this.
|
|
316
|
+
await this.db.run(`DROP INDEX ${ifExists}"${operation.data}"`);
|
|
470
317
|
break;
|
|
318
|
+
|
|
471
319
|
case 'addForeignKey':
|
|
472
|
-
|
|
320
|
+
const fk = operation.data as ForeignKeyInfo;
|
|
321
|
+
const fkColumns = fk.columns.map(col => `"${col}"`).join(', ');
|
|
322
|
+
const refColumns = fk.referencedColumns.map(col => `"${col}"`).join(', ');
|
|
323
|
+
let fkDef = `FOREIGN KEY (${fkColumns}) REFERENCES "${fk.referencedTable}" (${refColumns})`;
|
|
324
|
+
|
|
325
|
+
if (fk.onDelete) {
|
|
326
|
+
fkDef += ` ON DELETE ${fk.onDelete}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (fk.onUpdate) {
|
|
330
|
+
fkDef += ` ON UPDATE ${fk.onUpdate}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
await this.db.run(`ALTER TABLE "${this.tableName}" ADD CONSTRAINT "${fk.name}" ${fkDef}`);
|
|
473
334
|
break;
|
|
335
|
+
|
|
474
336
|
case 'dropForeignKey':
|
|
475
|
-
await this.
|
|
337
|
+
await this.db.run(`ALTER TABLE "${this.tableName}" DROP CONSTRAINT ${ifExists}"${operation.data}"`);
|
|
476
338
|
break;
|
|
477
339
|
}
|
|
478
340
|
}
|
|
479
|
-
|
|
480
|
-
private async addColumnOperation(column: ColumnDefinition): Promise<void> {
|
|
481
|
-
let sql = `ALTER TABLE "${this.tableName}" ADD COLUMN `;
|
|
482
|
-
sql += `"${column.name}" ${column.type}`;
|
|
483
|
-
|
|
484
|
-
if (column.notNull) {
|
|
485
|
-
sql += ' NOT NULL';
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (column.defaultValue !== undefined) {
|
|
489
|
-
if (typeof column.defaultValue === 'string') {
|
|
490
|
-
sql += ` DEFAULT '${column.defaultValue}'`;
|
|
491
|
-
} else {
|
|
492
|
-
sql += ` DEFAULT ${column.defaultValue}`;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
await this.db.run(sql);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
private async dropColumnOperation(data: { columnName: string }): Promise<void> {
|
|
500
|
-
// SQLite doesn't support DROP COLUMN directly, so we need to recreate the table
|
|
501
|
-
// This is a simplified implementation
|
|
502
|
-
const sql = `ALTER TABLE "${this.tableName}" DROP COLUMN "${data.columnName}"`;
|
|
503
|
-
await this.db.run(sql);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
private async renameColumnOperation(data: { oldName: string; newName: string }): Promise<void> {
|
|
507
|
-
const sql = `ALTER TABLE "${this.tableName}" RENAME COLUMN "${data.oldName}" TO "${data.newName}"`;
|
|
508
|
-
await this.db.run(sql);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
private async alterColumnOperation(data: { columnName: string; newDefinition: Partial<ColumnDefinition> }): Promise<void> {
|
|
512
|
-
// SQLite has limited ALTER COLUMN support, so we'll use a basic approach
|
|
513
|
-
// In practice, you might need to recreate the table for complex changes
|
|
514
|
-
const sql = `ALTER TABLE "${this.tableName}" ALTER COLUMN "${data.columnName}" SET DATA TYPE ${data.newDefinition.type}`;
|
|
515
|
-
await this.db.run(sql);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
private async addIndexOperation(index: IndexInfo): Promise<void> {
|
|
519
|
-
const unique = index.unique ? 'UNIQUE ' : '';
|
|
520
|
-
const indexName = index.name || `idx_${this.tableName}_${index.columns.join('_')}`;
|
|
521
|
-
const columns = index.columns.map(col => `"${col}"`).join(', ');
|
|
522
|
-
|
|
523
|
-
const sql = `CREATE ${unique}INDEX "${indexName}" ON "${this.tableName}" (${columns})`;
|
|
524
|
-
await this.db.run(sql);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
private async dropIndexOperation(data: { indexName: string }): Promise<void> {
|
|
528
|
-
const sql = `DROP INDEX "${data.indexName}"`;
|
|
529
|
-
await this.db.run(sql);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
private async addForeignKeyOperation(foreignKey: ForeignKeyInfo): Promise<void> {
|
|
533
|
-
const columns = foreignKey.columns.map(col => `"${col}"`).join(', ');
|
|
534
|
-
const referencedColumns = foreignKey.referencedColumns.map(col => `"${col}"`).join(', ');
|
|
535
|
-
|
|
536
|
-
let sql = `ALTER TABLE "${this.tableName}" ADD `;
|
|
537
|
-
sql += `FOREIGN KEY (${columns}) REFERENCES "${foreignKey.referencedTable}" (${referencedColumns})`;
|
|
538
|
-
|
|
539
|
-
if (foreignKey.onDelete) {
|
|
540
|
-
sql += ` ON DELETE ${foreignKey.onDelete}`;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (foreignKey.onUpdate) {
|
|
544
|
-
sql += ` ON UPDATE ${foreignKey.onUpdate}`;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
await this.db.run(sql);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
private async dropForeignKeyOperation(data: { constraintName: string }): Promise<void> {
|
|
551
|
-
// SQLite doesn't support dropping foreign key constraints directly
|
|
552
|
-
// This would require recreating the table in practice
|
|
553
|
-
const sql = `ALTER TABLE "${this.tableName}" DROP CONSTRAINT "${data.constraintName}"`;
|
|
554
|
-
await this.db.run(sql);
|
|
555
|
-
}
|
|
556
341
|
}
|
|
557
342
|
|
|
558
343
|
export class Schema {
|
|
559
344
|
private db: any; // Will be BunQL instance
|
|
560
|
-
private databaseType: 'sqlite' | 'postgresql' | 'mysql' = 'sqlite';
|
|
561
345
|
|
|
562
346
|
constructor(db: any) {
|
|
563
347
|
this.db = db;
|
|
564
|
-
this.detectDatabaseType();
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Detect the database type from connection string or environment
|
|
569
|
-
*/
|
|
570
|
-
private async detectDatabaseType(): Promise<void> {
|
|
571
|
-
try {
|
|
572
|
-
// Try to detect from connection string or environment
|
|
573
|
-
const connectionString = process.env.DATABASE_URL || '';
|
|
574
|
-
|
|
575
|
-
if (connectionString.includes('postgresql://') || connectionString.includes('postgres://')) {
|
|
576
|
-
this.databaseType = 'postgresql';
|
|
577
|
-
} else if (connectionString.includes('mysql://') || connectionString.includes('mariadb://')) {
|
|
578
|
-
this.databaseType = 'mysql';
|
|
579
|
-
} else {
|
|
580
|
-
// Default to SQLite for local development
|
|
581
|
-
this.databaseType = 'sqlite';
|
|
582
|
-
}
|
|
583
|
-
} catch (error) {
|
|
584
|
-
// Fallback to SQLite
|
|
585
|
-
this.databaseType = 'sqlite';
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Get database information
|
|
591
|
-
*/
|
|
592
|
-
async getDatabaseInfo(): Promise<DatabaseInfo> {
|
|
593
|
-
await this.detectDatabaseType();
|
|
594
|
-
|
|
595
|
-
let version = '';
|
|
596
|
-
try {
|
|
597
|
-
switch (this.databaseType) {
|
|
598
|
-
case 'postgresql':
|
|
599
|
-
const pgResult = await this.db.get('SELECT version() as version');
|
|
600
|
-
version = pgResult?.version || '';
|
|
601
|
-
break;
|
|
602
|
-
case 'mysql':
|
|
603
|
-
const mysqlResult = await this.db.get('SELECT VERSION() as version');
|
|
604
|
-
version = mysqlResult?.version || '';
|
|
605
|
-
break;
|
|
606
|
-
case 'sqlite':
|
|
607
|
-
const sqliteResult = await this.db.get('SELECT sqlite_version() as version');
|
|
608
|
-
version = sqliteResult?.version || '';
|
|
609
|
-
break;
|
|
610
|
-
}
|
|
611
|
-
} catch (error) {
|
|
612
|
-
// Ignore version detection errors
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return {
|
|
616
|
-
type: this.databaseType,
|
|
617
|
-
version
|
|
618
|
-
};
|
|
619
348
|
}
|
|
620
349
|
|
|
621
350
|
/**
|
|
@@ -635,332 +364,199 @@ export class Schema {
|
|
|
635
364
|
/**
|
|
636
365
|
* Drop a table
|
|
637
366
|
*/
|
|
638
|
-
async dropTable(tableName: string, ifExists: boolean =
|
|
367
|
+
async dropTable(tableName: string, ifExists: boolean = false): Promise<void> {
|
|
639
368
|
const ifExistsClause = ifExists ? 'IF EXISTS ' : '';
|
|
640
|
-
|
|
641
|
-
await this.db.run(sql);
|
|
369
|
+
await this.db.run(`DROP TABLE ${ifExistsClause}"${tableName}"`);
|
|
642
370
|
}
|
|
643
371
|
|
|
644
372
|
/**
|
|
645
373
|
* Check if a table exists
|
|
646
374
|
*/
|
|
647
375
|
async hasTable(tableName: string): Promise<boolean> {
|
|
648
|
-
await this.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
376
|
+
const result = await this.db.all(`
|
|
377
|
+
SELECT EXISTS (
|
|
378
|
+
SELECT FROM information_schema.tables
|
|
379
|
+
WHERE table_schema = 'public'
|
|
380
|
+
AND table_name = ?
|
|
381
|
+
) as exists
|
|
382
|
+
`, [tableName]);
|
|
383
|
+
|
|
384
|
+
return result[0]?.exists || false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Get table information
|
|
389
|
+
*/
|
|
390
|
+
async getTableInfo(tableName: string): Promise<TableInfo> {
|
|
391
|
+
const columns = await this.db.all(`
|
|
392
|
+
SELECT
|
|
393
|
+
c.column_name,
|
|
394
|
+
c.data_type,
|
|
395
|
+
c.is_nullable,
|
|
396
|
+
c.column_default,
|
|
397
|
+
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key,
|
|
398
|
+
CASE WHEN u.column_name IS NOT NULL THEN true ELSE false END as is_unique
|
|
399
|
+
FROM information_schema.columns c
|
|
400
|
+
LEFT JOIN (
|
|
401
|
+
SELECT ku.column_name
|
|
402
|
+
FROM information_schema.table_constraints tc
|
|
403
|
+
JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name
|
|
404
|
+
WHERE tc.table_name = ? AND tc.constraint_type = 'PRIMARY KEY'
|
|
405
|
+
) pk ON c.column_name = pk.column_name
|
|
406
|
+
LEFT JOIN (
|
|
407
|
+
SELECT ku.column_name
|
|
408
|
+
FROM information_schema.table_constraints tc
|
|
409
|
+
JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name
|
|
410
|
+
WHERE tc.table_name = ? AND tc.constraint_type = 'UNIQUE'
|
|
411
|
+
) u ON c.column_name = u.column_name
|
|
412
|
+
WHERE c.table_name = ?
|
|
413
|
+
ORDER BY c.ordinal_position
|
|
414
|
+
`, [tableName, tableName, tableName]);
|
|
680
415
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
column_default as dflt_value,
|
|
698
|
-
CASE WHEN pk.column_name IS NOT NULL THEN 1 ELSE 0 END as pk
|
|
699
|
-
FROM information_schema.columns c
|
|
700
|
-
LEFT JOIN (
|
|
701
|
-
SELECT ku.column_name
|
|
702
|
-
FROM information_schema.table_constraints tc
|
|
703
|
-
JOIN information_schema.key_column_usage ku
|
|
704
|
-
ON tc.constraint_name = ku.constraint_name
|
|
705
|
-
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
706
|
-
AND tc.table_name = ?
|
|
707
|
-
) pk ON c.column_name = pk.column_name
|
|
708
|
-
WHERE c.table_name = ?
|
|
709
|
-
ORDER BY c.ordinal_position
|
|
710
|
-
`;
|
|
711
|
-
params = [tableName, tableName];
|
|
712
|
-
break;
|
|
713
|
-
case 'mysql':
|
|
714
|
-
query = `
|
|
715
|
-
SELECT
|
|
716
|
-
COLUMN_NAME as name,
|
|
717
|
-
DATA_TYPE as type,
|
|
718
|
-
IS_NULLABLE = 'NO' as notnull,
|
|
719
|
-
COLUMN_DEFAULT as dflt_value,
|
|
720
|
-
CASE WHEN COLUMN_KEY = 'PRI' THEN 1 ELSE 0 END as pk
|
|
721
|
-
FROM information_schema.columns
|
|
722
|
-
WHERE table_schema = DATABASE() AND table_name = ?
|
|
723
|
-
ORDER BY ordinal_position
|
|
724
|
-
`;
|
|
725
|
-
break;
|
|
726
|
-
case 'sqlite':
|
|
727
|
-
default:
|
|
728
|
-
query = `PRAGMA table_info("${tableName}")`;
|
|
729
|
-
params = [];
|
|
730
|
-
break;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const result = await this.db.all(query, params);
|
|
734
|
-
|
|
735
|
-
return result.map((row: any) => ({
|
|
736
|
-
name: row.name,
|
|
737
|
-
type: row.type,
|
|
738
|
-
notNull: row.notnull === 1 || row.notnull === true,
|
|
739
|
-
primaryKey: row.pk === 1 || row.pk === true,
|
|
740
|
-
unique: false, // Would need additional queries to detect this
|
|
741
|
-
defaultValue: row.dflt_value,
|
|
742
|
-
autoIncrement: false // Would need additional logic to detect this
|
|
743
|
-
}));
|
|
416
|
+
const indexes = await this.getIndexes(tableName);
|
|
417
|
+
const foreignKeys = await this.getForeignKeys(tableName);
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
name: tableName,
|
|
421
|
+
columns: columns.map((col: any) => ({
|
|
422
|
+
name: col.column_name,
|
|
423
|
+
type: col.data_type,
|
|
424
|
+
notNull: col.is_nullable === 'NO',
|
|
425
|
+
primaryKey: col.is_primary_key,
|
|
426
|
+
unique: col.is_unique,
|
|
427
|
+
defaultValue: col.column_default
|
|
428
|
+
})),
|
|
429
|
+
indexes,
|
|
430
|
+
foreignKeys
|
|
431
|
+
};
|
|
744
432
|
}
|
|
745
433
|
|
|
746
434
|
/**
|
|
747
|
-
* Get all tables
|
|
435
|
+
* Get all tables
|
|
748
436
|
*/
|
|
749
437
|
async getTables(): Promise<string[]> {
|
|
750
|
-
await this.
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
case 'postgresql':
|
|
757
|
-
query = `
|
|
758
|
-
SELECT table_name
|
|
759
|
-
FROM information_schema.tables
|
|
760
|
-
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
761
|
-
`;
|
|
762
|
-
break;
|
|
763
|
-
case 'mysql':
|
|
764
|
-
query = `
|
|
765
|
-
SELECT table_name
|
|
766
|
-
FROM information_schema.tables
|
|
767
|
-
WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'
|
|
768
|
-
`;
|
|
769
|
-
break;
|
|
770
|
-
case 'sqlite':
|
|
771
|
-
default:
|
|
772
|
-
query = `
|
|
773
|
-
SELECT name FROM sqlite_master
|
|
774
|
-
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
775
|
-
`;
|
|
776
|
-
break;
|
|
777
|
-
}
|
|
438
|
+
const result = await this.db.all(`
|
|
439
|
+
SELECT table_name
|
|
440
|
+
FROM information_schema.tables
|
|
441
|
+
WHERE table_schema = 'public'
|
|
442
|
+
ORDER BY table_name
|
|
443
|
+
`);
|
|
778
444
|
|
|
779
|
-
|
|
780
|
-
return result.map((row: any) => row.table_name || row.name);
|
|
445
|
+
return result.map((row: any) => row.table_name);
|
|
781
446
|
}
|
|
782
447
|
|
|
783
448
|
/**
|
|
784
|
-
* Get
|
|
449
|
+
* Get indexes for a table
|
|
785
450
|
*/
|
|
786
451
|
async getIndexes(tableName: string): Promise<IndexInfo[]> {
|
|
787
|
-
await this.
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
JOIN pg_class c ON c.relname = i.tablename
|
|
802
|
-
JOIN pg_index ix ON ix.indexrelid = (i.schemaname||'.'||i.indexname)::regclass
|
|
803
|
-
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(ix.indkey)
|
|
804
|
-
WHERE i.tablename = $1 AND i.schemaname = 'public'
|
|
805
|
-
GROUP BY i.indexname, i.indexdef
|
|
806
|
-
`;
|
|
807
|
-
break;
|
|
808
|
-
case 'mysql':
|
|
809
|
-
query = `
|
|
810
|
-
SELECT
|
|
811
|
-
INDEX_NAME as name,
|
|
812
|
-
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
|
|
813
|
-
NON_UNIQUE = 0 as unique,
|
|
814
|
-
false as partial
|
|
815
|
-
FROM information_schema.statistics
|
|
816
|
-
WHERE table_schema = DATABASE() AND table_name = ?
|
|
817
|
-
GROUP BY INDEX_NAME, NON_UNIQUE
|
|
818
|
-
`;
|
|
819
|
-
break;
|
|
820
|
-
case 'sqlite':
|
|
821
|
-
default:
|
|
822
|
-
query = `PRAGMA index_list("${tableName}")`;
|
|
823
|
-
params = [];
|
|
824
|
-
break;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
const result = await this.db.all(query, params);
|
|
828
|
-
|
|
829
|
-
const indexes: IndexInfo[] = [];
|
|
452
|
+
const result = await this.db.all(`
|
|
453
|
+
SELECT
|
|
454
|
+
i.indexname as name,
|
|
455
|
+
a.attname as column_name,
|
|
456
|
+
i.indexdef
|
|
457
|
+
FROM pg_indexes i
|
|
458
|
+
JOIN pg_class c ON c.relname = i.indexname
|
|
459
|
+
JOIN pg_index idx ON idx.indexrelid = c.oid
|
|
460
|
+
JOIN pg_attribute a ON a.attrelid = idx.indrelid AND a.attnum = ANY(idx.indkey)
|
|
461
|
+
WHERE i.tablename = ?
|
|
462
|
+
ORDER BY i.indexname, a.attnum
|
|
463
|
+
`, [tableName]);
|
|
464
|
+
|
|
465
|
+
const indexMap = new Map<string, IndexInfo>();
|
|
830
466
|
|
|
831
467
|
for (const row of result) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
// PostgreSQL and MySQL return columns as comma-separated string
|
|
839
|
-
columns = row.columns.split(',').map((col: string) => col.trim());
|
|
468
|
+
if (!indexMap.has(row.name)) {
|
|
469
|
+
indexMap.set(row.name, {
|
|
470
|
+
name: row.name,
|
|
471
|
+
columns: [],
|
|
472
|
+
unique: row.indexdef.includes('UNIQUE')
|
|
473
|
+
});
|
|
840
474
|
}
|
|
841
|
-
|
|
842
|
-
indexes.push({
|
|
843
|
-
name: row.name,
|
|
844
|
-
columns: columns,
|
|
845
|
-
unique: row.unique === 1 || row.unique === true,
|
|
846
|
-
partial: row.partial === 1 || row.partial === true
|
|
847
|
-
});
|
|
475
|
+
indexMap.get(row.name)!.columns.push(row.column_name);
|
|
848
476
|
}
|
|
849
|
-
|
|
850
|
-
return
|
|
477
|
+
|
|
478
|
+
return Array.from(indexMap.values());
|
|
851
479
|
}
|
|
852
480
|
|
|
853
481
|
/**
|
|
854
|
-
* Get
|
|
482
|
+
* Get foreign keys for a table
|
|
855
483
|
*/
|
|
856
484
|
async getForeignKeys(tableName: string): Promise<ForeignKeyInfo[]> {
|
|
857
|
-
await this.
|
|
485
|
+
const result = await this.db.all(`
|
|
486
|
+
SELECT
|
|
487
|
+
tc.constraint_name as name,
|
|
488
|
+
kcu.column_name,
|
|
489
|
+
ccu.table_name AS foreign_table_name,
|
|
490
|
+
ccu.column_name AS foreign_column_name,
|
|
491
|
+
rc.delete_rule,
|
|
492
|
+
rc.update_rule
|
|
493
|
+
FROM information_schema.table_constraints AS tc
|
|
494
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
495
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
496
|
+
AND tc.table_schema = kcu.table_schema
|
|
497
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
498
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
499
|
+
AND ccu.table_schema = tc.table_schema
|
|
500
|
+
JOIN information_schema.referential_constraints AS rc
|
|
501
|
+
ON tc.constraint_name = rc.constraint_name
|
|
502
|
+
AND tc.table_schema = rc.constraint_schema
|
|
503
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
504
|
+
AND tc.table_name = ?
|
|
505
|
+
ORDER BY tc.constraint_name, kcu.ordinal_position
|
|
506
|
+
`, [tableName]);
|
|
507
|
+
|
|
508
|
+
const fkMap = new Map<string, ForeignKeyInfo>();
|
|
858
509
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
FROM information_schema.table_constraints tc
|
|
873
|
-
JOIN information_schema.key_column_usage kcu
|
|
874
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
875
|
-
JOIN information_schema.constraint_column_usage ccu
|
|
876
|
-
ON ccu.constraint_name = tc.constraint_name
|
|
877
|
-
JOIN information_schema.referential_constraints rc
|
|
878
|
-
ON tc.constraint_name = rc.constraint_name
|
|
879
|
-
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
880
|
-
AND tc.table_name = ?
|
|
881
|
-
`;
|
|
882
|
-
break;
|
|
883
|
-
case 'mysql':
|
|
884
|
-
query = `
|
|
885
|
-
SELECT
|
|
886
|
-
CONSTRAINT_NAME as name,
|
|
887
|
-
COLUMN_NAME as from_column,
|
|
888
|
-
REFERENCED_TABLE_NAME as referenced_table,
|
|
889
|
-
REFERENCED_COLUMN_NAME as referenced_column,
|
|
890
|
-
DELETE_RULE as on_delete,
|
|
891
|
-
UPDATE_RULE as on_update
|
|
892
|
-
FROM information_schema.key_column_usage
|
|
893
|
-
WHERE table_schema = DATABASE()
|
|
894
|
-
AND table_name = ?
|
|
895
|
-
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
896
|
-
`;
|
|
897
|
-
break;
|
|
898
|
-
case 'sqlite':
|
|
899
|
-
default:
|
|
900
|
-
query = `PRAGMA foreign_key_list("${tableName}")`;
|
|
901
|
-
params = [];
|
|
902
|
-
break;
|
|
510
|
+
for (const row of result) {
|
|
511
|
+
if (!fkMap.has(row.name)) {
|
|
512
|
+
fkMap.set(row.name, {
|
|
513
|
+
name: row.name,
|
|
514
|
+
columns: [],
|
|
515
|
+
referencedTable: row.foreign_table_name,
|
|
516
|
+
referencedColumns: [],
|
|
517
|
+
onDelete: row.delete_rule !== 'NO ACTION' ? row.delete_rule : undefined,
|
|
518
|
+
onUpdate: row.update_rule !== 'NO ACTION' ? row.update_rule : undefined
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
fkMap.get(row.name)!.columns.push(row.column_name);
|
|
522
|
+
fkMap.get(row.name)!.referencedColumns.push(row.foreign_column_name);
|
|
903
523
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
return result.map((row: any) => ({
|
|
908
|
-
name: row.name || row.id?.toString() || '',
|
|
909
|
-
columns: [row.from_column || row.from],
|
|
910
|
-
referencedTable: row.referenced_table || row.table,
|
|
911
|
-
referencedColumns: [row.referenced_column || row.to],
|
|
912
|
-
onDelete: row.on_delete || row.on_delete,
|
|
913
|
-
onUpdate: row.on_update || row.on_update
|
|
914
|
-
}));
|
|
524
|
+
|
|
525
|
+
return Array.from(fkMap.values());
|
|
915
526
|
}
|
|
916
527
|
|
|
917
528
|
/**
|
|
918
529
|
* Create an index
|
|
919
530
|
*/
|
|
920
|
-
async createIndex(tableName: string,
|
|
921
|
-
const unique =
|
|
922
|
-
const indexName =
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
const sql = `CREATE ${unique}INDEX "${indexName}" ON "${tableName}" (${columns})`;
|
|
531
|
+
async createIndex(tableName: string, columns: string[], options: { unique?: boolean; name?: string } = {}): Promise<void> {
|
|
532
|
+
const unique = options.unique ? 'UNIQUE ' : '';
|
|
533
|
+
const indexName = options.name || `idx_${tableName}_${columns.join('_')}`;
|
|
534
|
+
const columnsStr = columns.map(col => `"${col}"`).join(', ');
|
|
535
|
+
const sql = `CREATE ${unique}INDEX "${indexName}" ON "${tableName}" (${columnsStr})`;
|
|
926
536
|
await this.db.run(sql);
|
|
927
537
|
}
|
|
928
538
|
|
|
929
539
|
/**
|
|
930
540
|
* Drop an index
|
|
931
541
|
*/
|
|
932
|
-
async dropIndex(indexName: string): Promise<void> {
|
|
933
|
-
const
|
|
934
|
-
await this.db.run(
|
|
542
|
+
async dropIndex(indexName: string, ifExists: boolean = false): Promise<void> {
|
|
543
|
+
const ifExistsClause = ifExists ? 'IF EXISTS ' : '';
|
|
544
|
+
await this.db.run(`DROP INDEX ${ifExistsClause}"${indexName}"`);
|
|
935
545
|
}
|
|
936
546
|
|
|
937
547
|
/**
|
|
938
548
|
* Make columns unique
|
|
939
549
|
*/
|
|
940
|
-
async makeColumnsUnique(tableName: string, columns: string[]): Promise<void> {
|
|
941
|
-
const
|
|
550
|
+
async makeColumnsUnique(tableName: string, columns: string[], constraintName?: string): Promise<void> {
|
|
551
|
+
const name = constraintName || `uk_${tableName}_${columns.join('_')}`;
|
|
942
552
|
const columnsStr = columns.map(col => `"${col}"`).join(', ');
|
|
943
|
-
|
|
944
|
-
const sql = `CREATE UNIQUE INDEX "${indexName}" ON "${tableName}" (${columnsStr})`;
|
|
945
|
-
await this.db.run(sql);
|
|
553
|
+
await this.db.run(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${name}" UNIQUE (${columnsStr})`);
|
|
946
554
|
}
|
|
947
555
|
|
|
948
556
|
/**
|
|
949
557
|
* Get complete table information including indexes and foreign keys
|
|
950
558
|
*/
|
|
951
559
|
async getCompleteTableInfo(tableName: string): Promise<TableInfo> {
|
|
952
|
-
|
|
953
|
-
const indexes = await this.getIndexes(tableName);
|
|
954
|
-
const foreignKeys = await this.getForeignKeys(tableName);
|
|
955
|
-
|
|
956
|
-
return {
|
|
957
|
-
name: tableName,
|
|
958
|
-
columns,
|
|
959
|
-
indexes,
|
|
960
|
-
foreignKeys
|
|
961
|
-
};
|
|
560
|
+
return await this.getTableInfo(tableName);
|
|
962
561
|
}
|
|
963
562
|
}
|
|
964
|
-
|
|
965
|
-
// Schema functionality will be added to BunQL in the main index file
|
|
966
|
-
|