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