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