@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.mjs
CHANGED
|
@@ -4,21 +4,47 @@ import { nanoid } from "nanoid";
|
|
|
4
4
|
var DEFAULT_ID_LENGTH = 16;
|
|
5
5
|
var SqlDriver = class {
|
|
6
6
|
constructor(config) {
|
|
7
|
-
//
|
|
7
|
+
// IDataDriver metadata
|
|
8
8
|
this.name = "com.objectstack.driver.sql";
|
|
9
9
|
this.version = "1.0.0";
|
|
10
10
|
this.supports = {
|
|
11
|
+
// Basic CRUD Operations
|
|
12
|
+
create: true,
|
|
13
|
+
read: true,
|
|
14
|
+
update: true,
|
|
15
|
+
delete: true,
|
|
16
|
+
// Bulk Operations
|
|
17
|
+
bulkCreate: true,
|
|
18
|
+
bulkUpdate: true,
|
|
19
|
+
bulkDelete: true,
|
|
20
|
+
// Transaction & Connection Management
|
|
11
21
|
transactions: true,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
jsonFields: true,
|
|
15
|
-
arrayFields: true,
|
|
22
|
+
savepoints: false,
|
|
23
|
+
// Query Operations
|
|
16
24
|
queryFilters: true,
|
|
17
25
|
queryAggregations: true,
|
|
18
26
|
querySorting: true,
|
|
19
27
|
queryPagination: true,
|
|
20
28
|
queryWindowFunctions: true,
|
|
21
|
-
querySubqueries: true
|
|
29
|
+
querySubqueries: true,
|
|
30
|
+
queryCTE: false,
|
|
31
|
+
joins: true,
|
|
32
|
+
// Advanced Features
|
|
33
|
+
fullTextSearch: false,
|
|
34
|
+
jsonQuery: false,
|
|
35
|
+
geospatialQuery: false,
|
|
36
|
+
streaming: false,
|
|
37
|
+
jsonFields: true,
|
|
38
|
+
arrayFields: true,
|
|
39
|
+
vectorSearch: false,
|
|
40
|
+
// Schema Management
|
|
41
|
+
schemaSync: true,
|
|
42
|
+
migrations: false,
|
|
43
|
+
indexes: false,
|
|
44
|
+
// Performance & Optimization
|
|
45
|
+
connectionPooling: true,
|
|
46
|
+
preparedStatements: true,
|
|
47
|
+
queryCache: false
|
|
22
48
|
};
|
|
23
49
|
this.jsonFields = {};
|
|
24
50
|
this.booleanFields = {};
|
|
@@ -68,24 +94,18 @@ var SqlDriver = class {
|
|
|
68
94
|
} else {
|
|
69
95
|
builder.select("*");
|
|
70
96
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const field = item.field || item[0];
|
|
79
|
-
const dir = item.order || item[1] || "asc";
|
|
80
|
-
if (field) {
|
|
81
|
-
builder.orderBy(this.mapSortField(field), dir);
|
|
97
|
+
if (query.where) {
|
|
98
|
+
this.applyFilters(builder, query.where);
|
|
99
|
+
}
|
|
100
|
+
if (query.orderBy && Array.isArray(query.orderBy)) {
|
|
101
|
+
for (const item of query.orderBy) {
|
|
102
|
+
if (item.field) {
|
|
103
|
+
builder.orderBy(this.mapSortField(item.field), item.order || "asc");
|
|
82
104
|
}
|
|
83
105
|
}
|
|
84
106
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (offsetValue !== void 0) builder.offset(offsetValue);
|
|
88
|
-
if (limitValue !== void 0) builder.limit(limitValue);
|
|
107
|
+
if (query.offset !== void 0) builder.offset(query.offset);
|
|
108
|
+
if (query.limit !== void 0) builder.limit(query.limit);
|
|
89
109
|
let results;
|
|
90
110
|
try {
|
|
91
111
|
results = await builder;
|
|
@@ -116,6 +136,17 @@ var SqlDriver = class {
|
|
|
116
136
|
}
|
|
117
137
|
return null;
|
|
118
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Stream records matching a structured query.
|
|
141
|
+
* NOTE: Current implementation fetches all results then yields them.
|
|
142
|
+
* TODO: Use Knex .stream() for true cursor-based streaming on large datasets.
|
|
143
|
+
*/
|
|
144
|
+
async *findStream(object, query, options) {
|
|
145
|
+
const results = await this.find(object, query, options);
|
|
146
|
+
for (const row of results) {
|
|
147
|
+
yield row;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
119
150
|
async create(object, data, options) {
|
|
120
151
|
const { _id, ...rest } = data;
|
|
121
152
|
const toInsert = { ...rest };
|
|
@@ -144,44 +175,71 @@ var SqlDriver = class {
|
|
|
144
175
|
const updated = await this.getBuilder(object, options).where("id", id).first();
|
|
145
176
|
return this.formatOutput(object, updated) || null;
|
|
146
177
|
}
|
|
178
|
+
async upsert(object, data, conflictKeys, options) {
|
|
179
|
+
const { _id, ...rest } = data;
|
|
180
|
+
const toUpsert = { ...rest };
|
|
181
|
+
if (_id !== void 0 && toUpsert.id === void 0) {
|
|
182
|
+
toUpsert.id = _id;
|
|
183
|
+
} else if (toUpsert.id === void 0) {
|
|
184
|
+
toUpsert.id = nanoid(DEFAULT_ID_LENGTH);
|
|
185
|
+
}
|
|
186
|
+
const formatted = this.formatInput(object, toUpsert);
|
|
187
|
+
const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ["id"];
|
|
188
|
+
const builder = this.getBuilder(object, options);
|
|
189
|
+
await builder.insert(formatted).onConflict(mergeKeys).merge();
|
|
190
|
+
const result = await this.getBuilder(object, options).where("id", toUpsert.id).first();
|
|
191
|
+
return this.formatOutput(object, result) || toUpsert;
|
|
192
|
+
}
|
|
147
193
|
async delete(object, id, options) {
|
|
148
194
|
const builder = this.getBuilder(object, options);
|
|
149
|
-
|
|
195
|
+
const count = await builder.where("id", id).delete();
|
|
196
|
+
return count > 0;
|
|
150
197
|
}
|
|
151
198
|
// ===================================
|
|
152
|
-
//
|
|
199
|
+
// Bulk & Batch Operations
|
|
153
200
|
// ===================================
|
|
154
201
|
async bulkCreate(object, data, options) {
|
|
155
202
|
const builder = this.getBuilder(object, options);
|
|
156
203
|
return await builder.insert(data).returning("*");
|
|
157
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Batch-update multiple records by ID.
|
|
207
|
+
* NOTE: Current implementation performs sequential updates for correctness.
|
|
208
|
+
* TODO: Optimize with SQL CASE statements or batched transactions for performance.
|
|
209
|
+
*/
|
|
210
|
+
async bulkUpdate(object, updates, options) {
|
|
211
|
+
const results = [];
|
|
212
|
+
for (const { id, data } of updates) {
|
|
213
|
+
const updated = await this.update(object, id, data, options);
|
|
214
|
+
if (updated) results.push(updated);
|
|
215
|
+
}
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
218
|
+
async bulkDelete(object, ids, options) {
|
|
219
|
+
const builder = this.getBuilder(object, options);
|
|
220
|
+
await builder.whereIn("id", ids).delete();
|
|
221
|
+
}
|
|
158
222
|
async updateMany(object, query, data, options) {
|
|
159
223
|
const builder = this.getBuilder(object, options);
|
|
160
|
-
|
|
161
|
-
if (filters) this.applyFilters(builder, filters);
|
|
224
|
+
if (query.where) this.applyFilters(builder, query.where);
|
|
162
225
|
const count = await builder.update(data);
|
|
163
|
-
return
|
|
226
|
+
return count || 0;
|
|
164
227
|
}
|
|
165
228
|
async deleteMany(object, query, options) {
|
|
166
229
|
const builder = this.getBuilder(object, options);
|
|
167
|
-
|
|
168
|
-
if (filters) this.applyFilters(builder, filters);
|
|
230
|
+
if (query.where) this.applyFilters(builder, query.where);
|
|
169
231
|
const count = await builder.delete();
|
|
170
|
-
return
|
|
232
|
+
return count || 0;
|
|
171
233
|
}
|
|
172
234
|
async count(object, query, options) {
|
|
173
235
|
const builder = this.getBuilder(object, options);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
actualFilters = query.where || query.filters;
|
|
177
|
-
}
|
|
178
|
-
if (actualFilters) {
|
|
179
|
-
this.applyFilters(builder, actualFilters);
|
|
236
|
+
if (query?.where) {
|
|
237
|
+
this.applyFilters(builder, query.where);
|
|
180
238
|
}
|
|
181
239
|
const result = await builder.count("* as count");
|
|
182
240
|
if (result && result.length > 0) {
|
|
183
241
|
const row = result[0];
|
|
184
|
-
return Number(row.count
|
|
242
|
+
return Number(row.count ?? row["count(*)"] ?? 0);
|
|
185
243
|
}
|
|
186
244
|
return 0;
|
|
187
245
|
}
|
|
@@ -201,11 +259,21 @@ var SqlDriver = class {
|
|
|
201
259
|
async beginTransaction() {
|
|
202
260
|
return await this.knex.transaction();
|
|
203
261
|
}
|
|
262
|
+
/** IDataDriver standard */
|
|
263
|
+
async commit(transaction) {
|
|
264
|
+
await transaction.commit();
|
|
265
|
+
}
|
|
266
|
+
/** IDataDriver standard */
|
|
267
|
+
async rollback(transaction) {
|
|
268
|
+
await transaction.rollback();
|
|
269
|
+
}
|
|
270
|
+
/** @deprecated Use commit() instead */
|
|
204
271
|
async commitTransaction(trx) {
|
|
205
|
-
await
|
|
272
|
+
await this.commit(trx);
|
|
206
273
|
}
|
|
274
|
+
/** @deprecated Use rollback() instead */
|
|
207
275
|
async rollbackTransaction(trx) {
|
|
208
|
-
await
|
|
276
|
+
await this.rollback(trx);
|
|
209
277
|
}
|
|
210
278
|
// ===================================
|
|
211
279
|
// Aggregation
|
|
@@ -274,6 +342,10 @@ var SqlDriver = class {
|
|
|
274
342
|
// ===================================
|
|
275
343
|
// Query Plan Analysis
|
|
276
344
|
// ===================================
|
|
345
|
+
/** IDataDriver standard: analyze query performance */
|
|
346
|
+
async explain(object, query, options) {
|
|
347
|
+
return this.analyzeQuery(object, query, options);
|
|
348
|
+
}
|
|
277
349
|
async analyzeQuery(object, query, options) {
|
|
278
350
|
const builder = this.getBuilder(object, options);
|
|
279
351
|
if (query.fields) {
|
|
@@ -325,7 +397,10 @@ var SqlDriver = class {
|
|
|
325
397
|
// ===================================
|
|
326
398
|
async syncSchema(object, schema, _options) {
|
|
327
399
|
const objectDef = schema;
|
|
328
|
-
await this.initObjects([objectDef]);
|
|
400
|
+
await this.initObjects([{ ...objectDef, name: object }]);
|
|
401
|
+
}
|
|
402
|
+
async dropTable(object, _options) {
|
|
403
|
+
await this.knex.schema.dropTableIfExists(object);
|
|
329
404
|
}
|
|
330
405
|
/**
|
|
331
406
|
* Batch-initialise tables from an array of object definitions.
|
|
@@ -333,7 +408,7 @@ var SqlDriver = class {
|
|
|
333
408
|
async initObjects(objects) {
|
|
334
409
|
await this.ensureDatabaseExists();
|
|
335
410
|
for (const obj of objects) {
|
|
336
|
-
const tableName = obj.name;
|
|
411
|
+
const tableName = obj.tableName || obj.name;
|
|
337
412
|
const jsonCols = [];
|
|
338
413
|
const booleanCols = [];
|
|
339
414
|
if (obj.fields) {
|
|
@@ -358,6 +433,7 @@ var SqlDriver = class {
|
|
|
358
433
|
exists = false;
|
|
359
434
|
}
|
|
360
435
|
}
|
|
436
|
+
const builtinColumns = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
|
|
361
437
|
if (!exists) {
|
|
362
438
|
await this.knex.schema.createTable(tableName, (table) => {
|
|
363
439
|
table.string("id").primary();
|
|
@@ -365,6 +441,7 @@ var SqlDriver = class {
|
|
|
365
441
|
table.timestamp("updated_at").defaultTo(this.knex.fn.now());
|
|
366
442
|
if (obj.fields) {
|
|
367
443
|
for (const [name, field] of Object.entries(obj.fields)) {
|
|
444
|
+
if (builtinColumns.has(name)) continue;
|
|
368
445
|
this.createColumn(table, name, field);
|
|
369
446
|
}
|
|
370
447
|
}
|
|
@@ -458,7 +535,7 @@ var SqlDriver = class {
|
|
|
458
535
|
return;
|
|
459
536
|
}
|
|
460
537
|
for (const [key, value] of Object.entries(filters)) {
|
|
461
|
-
if (["
|
|
538
|
+
if (["limit", "offset", "fields", "orderBy"].includes(key)) continue;
|
|
462
539
|
builder.where(key, value);
|
|
463
540
|
}
|
|
464
541
|
return;
|
|
@@ -699,11 +776,12 @@ var SqlDriver = class {
|
|
|
699
776
|
}
|
|
700
777
|
// ── Database helpers ────────────────────────────────────────────────────────
|
|
701
778
|
async ensureDatabaseExists() {
|
|
702
|
-
if (
|
|
779
|
+
if (this.isSqlite) return;
|
|
780
|
+
if (!this.isPostgres && !this.isMysql) return;
|
|
703
781
|
try {
|
|
704
782
|
await this.knex.raw("SELECT 1");
|
|
705
783
|
} catch (e) {
|
|
706
|
-
if (e.code === "3D000") {
|
|
784
|
+
if (e.code === "3D000" || e.code === "ER_BAD_DB_ERROR" || e.errno === 1049) {
|
|
707
785
|
await this.createDatabase();
|
|
708
786
|
} else {
|
|
709
787
|
throw e;
|
|
@@ -715,18 +793,37 @@ var SqlDriver = class {
|
|
|
715
793
|
const connection = config.connection;
|
|
716
794
|
let dbName = "";
|
|
717
795
|
const adminConfig = { ...config };
|
|
718
|
-
if (
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
796
|
+
if (this.isPostgres) {
|
|
797
|
+
if (typeof connection === "string") {
|
|
798
|
+
const url = new URL(connection);
|
|
799
|
+
dbName = url.pathname.slice(1);
|
|
800
|
+
url.pathname = "/postgres";
|
|
801
|
+
adminConfig.connection = url.toString();
|
|
802
|
+
} else {
|
|
803
|
+
dbName = connection.database;
|
|
804
|
+
adminConfig.connection = { ...connection, database: "postgres" };
|
|
805
|
+
}
|
|
806
|
+
} else if (this.isMysql) {
|
|
807
|
+
if (typeof connection === "string") {
|
|
808
|
+
const url = new URL(connection);
|
|
809
|
+
dbName = url.pathname.slice(1);
|
|
810
|
+
url.pathname = "/";
|
|
811
|
+
adminConfig.connection = url.toString();
|
|
812
|
+
} else {
|
|
813
|
+
dbName = connection.database;
|
|
814
|
+
const { database: _db, ...rest } = connection;
|
|
815
|
+
adminConfig.connection = rest;
|
|
816
|
+
}
|
|
723
817
|
} else {
|
|
724
|
-
|
|
725
|
-
adminConfig.connection = { ...connection, database: "postgres" };
|
|
818
|
+
return;
|
|
726
819
|
}
|
|
727
820
|
const adminKnex = knex(adminConfig);
|
|
728
821
|
try {
|
|
729
|
-
|
|
822
|
+
if (this.isPostgres) {
|
|
823
|
+
await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
|
|
824
|
+
} else if (this.isMysql) {
|
|
825
|
+
await adminKnex.raw(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
|
|
826
|
+
}
|
|
730
827
|
} finally {
|
|
731
828
|
await adminKnex.destroy();
|
|
732
829
|
}
|