@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/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
- // DriverInterface metadata
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
- joins: true,
13
- fullTextSearch: false,
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
- const filterCondition = query.where || query.filters;
72
- if (filterCondition) {
73
- this.applyFilters(builder, filterCondition);
74
- }
75
- const sortArray = query.orderBy || query.sort;
76
- if (sortArray && Array.isArray(sortArray)) {
77
- for (const item of sortArray) {
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
- const offsetValue = query.offset ?? query.skip;
86
- const limitValue = query.limit ?? query.top;
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
- return await builder.where("id", id).delete();
196
+ const count = await builder.where("id", id).delete();
197
+ return count > 0;
150
198
  }
151
199
  // ===================================
152
- // Optional bulk & batch
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
- const filters = query.where || query.filters || query;
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 { modifiedCount: count || 0 };
227
+ return count || 0;
164
228
  }
165
229
  async deleteMany(object, query, options) {
166
230
  const builder = this.getBuilder(object, options);
167
- const filters = query.where || query.filters || query;
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 { deletedCount: count || 0 };
233
+ return count || 0;
171
234
  }
172
235
  async count(object, query, options) {
173
236
  const builder = this.getBuilder(object, options);
174
- let actualFilters = query;
175
- if (query && (query.where || query.filters)) {
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 || 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 trx.commit();
273
+ await this.commit(trx);
206
274
  }
275
+ /** @deprecated Use rollback() instead */
207
276
  async rollbackTransaction(trx) {
208
- await trx.rollback();
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 (["filters", "sort", "limit", "skip", "offset", "fields", "orderBy"].includes(key)) continue;
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 (!this.isPostgres) return;
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 (typeof connection === "string") {
719
- const url = new URL(connection);
720
- dbName = url.pathname.slice(1);
721
- url.pathname = "/postgres";
722
- adminConfig.connection = url.toString();
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
- dbName = connection.database;
725
- adminConfig.connection = { ...connection, database: "postgres" };
819
+ return;
726
820
  }
727
821
  const adminKnex = knex(adminConfig);
728
822
  try {
729
- await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
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
  }