@objectstack/driver-sql 3.2.9 → 3.3.1
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +19 -0
- package/dist/index.d.mts +97 -42
- package/dist/index.d.ts +97 -42
- package/dist/index.js +148 -50
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +149 -51
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/sql-driver-advanced.test.ts +9 -9
- package/src/sql-driver-queryast.test.ts +84 -21
- package/src/sql-driver-schema.test.ts +67 -0
- package/src/sql-driver.test.ts +1 -1
- package/src/sql-driver.ts +225 -92
- package/tsconfig.json +4 -1
package/src/sql-driver.test.ts
CHANGED
|
@@ -93,7 +93,7 @@ describe('SqlDriver (SQLite Integration)', () => {
|
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
it('should count objects', async () => {
|
|
96
|
-
const count = await driver.count('users', { age: 17 } as any);
|
|
96
|
+
const count = await driver.count('users', { where: { age: 17 } } as any);
|
|
97
97
|
expect(count).toBe(2);
|
|
98
98
|
});
|
|
99
99
|
|
package/src/sql-driver.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* SQL Driver for ObjectStack
|
|
5
5
|
*
|
|
6
|
-
* Implements the standard
|
|
6
|
+
* Implements the standard IDataDriver from @objectstack/spec via Knex.js.
|
|
7
7
|
* Supports PostgreSQL, MySQL, SQLite, and other SQL databases.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
11
|
-
import type {
|
|
10
|
+
import type { QueryAST, DriverOptions } from '@objectstack/spec/data';
|
|
11
|
+
import type { IDataDriver } from '@objectstack/spec/contracts';
|
|
12
12
|
import knex, { Knex } from 'knex';
|
|
13
13
|
import { nanoid } from 'nanoid';
|
|
14
14
|
|
|
@@ -60,47 +60,80 @@ export type SqlDriverConfig = Knex.Config;
|
|
|
60
60
|
/**
|
|
61
61
|
* SQL Driver for ObjectStack.
|
|
62
62
|
*
|
|
63
|
-
* Implements the
|
|
63
|
+
* Implements the IDataDriver contract via Knex.js for optimal SQL
|
|
64
64
|
* generation against PostgreSQL, MySQL, SQLite and other SQL databases.
|
|
65
65
|
*/
|
|
66
|
-
export class SqlDriver implements
|
|
67
|
-
//
|
|
68
|
-
public readonly name = 'com.objectstack.driver.sql';
|
|
69
|
-
public readonly version = '1.0.0';
|
|
66
|
+
export class SqlDriver implements IDataDriver {
|
|
67
|
+
// IDataDriver metadata
|
|
68
|
+
public readonly name: string = 'com.objectstack.driver.sql';
|
|
69
|
+
public readonly version: string = '1.0.0';
|
|
70
70
|
public readonly supports = {
|
|
71
|
+
// Basic CRUD Operations
|
|
72
|
+
create: true,
|
|
73
|
+
read: true,
|
|
74
|
+
update: true,
|
|
75
|
+
delete: true,
|
|
76
|
+
|
|
77
|
+
// Bulk Operations
|
|
78
|
+
bulkCreate: true,
|
|
79
|
+
bulkUpdate: true,
|
|
80
|
+
bulkDelete: true,
|
|
81
|
+
|
|
82
|
+
// Transaction & Connection Management
|
|
71
83
|
transactions: true,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
arrayFields: true,
|
|
84
|
+
savepoints: false,
|
|
85
|
+
|
|
86
|
+
// Query Operations
|
|
76
87
|
queryFilters: true,
|
|
77
88
|
queryAggregations: true,
|
|
78
89
|
querySorting: true,
|
|
79
90
|
queryPagination: true,
|
|
80
91
|
queryWindowFunctions: true,
|
|
81
92
|
querySubqueries: true,
|
|
93
|
+
queryCTE: false,
|
|
94
|
+
joins: true,
|
|
95
|
+
|
|
96
|
+
// Advanced Features
|
|
97
|
+
fullTextSearch: false,
|
|
98
|
+
jsonQuery: false,
|
|
99
|
+
geospatialQuery: false,
|
|
100
|
+
streaming: false,
|
|
101
|
+
jsonFields: true,
|
|
102
|
+
arrayFields: true,
|
|
103
|
+
vectorSearch: false,
|
|
104
|
+
|
|
105
|
+
// Schema Management
|
|
106
|
+
schemaSync: true,
|
|
107
|
+
batchSchemaSync: false,
|
|
108
|
+
migrations: false,
|
|
109
|
+
indexes: false,
|
|
110
|
+
|
|
111
|
+
// Performance & Optimization
|
|
112
|
+
connectionPooling: true,
|
|
113
|
+
preparedStatements: true,
|
|
114
|
+
queryCache: false,
|
|
82
115
|
};
|
|
83
116
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
117
|
+
protected knex: Knex;
|
|
118
|
+
protected config: Knex.Config;
|
|
119
|
+
protected jsonFields: Record<string, string[]> = {};
|
|
120
|
+
protected booleanFields: Record<string, string[]> = {};
|
|
121
|
+
protected tablesWithTimestamps: Set<string> = new Set();
|
|
89
122
|
|
|
90
123
|
/** Whether the underlying database is a SQLite variant (sqlite3 or better-sqlite3). */
|
|
91
|
-
|
|
124
|
+
protected get isSqlite(): boolean {
|
|
92
125
|
const c = (this.config as any).client;
|
|
93
126
|
return c === 'sqlite3' || c === 'better-sqlite3';
|
|
94
127
|
}
|
|
95
128
|
|
|
96
129
|
/** Whether the underlying database is PostgreSQL. */
|
|
97
|
-
|
|
130
|
+
protected get isPostgres(): boolean {
|
|
98
131
|
const c = (this.config as any).client;
|
|
99
132
|
return c === 'pg' || c === 'postgresql';
|
|
100
133
|
}
|
|
101
134
|
|
|
102
135
|
/** Whether the underlying database is MySQL. */
|
|
103
|
-
|
|
136
|
+
protected get isMysql(): boolean {
|
|
104
137
|
const c = (this.config as any).client;
|
|
105
138
|
return c === 'mysql' || c === 'mysql2';
|
|
106
139
|
}
|
|
@@ -135,7 +168,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
135
168
|
// CRUD — DriverInterface core
|
|
136
169
|
// ===================================
|
|
137
170
|
|
|
138
|
-
async find(object: string, query:
|
|
171
|
+
async find(object: string, query: QueryAST, options?: DriverOptions): Promise<any[]> {
|
|
139
172
|
const builder = this.getBuilder(object, options);
|
|
140
173
|
|
|
141
174
|
// SELECT
|
|
@@ -145,30 +178,23 @@ export class SqlDriver implements DriverInterface {
|
|
|
145
178
|
builder.select('*');
|
|
146
179
|
}
|
|
147
180
|
|
|
148
|
-
// WHERE
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.applyFilters(builder, filterCondition);
|
|
181
|
+
// WHERE
|
|
182
|
+
if (query.where) {
|
|
183
|
+
this.applyFilters(builder, query.where);
|
|
152
184
|
}
|
|
153
185
|
|
|
154
|
-
// ORDER BY
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const dir = item.order || item[1] || 'asc';
|
|
160
|
-
if (field) {
|
|
161
|
-
builder.orderBy(this.mapSortField(field), dir);
|
|
186
|
+
// ORDER BY
|
|
187
|
+
if (query.orderBy && Array.isArray(query.orderBy)) {
|
|
188
|
+
for (const item of query.orderBy) {
|
|
189
|
+
if (item.field) {
|
|
190
|
+
builder.orderBy(this.mapSortField(item.field), item.order || 'asc');
|
|
162
191
|
}
|
|
163
192
|
}
|
|
164
193
|
}
|
|
165
194
|
|
|
166
|
-
// PAGINATION
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (offsetValue !== undefined) builder.offset(offsetValue);
|
|
171
|
-
if (limitValue !== undefined) builder.limit(limitValue);
|
|
195
|
+
// PAGINATION
|
|
196
|
+
if (query.offset !== undefined) builder.offset(query.offset);
|
|
197
|
+
if (query.limit !== undefined) builder.limit(query.limit);
|
|
172
198
|
|
|
173
199
|
let results: any[];
|
|
174
200
|
try {
|
|
@@ -196,7 +222,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
196
222
|
return results;
|
|
197
223
|
}
|
|
198
224
|
|
|
199
|
-
async findOne(object: string, query:
|
|
225
|
+
async findOne(object: string, query: QueryAST, options?: DriverOptions): Promise<any> {
|
|
200
226
|
// When called with a string/number id fall back gracefully
|
|
201
227
|
if (typeof query === 'string' || typeof query === 'number') {
|
|
202
228
|
const res = await this.getBuilder(object, options).where('id', query).first();
|
|
@@ -211,6 +237,18 @@ export class SqlDriver implements DriverInterface {
|
|
|
211
237
|
return null;
|
|
212
238
|
}
|
|
213
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Stream records matching a structured query.
|
|
242
|
+
* NOTE: Current implementation fetches all results then yields them.
|
|
243
|
+
* TODO: Use Knex .stream() for true cursor-based streaming on large datasets.
|
|
244
|
+
*/
|
|
245
|
+
async *findStream(object: string, query: QueryAST, options?: DriverOptions): AsyncGenerator<Record<string, any>> {
|
|
246
|
+
const results = await this.find(object, query, options);
|
|
247
|
+
for (const row of results) {
|
|
248
|
+
yield row;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
214
252
|
async create(object: string, data: Record<string, any>, options?: DriverOptions): Promise<any> {
|
|
215
253
|
const { _id, ...rest } = data;
|
|
216
254
|
const toInsert = { ...rest };
|
|
@@ -247,13 +285,34 @@ export class SqlDriver implements DriverInterface {
|
|
|
247
285
|
return this.formatOutput(object, updated) || null;
|
|
248
286
|
}
|
|
249
287
|
|
|
250
|
-
async
|
|
288
|
+
async upsert(object: string, data: Record<string, any>, conflictKeys?: string[], options?: DriverOptions): Promise<Record<string, any>> {
|
|
289
|
+
const { _id, ...rest } = data;
|
|
290
|
+
const toUpsert = { ...rest };
|
|
291
|
+
|
|
292
|
+
if (_id !== undefined && toUpsert.id === undefined) {
|
|
293
|
+
toUpsert.id = _id;
|
|
294
|
+
} else if (toUpsert.id === undefined) {
|
|
295
|
+
toUpsert.id = nanoid(DEFAULT_ID_LENGTH);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const formatted = this.formatInput(object, toUpsert);
|
|
299
|
+
const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ['id'];
|
|
300
|
+
|
|
251
301
|
const builder = this.getBuilder(object, options);
|
|
252
|
-
|
|
302
|
+
await builder.insert(formatted).onConflict(mergeKeys).merge();
|
|
303
|
+
|
|
304
|
+
const result = await this.getBuilder(object, options).where('id', toUpsert.id).first();
|
|
305
|
+
return this.formatOutput(object, result) || toUpsert;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async delete(object: string, id: string | number, options?: DriverOptions): Promise<boolean> {
|
|
309
|
+
const builder = this.getBuilder(object, options);
|
|
310
|
+
const count = await builder.where('id', id).delete();
|
|
311
|
+
return count > 0;
|
|
253
312
|
}
|
|
254
313
|
|
|
255
314
|
// ===================================
|
|
256
|
-
//
|
|
315
|
+
// Bulk & Batch Operations
|
|
257
316
|
// ===================================
|
|
258
317
|
|
|
259
318
|
async bulkCreate(object: string, data: any[], options?: DriverOptions): Promise<any> {
|
|
@@ -261,38 +320,50 @@ export class SqlDriver implements DriverInterface {
|
|
|
261
320
|
return await builder.insert(data).returning('*');
|
|
262
321
|
}
|
|
263
322
|
|
|
264
|
-
|
|
323
|
+
/**
|
|
324
|
+
* Batch-update multiple records by ID.
|
|
325
|
+
* NOTE: Current implementation performs sequential updates for correctness.
|
|
326
|
+
* TODO: Optimize with SQL CASE statements or batched transactions for performance.
|
|
327
|
+
*/
|
|
328
|
+
async bulkUpdate(object: string, updates: Array<{ id: string | number; data: Record<string, any> }>, options?: DriverOptions): Promise<Record<string, any>[]> {
|
|
329
|
+
const results: Record<string, any>[] = [];
|
|
330
|
+
for (const { id, data } of updates) {
|
|
331
|
+
const updated = await this.update(object, id, data, options);
|
|
332
|
+
if (updated) results.push(updated);
|
|
333
|
+
}
|
|
334
|
+
return results;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async bulkDelete(object: string, ids: Array<string | number>, options?: DriverOptions): Promise<void> {
|
|
338
|
+
const builder = this.getBuilder(object, options);
|
|
339
|
+
await builder.whereIn('id', ids).delete();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async updateMany(object: string, query: QueryAST, data: any, options?: DriverOptions): Promise<number> {
|
|
265
343
|
const builder = this.getBuilder(object, options);
|
|
266
|
-
|
|
267
|
-
if (filters) this.applyFilters(builder, filters);
|
|
344
|
+
if (query.where) this.applyFilters(builder, query.where);
|
|
268
345
|
const count = await builder.update(data);
|
|
269
|
-
return
|
|
346
|
+
return count || 0;
|
|
270
347
|
}
|
|
271
348
|
|
|
272
|
-
async deleteMany(object: string, query:
|
|
349
|
+
async deleteMany(object: string, query: QueryAST, options?: DriverOptions): Promise<number> {
|
|
273
350
|
const builder = this.getBuilder(object, options);
|
|
274
|
-
|
|
275
|
-
if (filters) this.applyFilters(builder, filters);
|
|
351
|
+
if (query.where) this.applyFilters(builder, query.where);
|
|
276
352
|
const count = await builder.delete();
|
|
277
|
-
return
|
|
353
|
+
return count || 0;
|
|
278
354
|
}
|
|
279
355
|
|
|
280
|
-
async count(object: string, query
|
|
356
|
+
async count(object: string, query?: QueryAST, options?: DriverOptions): Promise<number> {
|
|
281
357
|
const builder = this.getBuilder(object, options);
|
|
282
358
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
actualFilters = query.where || (query as any).filters;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (actualFilters) {
|
|
289
|
-
this.applyFilters(builder, actualFilters);
|
|
359
|
+
if (query?.where) {
|
|
360
|
+
this.applyFilters(builder, query.where);
|
|
290
361
|
}
|
|
291
362
|
|
|
292
363
|
const result = await builder.count<{ count: number }[]>('* as count');
|
|
293
364
|
if (result && result.length > 0) {
|
|
294
365
|
const row: any = result[0];
|
|
295
|
-
return Number(row.count
|
|
366
|
+
return Number(row.count ?? row['count(*)'] ?? 0);
|
|
296
367
|
}
|
|
297
368
|
return 0;
|
|
298
369
|
}
|
|
@@ -322,12 +393,24 @@ export class SqlDriver implements DriverInterface {
|
|
|
322
393
|
return await this.knex.transaction();
|
|
323
394
|
}
|
|
324
395
|
|
|
396
|
+
/** IDataDriver standard */
|
|
397
|
+
async commit(transaction: unknown): Promise<void> {
|
|
398
|
+
await (transaction as Knex.Transaction).commit();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** IDataDriver standard */
|
|
402
|
+
async rollback(transaction: unknown): Promise<void> {
|
|
403
|
+
await (transaction as Knex.Transaction).rollback();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** @deprecated Use commit() instead */
|
|
325
407
|
async commitTransaction(trx: Knex.Transaction): Promise<void> {
|
|
326
|
-
await
|
|
408
|
+
await this.commit(trx);
|
|
327
409
|
}
|
|
328
410
|
|
|
411
|
+
/** @deprecated Use rollback() instead */
|
|
329
412
|
async rollbackTransaction(trx: Knex.Transaction): Promise<void> {
|
|
330
|
-
await
|
|
413
|
+
await this.rollback(trx);
|
|
331
414
|
}
|
|
332
415
|
|
|
333
416
|
// ===================================
|
|
@@ -416,6 +499,11 @@ export class SqlDriver implements DriverInterface {
|
|
|
416
499
|
// Query Plan Analysis
|
|
417
500
|
// ===================================
|
|
418
501
|
|
|
502
|
+
/** IDataDriver standard: analyze query performance */
|
|
503
|
+
async explain(object: string, query: any, options?: DriverOptions): Promise<any> {
|
|
504
|
+
return this.analyzeQuery(object, query, options);
|
|
505
|
+
}
|
|
506
|
+
|
|
419
507
|
async analyzeQuery(object: string, query: any, options?: DriverOptions): Promise<any> {
|
|
420
508
|
const builder = this.getBuilder(object, options);
|
|
421
509
|
|
|
@@ -476,17 +564,25 @@ export class SqlDriver implements DriverInterface {
|
|
|
476
564
|
|
|
477
565
|
async syncSchema(object: string, schema: unknown, _options?: DriverOptions): Promise<void> {
|
|
478
566
|
const objectDef = schema as { name: string; fields?: Record<string, any> };
|
|
479
|
-
|
|
567
|
+
// Use the physical table name (`object`) for DDL operations. The caller
|
|
568
|
+
// (e.g. syncRegisteredSchemas) passes the resolved tableName (e.g. 'sys_user')
|
|
569
|
+
// while objectDef.name may contain the FQN (e.g. 'sys__user').
|
|
570
|
+
await this.initObjects([{ ...objectDef, name: object }]);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async dropTable(object: string, _options?: DriverOptions): Promise<void> {
|
|
574
|
+
await this.knex.schema.dropTableIfExists(object);
|
|
480
575
|
}
|
|
481
576
|
|
|
482
577
|
/**
|
|
483
578
|
* Batch-initialise tables from an array of object definitions.
|
|
484
579
|
*/
|
|
485
|
-
async initObjects(objects: Array<{ name: string; fields?: Record<string, any> }>): Promise<void> {
|
|
580
|
+
async initObjects(objects: Array<{ name: string; tableName?: string; fields?: Record<string, any> }>): Promise<void> {
|
|
486
581
|
await this.ensureDatabaseExists();
|
|
487
582
|
|
|
488
583
|
for (const obj of objects) {
|
|
489
|
-
|
|
584
|
+
// Prefer explicit tableName (physical name) over obj.name (which may be a FQN).
|
|
585
|
+
const tableName = obj.tableName || obj.name;
|
|
490
586
|
|
|
491
587
|
const jsonCols: string[] = [];
|
|
492
588
|
const booleanCols: string[] = [];
|
|
@@ -516,6 +612,11 @@ export class SqlDriver implements DriverInterface {
|
|
|
516
612
|
}
|
|
517
613
|
}
|
|
518
614
|
|
|
615
|
+
// Columns created unconditionally by initObjects — skip them when
|
|
616
|
+
// iterating obj.fields to avoid duplicate-column errors (e.g. SQLite
|
|
617
|
+
// rejects CREATE TABLE with two columns of the same name).
|
|
618
|
+
const builtinColumns = new Set(['id', 'created_at', 'updated_at']);
|
|
619
|
+
|
|
519
620
|
if (!exists) {
|
|
520
621
|
await this.knex.schema.createTable(tableName, (table) => {
|
|
521
622
|
table.string('id').primary();
|
|
@@ -523,6 +624,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
523
624
|
table.timestamp('updated_at').defaultTo(this.knex.fn.now());
|
|
524
625
|
if (obj.fields) {
|
|
525
626
|
for (const [name, field] of Object.entries(obj.fields)) {
|
|
627
|
+
if (builtinColumns.has(name)) continue;
|
|
526
628
|
this.createColumn(table, name, field);
|
|
527
629
|
}
|
|
528
630
|
}
|
|
@@ -609,7 +711,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
609
711
|
return this.knex;
|
|
610
712
|
}
|
|
611
713
|
|
|
612
|
-
|
|
714
|
+
protected getBuilder(object: string, options?: DriverOptions) {
|
|
613
715
|
let builder = this.knex(object);
|
|
614
716
|
if (options?.transaction) {
|
|
615
717
|
builder = builder.transacting(options.transaction as Knex.Transaction);
|
|
@@ -619,7 +721,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
619
721
|
|
|
620
722
|
// ── Filter helpers ──────────────────────────────────────────────────────────
|
|
621
723
|
|
|
622
|
-
|
|
724
|
+
protected applyFilters(builder: Knex.QueryBuilder, filters: any) {
|
|
623
725
|
if (!filters) return;
|
|
624
726
|
|
|
625
727
|
if (!Array.isArray(filters) && typeof filters === 'object') {
|
|
@@ -637,7 +739,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
637
739
|
}
|
|
638
740
|
|
|
639
741
|
for (const [key, value] of Object.entries(filters)) {
|
|
640
|
-
if (['
|
|
742
|
+
if (['limit', 'offset', 'fields', 'orderBy'].includes(key)) continue;
|
|
641
743
|
builder.where(key, value as any);
|
|
642
744
|
}
|
|
643
745
|
return;
|
|
@@ -700,7 +802,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
700
802
|
}
|
|
701
803
|
}
|
|
702
804
|
|
|
703
|
-
|
|
805
|
+
protected applyFilterCondition(builder: Knex.QueryBuilder, condition: any, logicalOp: 'and' | 'or' = 'and') {
|
|
704
806
|
if (!condition || typeof condition !== 'object') return;
|
|
705
807
|
|
|
706
808
|
for (const [key, value] of Object.entries(condition)) {
|
|
@@ -771,13 +873,13 @@ export class SqlDriver implements DriverInterface {
|
|
|
771
873
|
|
|
772
874
|
// ── Field mapping ───────────────────────────────────────────────────────────
|
|
773
875
|
|
|
774
|
-
|
|
876
|
+
protected mapSortField(field: string): string {
|
|
775
877
|
if (field === 'createdAt') return 'created_at';
|
|
776
878
|
if (field === 'updatedAt') return 'updated_at';
|
|
777
879
|
return field;
|
|
778
880
|
}
|
|
779
881
|
|
|
780
|
-
|
|
882
|
+
protected mapAggregateFunc(func: string): string {
|
|
781
883
|
switch (func) {
|
|
782
884
|
case 'count':
|
|
783
885
|
return 'count';
|
|
@@ -796,7 +898,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
796
898
|
|
|
797
899
|
// ── Window function builder ─────────────────────────────────────────────────
|
|
798
900
|
|
|
799
|
-
|
|
901
|
+
protected buildWindowFunction(spec: any): string {
|
|
800
902
|
const func = spec.function.toUpperCase();
|
|
801
903
|
let sql = `${func}()`;
|
|
802
904
|
|
|
@@ -824,7 +926,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
824
926
|
|
|
825
927
|
// ── Column creation helper ──────────────────────────────────────────────────
|
|
826
928
|
|
|
827
|
-
|
|
929
|
+
protected createColumn(table: Knex.CreateTableBuilder, name: string, field: any) {
|
|
828
930
|
if (field.multiple) {
|
|
829
931
|
table.json(name);
|
|
830
932
|
return;
|
|
@@ -903,13 +1005,23 @@ export class SqlDriver implements DriverInterface {
|
|
|
903
1005
|
|
|
904
1006
|
// ── Database helpers ────────────────────────────────────────────────────────
|
|
905
1007
|
|
|
906
|
-
|
|
907
|
-
|
|
1008
|
+
protected async ensureDatabaseExists() {
|
|
1009
|
+
// SQLite auto-creates database files — no need to check
|
|
1010
|
+
if (this.isSqlite) return;
|
|
1011
|
+
|
|
1012
|
+
// Only PostgreSQL and MySQL support programmatic database creation
|
|
1013
|
+
if (!this.isPostgres && !this.isMysql) return;
|
|
908
1014
|
|
|
909
1015
|
try {
|
|
910
1016
|
await this.knex.raw('SELECT 1');
|
|
911
1017
|
} catch (e: any) {
|
|
912
|
-
|
|
1018
|
+
// PostgreSQL: '3D000' = database does not exist
|
|
1019
|
+
// MySQL: 'ER_BAD_DB_ERROR' (errno 1049) = unknown database
|
|
1020
|
+
if (
|
|
1021
|
+
e.code === '3D000' ||
|
|
1022
|
+
e.code === 'ER_BAD_DB_ERROR' ||
|
|
1023
|
+
e.errno === 1049
|
|
1024
|
+
) {
|
|
913
1025
|
await this.createDatabase();
|
|
914
1026
|
} else {
|
|
915
1027
|
throw e;
|
|
@@ -917,37 +1029,58 @@ export class SqlDriver implements DriverInterface {
|
|
|
917
1029
|
}
|
|
918
1030
|
}
|
|
919
1031
|
|
|
920
|
-
|
|
1032
|
+
protected async createDatabase() {
|
|
921
1033
|
const config = this.config as any;
|
|
922
1034
|
const connection = config.connection;
|
|
923
1035
|
let dbName = '';
|
|
924
1036
|
const adminConfig = { ...config };
|
|
925
1037
|
|
|
926
|
-
if (
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1038
|
+
if (this.isPostgres) {
|
|
1039
|
+
// PostgreSQL: connect to the 'postgres' maintenance database
|
|
1040
|
+
if (typeof connection === 'string') {
|
|
1041
|
+
const url = new URL(connection);
|
|
1042
|
+
dbName = url.pathname.slice(1);
|
|
1043
|
+
url.pathname = '/postgres';
|
|
1044
|
+
adminConfig.connection = url.toString();
|
|
1045
|
+
} else {
|
|
1046
|
+
dbName = connection.database;
|
|
1047
|
+
adminConfig.connection = { ...connection, database: 'postgres' };
|
|
1048
|
+
}
|
|
1049
|
+
} else if (this.isMysql) {
|
|
1050
|
+
// MySQL: connect without specifying a database
|
|
1051
|
+
if (typeof connection === 'string') {
|
|
1052
|
+
const url = new URL(connection);
|
|
1053
|
+
dbName = url.pathname.slice(1);
|
|
1054
|
+
url.pathname = '/';
|
|
1055
|
+
adminConfig.connection = url.toString();
|
|
1056
|
+
} else {
|
|
1057
|
+
dbName = connection.database;
|
|
1058
|
+
const { database: _db, ...rest } = connection;
|
|
1059
|
+
adminConfig.connection = rest;
|
|
1060
|
+
}
|
|
931
1061
|
} else {
|
|
932
|
-
|
|
933
|
-
adminConfig.connection = { ...connection, database: 'postgres' };
|
|
1062
|
+
return; // Unsupported dialect for auto-creation
|
|
934
1063
|
}
|
|
935
1064
|
|
|
936
1065
|
const adminKnex = knex(adminConfig);
|
|
937
1066
|
try {
|
|
938
|
-
|
|
1067
|
+
if (this.isPostgres) {
|
|
1068
|
+
await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
|
|
1069
|
+
} else if (this.isMysql) {
|
|
1070
|
+
await adminKnex.raw(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
|
|
1071
|
+
}
|
|
939
1072
|
} finally {
|
|
940
1073
|
await adminKnex.destroy();
|
|
941
1074
|
}
|
|
942
1075
|
}
|
|
943
1076
|
|
|
944
|
-
|
|
1077
|
+
protected isJsonField(type: string, field: any): boolean {
|
|
945
1078
|
return ['json', 'object', 'array', 'image', 'file', 'avatar', 'location'].includes(type) || field.multiple;
|
|
946
1079
|
}
|
|
947
1080
|
|
|
948
1081
|
// ── SQLite serialisation ────────────────────────────────────────────────────
|
|
949
1082
|
|
|
950
|
-
|
|
1083
|
+
protected formatInput(object: string, data: any): any {
|
|
951
1084
|
if (!this.isSqlite) return data;
|
|
952
1085
|
|
|
953
1086
|
const fields = this.jsonFields[object];
|
|
@@ -962,7 +1095,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
962
1095
|
return copy;
|
|
963
1096
|
}
|
|
964
1097
|
|
|
965
|
-
|
|
1098
|
+
protected formatOutput(object: string, data: any): any {
|
|
966
1099
|
if (!data) return data;
|
|
967
1100
|
|
|
968
1101
|
if (this.isSqlite) {
|
|
@@ -994,7 +1127,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
994
1127
|
|
|
995
1128
|
// ── Introspection internals ─────────────────────────────────────────────────
|
|
996
1129
|
|
|
997
|
-
|
|
1130
|
+
protected async introspectColumns(tableName: string): Promise<IntrospectedColumn[]> {
|
|
998
1131
|
const columnInfo = await this.knex(tableName).columnInfo();
|
|
999
1132
|
const columns: IntrospectedColumn[] = [];
|
|
1000
1133
|
|
|
@@ -1026,7 +1159,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
1026
1159
|
return columns;
|
|
1027
1160
|
}
|
|
1028
1161
|
|
|
1029
|
-
|
|
1162
|
+
protected async introspectForeignKeys(tableName: string): Promise<IntrospectedForeignKey[]> {
|
|
1030
1163
|
const foreignKeys: IntrospectedForeignKey[] = [];
|
|
1031
1164
|
|
|
1032
1165
|
try {
|
|
@@ -1112,7 +1245,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
1112
1245
|
return foreignKeys;
|
|
1113
1246
|
}
|
|
1114
1247
|
|
|
1115
|
-
|
|
1248
|
+
protected async introspectPrimaryKeys(tableName: string): Promise<string[]> {
|
|
1116
1249
|
const primaryKeys: string[] = [];
|
|
1117
1250
|
|
|
1118
1251
|
try {
|
|
@@ -1172,7 +1305,7 @@ export class SqlDriver implements DriverInterface {
|
|
|
1172
1305
|
return primaryKeys;
|
|
1173
1306
|
}
|
|
1174
1307
|
|
|
1175
|
-
|
|
1308
|
+
protected async introspectUniqueConstraints(tableName: string): Promise<string[]> {
|
|
1176
1309
|
const uniqueColumns: string[] = [];
|
|
1177
1310
|
|
|
1178
1311
|
try {
|
package/tsconfig.json
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"skipLibCheck": true,
|
|
12
12
|
"noUnusedLocals": false,
|
|
13
13
|
"noUnusedParameters": false,
|
|
14
|
-
"forceConsistentCasingInFileNames": true
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"knex": ["./node_modules/knex/types/index.d.ts"]
|
|
17
|
+
}
|
|
15
18
|
},
|
|
16
19
|
"include": ["src/**/*"],
|
|
17
20
|
"exclude": ["node_modules", "dist"]
|