@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/dist/index.js
CHANGED
|
@@ -41,21 +41,47 @@ var import_nanoid = require("nanoid");
|
|
|
41
41
|
var DEFAULT_ID_LENGTH = 16;
|
|
42
42
|
var SqlDriver = class {
|
|
43
43
|
constructor(config) {
|
|
44
|
-
//
|
|
44
|
+
// IDataDriver metadata
|
|
45
45
|
this.name = "com.objectstack.driver.sql";
|
|
46
46
|
this.version = "1.0.0";
|
|
47
47
|
this.supports = {
|
|
48
|
+
// Basic CRUD Operations
|
|
49
|
+
create: true,
|
|
50
|
+
read: true,
|
|
51
|
+
update: true,
|
|
52
|
+
delete: true,
|
|
53
|
+
// Bulk Operations
|
|
54
|
+
bulkCreate: true,
|
|
55
|
+
bulkUpdate: true,
|
|
56
|
+
bulkDelete: true,
|
|
57
|
+
// Transaction & Connection Management
|
|
48
58
|
transactions: true,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
jsonFields: true,
|
|
52
|
-
arrayFields: true,
|
|
59
|
+
savepoints: false,
|
|
60
|
+
// Query Operations
|
|
53
61
|
queryFilters: true,
|
|
54
62
|
queryAggregations: true,
|
|
55
63
|
querySorting: true,
|
|
56
64
|
queryPagination: true,
|
|
57
65
|
queryWindowFunctions: true,
|
|
58
|
-
querySubqueries: true
|
|
66
|
+
querySubqueries: true,
|
|
67
|
+
queryCTE: false,
|
|
68
|
+
joins: true,
|
|
69
|
+
// Advanced Features
|
|
70
|
+
fullTextSearch: false,
|
|
71
|
+
jsonQuery: false,
|
|
72
|
+
geospatialQuery: false,
|
|
73
|
+
streaming: false,
|
|
74
|
+
jsonFields: true,
|
|
75
|
+
arrayFields: true,
|
|
76
|
+
vectorSearch: false,
|
|
77
|
+
// Schema Management
|
|
78
|
+
schemaSync: true,
|
|
79
|
+
migrations: false,
|
|
80
|
+
indexes: false,
|
|
81
|
+
// Performance & Optimization
|
|
82
|
+
connectionPooling: true,
|
|
83
|
+
preparedStatements: true,
|
|
84
|
+
queryCache: false
|
|
59
85
|
};
|
|
60
86
|
this.jsonFields = {};
|
|
61
87
|
this.booleanFields = {};
|
|
@@ -105,24 +131,18 @@ var SqlDriver = class {
|
|
|
105
131
|
} else {
|
|
106
132
|
builder.select("*");
|
|
107
133
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
this.applyFilters(builder, filterCondition);
|
|
134
|
+
if (query.where) {
|
|
135
|
+
this.applyFilters(builder, query.where);
|
|
111
136
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const dir = item.order || item[1] || "asc";
|
|
117
|
-
if (field) {
|
|
118
|
-
builder.orderBy(this.mapSortField(field), dir);
|
|
137
|
+
if (query.orderBy && Array.isArray(query.orderBy)) {
|
|
138
|
+
for (const item of query.orderBy) {
|
|
139
|
+
if (item.field) {
|
|
140
|
+
builder.orderBy(this.mapSortField(item.field), item.order || "asc");
|
|
119
141
|
}
|
|
120
142
|
}
|
|
121
143
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (offsetValue !== void 0) builder.offset(offsetValue);
|
|
125
|
-
if (limitValue !== void 0) builder.limit(limitValue);
|
|
144
|
+
if (query.offset !== void 0) builder.offset(query.offset);
|
|
145
|
+
if (query.limit !== void 0) builder.limit(query.limit);
|
|
126
146
|
let results;
|
|
127
147
|
try {
|
|
128
148
|
results = await builder;
|
|
@@ -153,6 +173,17 @@ var SqlDriver = class {
|
|
|
153
173
|
}
|
|
154
174
|
return null;
|
|
155
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Stream records matching a structured query.
|
|
178
|
+
* NOTE: Current implementation fetches all results then yields them.
|
|
179
|
+
* TODO: Use Knex .stream() for true cursor-based streaming on large datasets.
|
|
180
|
+
*/
|
|
181
|
+
async *findStream(object, query, options) {
|
|
182
|
+
const results = await this.find(object, query, options);
|
|
183
|
+
for (const row of results) {
|
|
184
|
+
yield row;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
156
187
|
async create(object, data, options) {
|
|
157
188
|
const { _id, ...rest } = data;
|
|
158
189
|
const toInsert = { ...rest };
|
|
@@ -181,44 +212,71 @@ var SqlDriver = class {
|
|
|
181
212
|
const updated = await this.getBuilder(object, options).where("id", id).first();
|
|
182
213
|
return this.formatOutput(object, updated) || null;
|
|
183
214
|
}
|
|
215
|
+
async upsert(object, data, conflictKeys, options) {
|
|
216
|
+
const { _id, ...rest } = data;
|
|
217
|
+
const toUpsert = { ...rest };
|
|
218
|
+
if (_id !== void 0 && toUpsert.id === void 0) {
|
|
219
|
+
toUpsert.id = _id;
|
|
220
|
+
} else if (toUpsert.id === void 0) {
|
|
221
|
+
toUpsert.id = (0, import_nanoid.nanoid)(DEFAULT_ID_LENGTH);
|
|
222
|
+
}
|
|
223
|
+
const formatted = this.formatInput(object, toUpsert);
|
|
224
|
+
const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ["id"];
|
|
225
|
+
const builder = this.getBuilder(object, options);
|
|
226
|
+
await builder.insert(formatted).onConflict(mergeKeys).merge();
|
|
227
|
+
const result = await this.getBuilder(object, options).where("id", toUpsert.id).first();
|
|
228
|
+
return this.formatOutput(object, result) || toUpsert;
|
|
229
|
+
}
|
|
184
230
|
async delete(object, id, options) {
|
|
185
231
|
const builder = this.getBuilder(object, options);
|
|
186
|
-
|
|
232
|
+
const count = await builder.where("id", id).delete();
|
|
233
|
+
return count > 0;
|
|
187
234
|
}
|
|
188
235
|
// ===================================
|
|
189
|
-
//
|
|
236
|
+
// Bulk & Batch Operations
|
|
190
237
|
// ===================================
|
|
191
238
|
async bulkCreate(object, data, options) {
|
|
192
239
|
const builder = this.getBuilder(object, options);
|
|
193
240
|
return await builder.insert(data).returning("*");
|
|
194
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Batch-update multiple records by ID.
|
|
244
|
+
* NOTE: Current implementation performs sequential updates for correctness.
|
|
245
|
+
* TODO: Optimize with SQL CASE statements or batched transactions for performance.
|
|
246
|
+
*/
|
|
247
|
+
async bulkUpdate(object, updates, options) {
|
|
248
|
+
const results = [];
|
|
249
|
+
for (const { id, data } of updates) {
|
|
250
|
+
const updated = await this.update(object, id, data, options);
|
|
251
|
+
if (updated) results.push(updated);
|
|
252
|
+
}
|
|
253
|
+
return results;
|
|
254
|
+
}
|
|
255
|
+
async bulkDelete(object, ids, options) {
|
|
256
|
+
const builder = this.getBuilder(object, options);
|
|
257
|
+
await builder.whereIn("id", ids).delete();
|
|
258
|
+
}
|
|
195
259
|
async updateMany(object, query, data, options) {
|
|
196
260
|
const builder = this.getBuilder(object, options);
|
|
197
|
-
|
|
198
|
-
if (filters) this.applyFilters(builder, filters);
|
|
261
|
+
if (query.where) this.applyFilters(builder, query.where);
|
|
199
262
|
const count = await builder.update(data);
|
|
200
|
-
return
|
|
263
|
+
return count || 0;
|
|
201
264
|
}
|
|
202
265
|
async deleteMany(object, query, options) {
|
|
203
266
|
const builder = this.getBuilder(object, options);
|
|
204
|
-
|
|
205
|
-
if (filters) this.applyFilters(builder, filters);
|
|
267
|
+
if (query.where) this.applyFilters(builder, query.where);
|
|
206
268
|
const count = await builder.delete();
|
|
207
|
-
return
|
|
269
|
+
return count || 0;
|
|
208
270
|
}
|
|
209
271
|
async count(object, query, options) {
|
|
210
272
|
const builder = this.getBuilder(object, options);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
actualFilters = query.where || query.filters;
|
|
214
|
-
}
|
|
215
|
-
if (actualFilters) {
|
|
216
|
-
this.applyFilters(builder, actualFilters);
|
|
273
|
+
if (query?.where) {
|
|
274
|
+
this.applyFilters(builder, query.where);
|
|
217
275
|
}
|
|
218
276
|
const result = await builder.count("* as count");
|
|
219
277
|
if (result && result.length > 0) {
|
|
220
278
|
const row = result[0];
|
|
221
|
-
return Number(row.count
|
|
279
|
+
return Number(row.count ?? row["count(*)"] ?? 0);
|
|
222
280
|
}
|
|
223
281
|
return 0;
|
|
224
282
|
}
|
|
@@ -238,11 +296,21 @@ var SqlDriver = class {
|
|
|
238
296
|
async beginTransaction() {
|
|
239
297
|
return await this.knex.transaction();
|
|
240
298
|
}
|
|
299
|
+
/** IDataDriver standard */
|
|
300
|
+
async commit(transaction) {
|
|
301
|
+
await transaction.commit();
|
|
302
|
+
}
|
|
303
|
+
/** IDataDriver standard */
|
|
304
|
+
async rollback(transaction) {
|
|
305
|
+
await transaction.rollback();
|
|
306
|
+
}
|
|
307
|
+
/** @deprecated Use commit() instead */
|
|
241
308
|
async commitTransaction(trx) {
|
|
242
|
-
await
|
|
309
|
+
await this.commit(trx);
|
|
243
310
|
}
|
|
311
|
+
/** @deprecated Use rollback() instead */
|
|
244
312
|
async rollbackTransaction(trx) {
|
|
245
|
-
await
|
|
313
|
+
await this.rollback(trx);
|
|
246
314
|
}
|
|
247
315
|
// ===================================
|
|
248
316
|
// Aggregation
|
|
@@ -311,6 +379,10 @@ var SqlDriver = class {
|
|
|
311
379
|
// ===================================
|
|
312
380
|
// Query Plan Analysis
|
|
313
381
|
// ===================================
|
|
382
|
+
/** IDataDriver standard: analyze query performance */
|
|
383
|
+
async explain(object, query, options) {
|
|
384
|
+
return this.analyzeQuery(object, query, options);
|
|
385
|
+
}
|
|
314
386
|
async analyzeQuery(object, query, options) {
|
|
315
387
|
const builder = this.getBuilder(object, options);
|
|
316
388
|
if (query.fields) {
|
|
@@ -362,7 +434,10 @@ var SqlDriver = class {
|
|
|
362
434
|
// ===================================
|
|
363
435
|
async syncSchema(object, schema, _options) {
|
|
364
436
|
const objectDef = schema;
|
|
365
|
-
await this.initObjects([objectDef]);
|
|
437
|
+
await this.initObjects([{ ...objectDef, name: object }]);
|
|
438
|
+
}
|
|
439
|
+
async dropTable(object, _options) {
|
|
440
|
+
await this.knex.schema.dropTableIfExists(object);
|
|
366
441
|
}
|
|
367
442
|
/**
|
|
368
443
|
* Batch-initialise tables from an array of object definitions.
|
|
@@ -370,7 +445,7 @@ var SqlDriver = class {
|
|
|
370
445
|
async initObjects(objects) {
|
|
371
446
|
await this.ensureDatabaseExists();
|
|
372
447
|
for (const obj of objects) {
|
|
373
|
-
const tableName = obj.name;
|
|
448
|
+
const tableName = obj.tableName || obj.name;
|
|
374
449
|
const jsonCols = [];
|
|
375
450
|
const booleanCols = [];
|
|
376
451
|
if (obj.fields) {
|
|
@@ -395,6 +470,7 @@ var SqlDriver = class {
|
|
|
395
470
|
exists = false;
|
|
396
471
|
}
|
|
397
472
|
}
|
|
473
|
+
const builtinColumns = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
|
|
398
474
|
if (!exists) {
|
|
399
475
|
await this.knex.schema.createTable(tableName, (table) => {
|
|
400
476
|
table.string("id").primary();
|
|
@@ -402,6 +478,7 @@ var SqlDriver = class {
|
|
|
402
478
|
table.timestamp("updated_at").defaultTo(this.knex.fn.now());
|
|
403
479
|
if (obj.fields) {
|
|
404
480
|
for (const [name, field] of Object.entries(obj.fields)) {
|
|
481
|
+
if (builtinColumns.has(name)) continue;
|
|
405
482
|
this.createColumn(table, name, field);
|
|
406
483
|
}
|
|
407
484
|
}
|
|
@@ -495,7 +572,7 @@ var SqlDriver = class {
|
|
|
495
572
|
return;
|
|
496
573
|
}
|
|
497
574
|
for (const [key, value] of Object.entries(filters)) {
|
|
498
|
-
if (["
|
|
575
|
+
if (["limit", "offset", "fields", "orderBy"].includes(key)) continue;
|
|
499
576
|
builder.where(key, value);
|
|
500
577
|
}
|
|
501
578
|
return;
|
|
@@ -736,11 +813,12 @@ var SqlDriver = class {
|
|
|
736
813
|
}
|
|
737
814
|
// ── Database helpers ────────────────────────────────────────────────────────
|
|
738
815
|
async ensureDatabaseExists() {
|
|
739
|
-
if (
|
|
816
|
+
if (this.isSqlite) return;
|
|
817
|
+
if (!this.isPostgres && !this.isMysql) return;
|
|
740
818
|
try {
|
|
741
819
|
await this.knex.raw("SELECT 1");
|
|
742
820
|
} catch (e) {
|
|
743
|
-
if (e.code === "3D000") {
|
|
821
|
+
if (e.code === "3D000" || e.code === "ER_BAD_DB_ERROR" || e.errno === 1049) {
|
|
744
822
|
await this.createDatabase();
|
|
745
823
|
} else {
|
|
746
824
|
throw e;
|
|
@@ -752,18 +830,37 @@ var SqlDriver = class {
|
|
|
752
830
|
const connection = config.connection;
|
|
753
831
|
let dbName = "";
|
|
754
832
|
const adminConfig = { ...config };
|
|
755
|
-
if (
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
833
|
+
if (this.isPostgres) {
|
|
834
|
+
if (typeof connection === "string") {
|
|
835
|
+
const url = new URL(connection);
|
|
836
|
+
dbName = url.pathname.slice(1);
|
|
837
|
+
url.pathname = "/postgres";
|
|
838
|
+
adminConfig.connection = url.toString();
|
|
839
|
+
} else {
|
|
840
|
+
dbName = connection.database;
|
|
841
|
+
adminConfig.connection = { ...connection, database: "postgres" };
|
|
842
|
+
}
|
|
843
|
+
} else if (this.isMysql) {
|
|
844
|
+
if (typeof connection === "string") {
|
|
845
|
+
const url = new URL(connection);
|
|
846
|
+
dbName = url.pathname.slice(1);
|
|
847
|
+
url.pathname = "/";
|
|
848
|
+
adminConfig.connection = url.toString();
|
|
849
|
+
} else {
|
|
850
|
+
dbName = connection.database;
|
|
851
|
+
const { database: _db, ...rest } = connection;
|
|
852
|
+
adminConfig.connection = rest;
|
|
853
|
+
}
|
|
760
854
|
} else {
|
|
761
|
-
|
|
762
|
-
adminConfig.connection = { ...connection, database: "postgres" };
|
|
855
|
+
return;
|
|
763
856
|
}
|
|
764
857
|
const adminKnex = (0, import_knex.default)(adminConfig);
|
|
765
858
|
try {
|
|
766
|
-
|
|
859
|
+
if (this.isPostgres) {
|
|
860
|
+
await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
|
|
861
|
+
} else if (this.isMysql) {
|
|
862
|
+
await adminKnex.raw(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
|
|
863
|
+
}
|
|
767
864
|
} finally {
|
|
768
865
|
await adminKnex.destroy();
|
|
769
866
|
}
|