bunql 1.0.1-dev.2 → 1.0.1-dev.4
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 +39 -1
- package/package.json +1 -1
- package/src/schema.ts +435 -45
package/README.md
CHANGED
|
@@ -255,7 +255,45 @@ console.log('Transaction completed, user ID:', result);
|
|
|
255
255
|
|
|
256
256
|
## Schema Management
|
|
257
257
|
|
|
258
|
-
BunQL provides a comprehensive schema management API that can replace Bunely for database schema operations.
|
|
258
|
+
BunQL provides a comprehensive schema management API that can replace Bunely for database schema operations. The schema API is **database-agnostic** and supports SQLite, PostgreSQL, and MySQL.
|
|
259
|
+
|
|
260
|
+
### Database Support
|
|
261
|
+
|
|
262
|
+
BunQL automatically detects your database type and adapts the schema operations accordingly:
|
|
263
|
+
|
|
264
|
+
- **SQLite**: Uses `PRAGMA` statements and `sqlite_master` table
|
|
265
|
+
- **PostgreSQL**: Uses `information_schema` views and `pg_*` system tables
|
|
266
|
+
- **MySQL**: Uses `information_schema` views and `SHOW` statements
|
|
267
|
+
|
|
268
|
+
### Database Detection
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// Get database information
|
|
272
|
+
const dbInfo = await db.schema.getDatabaseInfo();
|
|
273
|
+
console.log(`Database: ${dbInfo.type} ${dbInfo.version}`);
|
|
274
|
+
|
|
275
|
+
### Database-Specific Features
|
|
276
|
+
|
|
277
|
+
BunQL handles database differences automatically:
|
|
278
|
+
|
|
279
|
+
**Column Types:**
|
|
280
|
+
- `INTEGER` → `INTEGER` (SQLite), `INTEGER` (PostgreSQL), `INT` (MySQL)
|
|
281
|
+
- `BOOLEAN` → `INTEGER` (SQLite), `BOOLEAN` (PostgreSQL/MySQL)
|
|
282
|
+
- `BLOB` → `BLOB` (SQLite/MySQL), `BYTEA` (PostgreSQL)
|
|
283
|
+
|
|
284
|
+
**Auto-increment:**
|
|
285
|
+
- SQLite: `AUTOINCREMENT`
|
|
286
|
+
- MySQL: `AUTO_INCREMENT`
|
|
287
|
+
- PostgreSQL: Uses `SERIAL` or `IDENTITY` (handled automatically)
|
|
288
|
+
|
|
289
|
+
**Quoting:**
|
|
290
|
+
- SQLite/PostgreSQL: `"table_name"`
|
|
291
|
+
- MySQL: `` `table_name` ``
|
|
292
|
+
|
|
293
|
+
**Schema Introspection:**
|
|
294
|
+
- SQLite: `PRAGMA table_info()`, `sqlite_master`
|
|
295
|
+
- PostgreSQL: `information_schema.columns`, `pg_indexes`
|
|
296
|
+
- MySQL: `information_schema.tables`, `information_schema.statistics`
|
|
259
297
|
|
|
260
298
|
### Creating Tables
|
|
261
299
|
|
package/package.json
CHANGED
package/src/schema.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Import will be handled by the main index file to avoid circular dependency
|
|
2
2
|
|
|
3
|
+
export interface DatabaseInfo {
|
|
4
|
+
type: 'sqlite' | 'postgresql' | 'mysql';
|
|
5
|
+
version?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export interface ColumnDefinition {
|
|
4
9
|
name: string;
|
|
5
10
|
type: string;
|
|
@@ -49,10 +54,31 @@ export class SchemaBuilder {
|
|
|
49
54
|
private indexes: IndexInfo[] = [];
|
|
50
55
|
private foreignKeys: ForeignKeyInfo[] = [];
|
|
51
56
|
private options: CreateTableOptions = {};
|
|
57
|
+
private databaseType: 'sqlite' | 'postgresql' | 'mysql' = 'sqlite';
|
|
52
58
|
|
|
53
59
|
constructor(db: any, tableName: string) {
|
|
54
60
|
this.db = db;
|
|
55
61
|
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
|
+
}
|
|
56
82
|
}
|
|
57
83
|
|
|
58
84
|
/**
|
|
@@ -103,6 +129,7 @@ export class SchemaBuilder {
|
|
|
103
129
|
throw new Error('At least one column must be defined');
|
|
104
130
|
}
|
|
105
131
|
|
|
132
|
+
await this.detectDatabaseType();
|
|
106
133
|
const sql = this.buildCreateTableSQL();
|
|
107
134
|
await this.db.run(sql);
|
|
108
135
|
|
|
@@ -113,18 +140,33 @@ export class SchemaBuilder {
|
|
|
113
140
|
}
|
|
114
141
|
|
|
115
142
|
private buildCreateTableSQL(): string {
|
|
143
|
+
|
|
116
144
|
const ifNotExists = this.options.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
117
145
|
const temporary = this.options.temporary ? 'TEMPORARY ' : '';
|
|
118
146
|
|
|
119
|
-
let sql = `CREATE ${temporary}TABLE ${ifNotExists}
|
|
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
|
+
}
|
|
120
162
|
|
|
121
163
|
const columnDefinitions = this.columns.map(col => {
|
|
122
|
-
let def =
|
|
164
|
+
let def = this.quoteColumn(col.name) + ' ' + this.mapColumnType(col.type);
|
|
123
165
|
|
|
124
166
|
if (col.primaryKey) {
|
|
125
167
|
def += ' PRIMARY KEY';
|
|
126
|
-
if (col.autoIncrement) {
|
|
127
|
-
def +=
|
|
168
|
+
if (col.autoIncrement && this.databaseType !== 'postgresql') {
|
|
169
|
+
def += this.getAutoIncrementKeyword();
|
|
128
170
|
}
|
|
129
171
|
}
|
|
130
172
|
|
|
@@ -137,11 +179,7 @@ export class SchemaBuilder {
|
|
|
137
179
|
}
|
|
138
180
|
|
|
139
181
|
if (col.defaultValue !== undefined) {
|
|
140
|
-
|
|
141
|
-
def += ` DEFAULT '${col.defaultValue}'`;
|
|
142
|
-
} else {
|
|
143
|
-
def += ` DEFAULT ${col.defaultValue}`;
|
|
144
|
-
}
|
|
182
|
+
def += this.formatDefaultValue(col.defaultValue);
|
|
145
183
|
}
|
|
146
184
|
|
|
147
185
|
return def;
|
|
@@ -149,10 +187,10 @@ export class SchemaBuilder {
|
|
|
149
187
|
|
|
150
188
|
// Add foreign key constraints
|
|
151
189
|
const foreignKeyDefinitions = this.foreignKeys.map(fk => {
|
|
152
|
-
const columns = fk.columns.map(col =>
|
|
153
|
-
const referencedColumns = fk.referencedColumns.map(col =>
|
|
190
|
+
const columns = fk.columns.map(col => this.quoteColumn(col)).join(', ');
|
|
191
|
+
const referencedColumns = fk.referencedColumns.map(col => this.quoteColumn(col)).join(', ');
|
|
154
192
|
|
|
155
|
-
let fkDef = `FOREIGN KEY (${columns}) REFERENCES
|
|
193
|
+
let fkDef = `FOREIGN KEY (${columns}) REFERENCES ${this.quoteTable(fk.referencedTable)} (${referencedColumns})`;
|
|
156
194
|
|
|
157
195
|
if (fk.onDelete) {
|
|
158
196
|
fkDef += ` ON DELETE ${fk.onDelete}`;
|
|
@@ -172,6 +210,115 @@ export class SchemaBuilder {
|
|
|
172
210
|
return sql;
|
|
173
211
|
}
|
|
174
212
|
|
|
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
|
+
|
|
175
322
|
private async createIndex(index: IndexInfo): Promise<void> {
|
|
176
323
|
const unique = index.unique ? 'UNIQUE ' : '';
|
|
177
324
|
const indexName = index.name || `idx_${this.tableName}_${index.columns.join('_')}`;
|
|
@@ -410,9 +557,65 @@ export class AlterTableBuilder {
|
|
|
410
557
|
|
|
411
558
|
export class Schema {
|
|
412
559
|
private db: any; // Will be BunQL instance
|
|
560
|
+
private databaseType: 'sqlite' | 'postgresql' | 'mysql' = 'sqlite';
|
|
413
561
|
|
|
414
562
|
constructor(db: any) {
|
|
415
563
|
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
|
+
};
|
|
416
619
|
}
|
|
417
620
|
|
|
418
621
|
/**
|
|
@@ -442,10 +645,36 @@ export class Schema {
|
|
|
442
645
|
* Check if a table exists
|
|
443
646
|
*/
|
|
444
647
|
async hasTable(tableName: string): Promise<boolean> {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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);
|
|
449
678
|
return result !== null;
|
|
450
679
|
}
|
|
451
680
|
|
|
@@ -453,16 +682,62 @@ export class Schema {
|
|
|
453
682
|
* Get information about a table
|
|
454
683
|
*/
|
|
455
684
|
async getTableInfo(tableName: string): Promise<ColumnDefinition[]> {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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);
|
|
459
734
|
|
|
460
735
|
return result.map((row: any) => ({
|
|
461
736
|
name: row.name,
|
|
462
737
|
type: row.type,
|
|
463
|
-
notNull: row.notnull === 1,
|
|
464
|
-
primaryKey: row.pk === 1,
|
|
465
|
-
unique: false, //
|
|
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
|
|
466
741
|
defaultValue: row.dflt_value,
|
|
467
742
|
autoIncrement: false // Would need additional logic to detect this
|
|
468
743
|
}));
|
|
@@ -472,34 +747,103 @@ export class Schema {
|
|
|
472
747
|
* Get all tables in the database
|
|
473
748
|
*/
|
|
474
749
|
async getTables(): Promise<string[]> {
|
|
475
|
-
|
|
476
|
-
SELECT name FROM sqlite_master
|
|
477
|
-
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
478
|
-
`);
|
|
750
|
+
await this.detectDatabaseType();
|
|
479
751
|
|
|
480
|
-
|
|
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
|
+
}
|
|
778
|
+
|
|
779
|
+
const result = await this.db.all(query, params);
|
|
780
|
+
return result.map((row: any) => row.table_name || row.name);
|
|
481
781
|
}
|
|
482
782
|
|
|
483
783
|
/**
|
|
484
784
|
* Get all indexes for a table
|
|
485
785
|
*/
|
|
486
786
|
async getIndexes(tableName: string): Promise<IndexInfo[]> {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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);
|
|
490
828
|
|
|
491
829
|
const indexes: IndexInfo[] = [];
|
|
492
830
|
|
|
493
831
|
for (const row of result) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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());
|
|
840
|
+
}
|
|
497
841
|
|
|
498
842
|
indexes.push({
|
|
499
843
|
name: row.name,
|
|
500
|
-
columns:
|
|
501
|
-
unique: row.unique === 1,
|
|
502
|
-
partial: row.partial === 1
|
|
844
|
+
columns: columns,
|
|
845
|
+
unique: row.unique === 1 || row.unique === true,
|
|
846
|
+
partial: row.partial === 1 || row.partial === true
|
|
503
847
|
});
|
|
504
848
|
}
|
|
505
849
|
|
|
@@ -510,17 +854,63 @@ export class Schema {
|
|
|
510
854
|
* Get all foreign keys for a table
|
|
511
855
|
*/
|
|
512
856
|
async getForeignKeys(tableName: string): Promise<ForeignKeyInfo[]> {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
857
|
+
await this.detectDatabaseType();
|
|
858
|
+
|
|
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;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const result = await this.db.all(query, params);
|
|
516
906
|
|
|
517
907
|
return result.map((row: any) => ({
|
|
518
|
-
name: row.id
|
|
519
|
-
columns: [row.from],
|
|
520
|
-
referencedTable: row.table,
|
|
521
|
-
referencedColumns: [row.to],
|
|
522
|
-
onDelete: row.on_delete,
|
|
523
|
-
onUpdate: row.on_update
|
|
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
|
|
524
914
|
}));
|
|
525
915
|
}
|
|
526
916
|
|