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/src/schema.ts CHANGED
@@ -1,9 +1,4 @@
1
- // Import will be handled by the main index file to avoid circular dependency
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 after table creation
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 = this.quoteColumn(col.name) + ' ' + this.mapColumnType(col.type);
118
+ let def = `"${col.name}" ${col.type}`;
165
119
 
166
120
  if (col.primaryKey) {
167
121
  def += ' PRIMARY KEY';
168
- if (col.autoIncrement && this.databaseType !== 'postgresql') {
169
- def += this.getAutoIncrementKeyword();
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
- def += this.formatDefaultValue(col.defaultValue);
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 => this.quoteColumn(col)).join(', ');
191
- const referencedColumns = fk.referencedColumns.map(col => this.quoteColumn(col)).join(', ');
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 ${this.quoteTable(fk.referencedTable)} (${referencedColumns})`;
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 definition
219
+ * Alter a column
381
220
  */
382
- alterColumn(columnName: string, newDefinition: Partial<ColumnDefinition>): this {
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
- await this.addColumnOperation(operation.data);
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.dropColumnOperation(operation.data);
299
+ await this.db.run(`ALTER TABLE "${this.tableName}" DROP COLUMN ${ifExists}"${operation.data}"`);
458
300
  break;
301
+
459
302
  case 'renameColumn':
460
- await this.renameColumnOperation(operation.data);
461
- break;
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
- await this.addIndexOperation(operation.data);
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.dropIndexOperation(operation.data);
316
+ await this.db.run(`DROP INDEX ${ifExists}"${operation.data}"`);
470
317
  break;
318
+
471
319
  case 'addForeignKey':
472
- await this.addForeignKeyOperation(operation.data);
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.dropForeignKeyOperation(operation.data);
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 = true): Promise<void> {
367
+ async dropTable(tableName: string, ifExists: boolean = false): Promise<void> {
639
368
  const ifExistsClause = ifExists ? 'IF EXISTS ' : '';
640
- const sql = `DROP TABLE ${ifExistsClause}"${tableName}"`;
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.detectDatabaseType();
649
-
650
- let query: string;
651
- let params: any[] = [tableName];
652
-
653
- switch (this.databaseType) {
654
- case 'postgresql':
655
- query = `
656
- SELECT table_name
657
- FROM information_schema.tables
658
- WHERE table_schema = 'public' AND table_name = ?
659
- `;
660
- break;
661
- case 'mysql':
662
- query = `
663
- SELECT table_name
664
- FROM information_schema.tables
665
- WHERE table_schema = DATABASE() AND table_name = ?
666
- `;
667
- break;
668
- case 'sqlite':
669
- default:
670
- query = `
671
- SELECT name FROM sqlite_master
672
- WHERE type='table' AND name = ?
673
- `;
674
- break;
675
- }
676
-
677
- const result = await this.db.get(query, params);
678
- return result !== null;
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
- * Get information about a table
683
- */
684
- async getTableInfo(tableName: string): Promise<ColumnDefinition[]> {
685
- await this.detectDatabaseType();
686
-
687
- let query: string;
688
- let params: any[] = [tableName];
689
-
690
- switch (this.databaseType) {
691
- case 'postgresql':
692
- query = `
693
- SELECT
694
- column_name as name,
695
- data_type as type,
696
- is_nullable = 'NO' as notnull,
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 in the database
435
+ * Get all tables
748
436
  */
749
437
  async getTables(): Promise<string[]> {
750
- await this.detectDatabaseType();
751
-
752
- let query: string;
753
- let params: any[] = [];
754
-
755
- switch (this.databaseType) {
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
- const result = await this.db.all(query, params);
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 all indexes for a table
449
+ * Get indexes for a table
785
450
  */
786
451
  async getIndexes(tableName: string): Promise<IndexInfo[]> {
787
- await this.detectDatabaseType();
788
-
789
- let query: string;
790
- let params: any[] = [tableName];
791
-
792
- switch (this.databaseType) {
793
- case 'postgresql':
794
- query = `
795
- SELECT
796
- i.indexname as name,
797
- array_agg(a.attname ORDER BY a.attnum) as columns,
798
- i.indexdef LIKE '%UNIQUE%' as unique,
799
- false as partial
800
- FROM pg_indexes i
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
- let columns: string[];
833
-
834
- if (this.databaseType === 'sqlite') {
835
- const indexInfo = await this.db.all(`PRAGMA index_info("${row.name}")`);
836
- columns = indexInfo.map((col: any) => col.name);
837
- } else {
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 indexes;
477
+
478
+ return Array.from(indexMap.values());
851
479
  }
852
480
 
853
481
  /**
854
- * Get all foreign keys for a table
482
+ * Get foreign keys for a table
855
483
  */
856
484
  async getForeignKeys(tableName: string): Promise<ForeignKeyInfo[]> {
857
- await this.detectDatabaseType();
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
- let query: string;
860
- let params: any[] = [tableName];
861
-
862
- switch (this.databaseType) {
863
- case 'postgresql':
864
- query = `
865
- SELECT
866
- tc.constraint_name as name,
867
- kcu.column_name as from_column,
868
- ccu.table_name as referenced_table,
869
- ccu.column_name as referenced_column,
870
- rc.delete_rule as on_delete,
871
- rc.update_rule as on_update
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
- const result = await this.db.all(query, params);
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, index: IndexInfo): Promise<void> {
921
- const unique = index.unique ? 'UNIQUE ' : '';
922
- const indexName = index.name || `idx_${tableName}_${index.columns.join('_')}`;
923
- const columns = index.columns.map(col => `"${col}"`).join(', ');
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 sql = `DROP INDEX "${indexName}"`;
934
- await this.db.run(sql);
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 indexName = `idx_${tableName}_${columns.join('_')}_unique`;
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
- const columns = await this.getTableInfo(tableName);
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
-