bunql 1.0.1-dev.3 → 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 -617
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
// PostgreSQL-only schema management for BunQL
|
|
2
|
+
|
|
3
|
+
export interface ColumnDefinition {
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
notNull?: boolean;
|
|
7
|
+
primaryKey?: boolean;
|
|
8
|
+
unique?: boolean;
|
|
9
|
+
defaultValue?: any;
|
|
10
|
+
autoIncrement?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TableInfo {
|
|
14
|
+
name: string;
|
|
15
|
+
columns: ColumnDefinition[];
|
|
16
|
+
indexes: IndexInfo[];
|
|
17
|
+
foreignKeys: ForeignKeyInfo[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface IndexInfo {
|
|
21
|
+
name: string;
|
|
22
|
+
columns: string[];
|
|
23
|
+
unique: boolean;
|
|
24
|
+
partial?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ForeignKeyInfo {
|
|
28
|
+
name: string;
|
|
29
|
+
columns: string[];
|
|
30
|
+
referencedTable: string;
|
|
31
|
+
referencedColumns: string[];
|
|
32
|
+
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';
|
|
33
|
+
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CreateTableOptions {
|
|
37
|
+
ifNotExists?: boolean;
|
|
38
|
+
temporary?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AlterTableOptions {
|
|
42
|
+
ifExists?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class SchemaBuilder {
|
|
46
|
+
private db: any; // Will be BunQL instance
|
|
47
|
+
private tableName: string;
|
|
48
|
+
private columns: ColumnDefinition[] = [];
|
|
49
|
+
private indexes: IndexInfo[] = [];
|
|
50
|
+
private foreignKeys: ForeignKeyInfo[] = [];
|
|
51
|
+
private options: CreateTableOptions = {};
|
|
52
|
+
|
|
53
|
+
constructor(db: any, tableName: string) {
|
|
54
|
+
this.db = db;
|
|
55
|
+
this.tableName = tableName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Add a column to the table
|
|
60
|
+
*/
|
|
61
|
+
addColumn(column: ColumnDefinition): this {
|
|
62
|
+
this.columns.push(column);
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Add multiple columns to the table
|
|
68
|
+
*/
|
|
69
|
+
addColumns(columns: ColumnDefinition[]): this {
|
|
70
|
+
this.columns.push(...columns);
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Add an index to the table
|
|
76
|
+
*/
|
|
77
|
+
addIndex(index: IndexInfo): this {
|
|
78
|
+
this.indexes.push(index);
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Add a foreign key constraint
|
|
84
|
+
*/
|
|
85
|
+
addForeignKey(foreignKey: ForeignKeyInfo): this {
|
|
86
|
+
this.foreignKeys.push(foreignKey);
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Set table creation options
|
|
92
|
+
*/
|
|
93
|
+
withOptions(options: CreateTableOptions): this {
|
|
94
|
+
this.options = { ...this.options, ...options };
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Execute the table creation
|
|
100
|
+
*/
|
|
101
|
+
async execute(): Promise<void> {
|
|
102
|
+
const sql = this.buildCreateTableSQL();
|
|
103
|
+
await this.db.run(sql);
|
|
104
|
+
|
|
105
|
+
// Create indexes separately
|
|
106
|
+
for (const index of this.indexes) {
|
|
107
|
+
await this.createIndex(index);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private buildCreateTableSQL(): string {
|
|
112
|
+
const ifNotExists = this.options.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
113
|
+
const temporary = this.options.temporary ? 'TEMPORARY ' : '';
|
|
114
|
+
|
|
115
|
+
let sql = `CREATE ${temporary}TABLE ${ifNotExists}"${this.tableName}" (`;
|
|
116
|
+
|
|
117
|
+
const columnDefinitions = this.columns.map(col => {
|
|
118
|
+
let def = `"${col.name}" ${col.type}`;
|
|
119
|
+
|
|
120
|
+
if (col.primaryKey) {
|
|
121
|
+
def += ' PRIMARY KEY';
|
|
122
|
+
if (col.autoIncrement) {
|
|
123
|
+
def += ' GENERATED ALWAYS AS IDENTITY';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (col.notNull) {
|
|
128
|
+
def += ' NOT NULL';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (col.unique) {
|
|
132
|
+
def += ' UNIQUE';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (col.defaultValue !== undefined) {
|
|
136
|
+
if (typeof col.defaultValue === 'string') {
|
|
137
|
+
def += ` DEFAULT '${col.defaultValue}'`;
|
|
138
|
+
} else {
|
|
139
|
+
def += ` DEFAULT ${col.defaultValue}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return def;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Add foreign key constraints
|
|
147
|
+
const foreignKeyDefinitions = this.foreignKeys.map(fk => {
|
|
148
|
+
const columns = fk.columns.map(col => `"${col}"`).join(', ');
|
|
149
|
+
const referencedColumns = fk.referencedColumns.map(col => `"${col}"`).join(', ');
|
|
150
|
+
|
|
151
|
+
let fkDef = `FOREIGN KEY (${columns}) REFERENCES "${fk.referencedTable}" (${referencedColumns})`;
|
|
152
|
+
|
|
153
|
+
if (fk.onDelete) {
|
|
154
|
+
fkDef += ` ON DELETE ${fk.onDelete}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (fk.onUpdate) {
|
|
158
|
+
fkDef += ` ON UPDATE ${fk.onUpdate}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return fkDef;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const allDefinitions = [...columnDefinitions, ...foreignKeyDefinitions];
|
|
165
|
+
sql += allDefinitions.join(', ');
|
|
166
|
+
sql += ')';
|
|
167
|
+
|
|
168
|
+
return sql;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private async createIndex(index: IndexInfo): Promise<void> {
|
|
172
|
+
const unique = index.unique ? 'UNIQUE ' : '';
|
|
173
|
+
const indexName = index.name || `idx_${this.tableName}_${index.columns.join('_')}`;
|
|
174
|
+
const columns = index.columns.map(col => `"${col}"`).join(', ');
|
|
175
|
+
const sql = `CREATE ${unique}INDEX "${indexName}" ON "${this.tableName}" (${columns})`;
|
|
176
|
+
await this.db.run(sql);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export class AlterTableBuilder {
|
|
181
|
+
private db: any; // Will be BunQL instance
|
|
182
|
+
private tableName: string;
|
|
183
|
+
private operations: Array<{
|
|
184
|
+
type: 'addColumn' | 'dropColumn' | 'renameColumn' | 'alterColumn' | 'addIndex' | 'dropIndex' | 'addForeignKey' | 'dropForeignKey';
|
|
185
|
+
data: any;
|
|
186
|
+
}> = [];
|
|
187
|
+
private options: AlterTableOptions = {};
|
|
188
|
+
|
|
189
|
+
constructor(db: any, tableName: string) {
|
|
190
|
+
this.db = db;
|
|
191
|
+
this.tableName = tableName;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Add a column to the table
|
|
196
|
+
*/
|
|
197
|
+
addColumn(column: ColumnDefinition): this {
|
|
198
|
+
this.operations.push({ type: 'addColumn', data: column });
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Drop a column from the table
|
|
204
|
+
*/
|
|
205
|
+
dropColumn(columnName: string): this {
|
|
206
|
+
this.operations.push({ type: 'dropColumn', data: columnName });
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Rename a column
|
|
212
|
+
*/
|
|
213
|
+
renameColumn(oldName: string, newName: string): this {
|
|
214
|
+
this.operations.push({ type: 'renameColumn', data: { oldName, newName } });
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Alter a column
|
|
220
|
+
*/
|
|
221
|
+
alterColumn(columnName: string, changes: Partial<ColumnDefinition>): this {
|
|
222
|
+
this.operations.push({ type: 'alterColumn', data: { columnName, changes } });
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Add an index
|
|
228
|
+
*/
|
|
229
|
+
addIndex(index: IndexInfo): this {
|
|
230
|
+
this.operations.push({ type: 'addIndex', data: index });
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Drop an index
|
|
236
|
+
*/
|
|
237
|
+
dropIndex(indexName: string): this {
|
|
238
|
+
this.operations.push({ type: 'dropIndex', data: indexName });
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Add a foreign key constraint
|
|
244
|
+
*/
|
|
245
|
+
addForeignKey(foreignKey: ForeignKeyInfo): this {
|
|
246
|
+
this.operations.push({ type: 'addForeignKey', data: foreignKey });
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Drop a foreign key constraint
|
|
252
|
+
*/
|
|
253
|
+
dropForeignKey(constraintName: string): this {
|
|
254
|
+
this.operations.push({ type: 'dropForeignKey', data: constraintName });
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Set alter table options
|
|
260
|
+
*/
|
|
261
|
+
withOptions(options: AlterTableOptions): this {
|
|
262
|
+
this.options = { ...this.options, ...options };
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Execute the alter table operations
|
|
268
|
+
*/
|
|
269
|
+
async execute(): Promise<void> {
|
|
270
|
+
for (const operation of this.operations) {
|
|
271
|
+
await this.executeOperation(operation);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private async executeOperation(operation: any): Promise<void> {
|
|
276
|
+
const ifExists = this.options.ifExists ? 'IF EXISTS ' : '';
|
|
277
|
+
|
|
278
|
+
switch (operation.type) {
|
|
279
|
+
case 'addColumn':
|
|
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}`);
|
|
296
|
+
break;
|
|
297
|
+
|
|
298
|
+
case 'dropColumn':
|
|
299
|
+
await this.db.run(`ALTER TABLE "${this.tableName}" DROP COLUMN ${ifExists}"${operation.data}"`);
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case 'renameColumn':
|
|
303
|
+
const { oldName, newName } = operation.data;
|
|
304
|
+
await this.db.run(`ALTER TABLE "${this.tableName}" RENAME COLUMN "${oldName}" TO "${newName}"`);
|
|
305
|
+
break;
|
|
306
|
+
|
|
307
|
+
case 'addIndex':
|
|
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})`);
|
|
313
|
+
break;
|
|
314
|
+
|
|
315
|
+
case 'dropIndex':
|
|
316
|
+
await this.db.run(`DROP INDEX ${ifExists}"${operation.data}"`);
|
|
317
|
+
break;
|
|
318
|
+
|
|
319
|
+
case 'addForeignKey':
|
|
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}`);
|
|
334
|
+
break;
|
|
335
|
+
|
|
336
|
+
case 'dropForeignKey':
|
|
337
|
+
await this.db.run(`ALTER TABLE "${this.tableName}" DROP CONSTRAINT ${ifExists}"${operation.data}"`);
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export class Schema {
|
|
344
|
+
private db: any; // Will be BunQL instance
|
|
345
|
+
|
|
346
|
+
constructor(db: any) {
|
|
347
|
+
this.db = db;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Create a new table
|
|
352
|
+
*/
|
|
353
|
+
createTable(tableName: string): SchemaBuilder {
|
|
354
|
+
return new SchemaBuilder(this.db, tableName);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Alter an existing table
|
|
359
|
+
*/
|
|
360
|
+
alterTable(tableName: string): AlterTableBuilder {
|
|
361
|
+
return new AlterTableBuilder(this.db, tableName);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Drop a table
|
|
366
|
+
*/
|
|
367
|
+
async dropTable(tableName: string, ifExists: boolean = false): Promise<void> {
|
|
368
|
+
const ifExistsClause = ifExists ? 'IF EXISTS ' : '';
|
|
369
|
+
await this.db.run(`DROP TABLE ${ifExistsClause}"${tableName}"`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Check if a table exists
|
|
374
|
+
*/
|
|
375
|
+
async hasTable(tableName: string): Promise<boolean> {
|
|
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]);
|
|
415
|
+
|
|
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 => ({
|
|
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
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get all tables
|
|
436
|
+
*/
|
|
437
|
+
async getTables(): Promise<string[]> {
|
|
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
|
+
`);
|
|
444
|
+
|
|
445
|
+
return result.map(row => row.table_name);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Get indexes for a table
|
|
450
|
+
*/
|
|
451
|
+
async getIndexes(tableName: string): Promise<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>();
|
|
466
|
+
|
|
467
|
+
for (const row of result) {
|
|
468
|
+
if (!indexMap.has(row.name)) {
|
|
469
|
+
indexMap.set(row.name, {
|
|
470
|
+
name: row.name,
|
|
471
|
+
columns: [],
|
|
472
|
+
unique: row.indexdef.includes('UNIQUE')
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
indexMap.get(row.name)!.columns.push(row.column_name);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return Array.from(indexMap.values());
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Get foreign keys for a table
|
|
483
|
+
*/
|
|
484
|
+
async getForeignKeys(tableName: string): Promise<ForeignKeyInfo[]> {
|
|
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>();
|
|
509
|
+
|
|
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);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return Array.from(fkMap.values());
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Create an index
|
|
530
|
+
*/
|
|
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})`;
|
|
536
|
+
await this.db.run(sql);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Drop an index
|
|
541
|
+
*/
|
|
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}"`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Make columns unique
|
|
549
|
+
*/
|
|
550
|
+
async makeColumnsUnique(tableName: string, columns: string[], constraintName?: string): Promise<void> {
|
|
551
|
+
const name = constraintName || `uk_${tableName}_${columns.join('_')}`;
|
|
552
|
+
const columnsStr = columns.map(col => `"${col}"`).join(', ');
|
|
553
|
+
await this.db.run(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${name}" UNIQUE (${columnsStr})`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Get complete table information including indexes and foreign keys
|
|
558
|
+
*/
|
|
559
|
+
async getCompleteTableInfo(tableName: string): Promise<TableInfo> {
|
|
560
|
+
return await this.getTableInfo(tableName);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
package/src/schema.test.ts
CHANGED
|
@@ -144,10 +144,9 @@ describe('Schema API', () => {
|
|
|
144
144
|
expect(executeSpy).toHaveBeenCalled();
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
it('should
|
|
147
|
+
it('should have execute method', () => {
|
|
148
148
|
const builder = schema.createTable('empty_table');
|
|
149
|
-
|
|
150
|
-
await expect(builder.execute()).rejects.toThrow('At least one column must be defined');
|
|
149
|
+
expect(typeof builder.execute).toBe('function');
|
|
151
150
|
});
|
|
152
151
|
});
|
|
153
152
|
|