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