db-model-router 1.0.4 → 1.0.5

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.
Files changed (92) hide show
  1. package/README.md +110 -16
  2. package/TODO.md +14 -0
  3. package/dbmr.schema.json +333 -0
  4. package/demo/.dockerignore +7 -0
  5. package/demo/.env.example +13 -0
  6. package/demo/Dockerfile +20 -0
  7. package/demo/app.js +37 -0
  8. package/demo/commons/add_migration.js +43 -0
  9. package/demo/commons/db.js +17 -0
  10. package/demo/commons/migrate.js +65 -0
  11. package/demo/commons/security.js +30 -0
  12. package/demo/commons/session.js +13 -0
  13. package/demo/dbmr.schema.json +362 -0
  14. package/demo/docs/llm.md +197 -0
  15. package/demo/llms.txt +70 -0
  16. package/demo/middleware/logger.js +67 -0
  17. package/demo/migrations/20260430155808_create_migrations_table.sql +6 -0
  18. package/demo/migrations/20260430155809_create_tables.sql +207 -0
  19. package/demo/models/addresses.js +22 -0
  20. package/demo/models/cart_items.js +18 -0
  21. package/demo/models/carts.js +16 -0
  22. package/demo/models/categories.js +20 -0
  23. package/demo/models/coupons.js +23 -0
  24. package/demo/models/order_items.js +21 -0
  25. package/demo/models/orders.js +25 -0
  26. package/demo/models/payments.js +21 -0
  27. package/demo/models/product_images.js +18 -0
  28. package/demo/models/product_reviews.js +20 -0
  29. package/demo/models/product_variants.js +20 -0
  30. package/demo/models/products.js +30 -0
  31. package/demo/models/shipments.js +19 -0
  32. package/demo/models/users.js +19 -0
  33. package/demo/models/wishlists.js +15 -0
  34. package/demo/openapi.json +5872 -0
  35. package/demo/package-lock.json +2810 -0
  36. package/demo/package.json +34 -0
  37. package/demo/routes/addresses.js +6 -0
  38. package/demo/routes/carts/cart_items.js +7 -0
  39. package/demo/routes/carts.js +6 -0
  40. package/demo/routes/categories.js +6 -0
  41. package/demo/routes/coupons.js +6 -0
  42. package/demo/routes/docs.js +18 -0
  43. package/demo/routes/health.js +35 -0
  44. package/demo/routes/index.js +39 -0
  45. package/demo/routes/orders/order_items.js +7 -0
  46. package/demo/routes/orders/payments.js +7 -0
  47. package/demo/routes/orders/shipments.js +7 -0
  48. package/demo/routes/orders.js +6 -0
  49. package/demo/routes/products/product_images.js +7 -0
  50. package/demo/routes/products/product_reviews.js +7 -0
  51. package/demo/routes/products/product_variants.js +7 -0
  52. package/demo/routes/products.js +6 -0
  53. package/demo/routes/users.js +6 -0
  54. package/demo/routes/wishlists.js +6 -0
  55. package/docker-compose.yml +1 -1
  56. package/package.json +8 -7
  57. package/scripts/demo-create.js +47 -0
  58. package/skill/SKILL.md +464 -0
  59. package/skill/references/cockroachdb.md +49 -0
  60. package/skill/references/dynamodb.md +53 -0
  61. package/skill/references/mongodb.md +56 -0
  62. package/skill/references/mssql.md +55 -0
  63. package/skill/references/oracle.md +52 -0
  64. package/skill/references/postgres.md +50 -0
  65. package/skill/references/redis.md +53 -0
  66. package/skill/references/sqlite3.md +43 -0
  67. package/src/cli/commands/generate.js +58 -17
  68. package/src/cli/commands/help.js +11 -6
  69. package/src/cli/commands/init.js +2 -2
  70. package/src/cli/commands/inspect.js +1 -0
  71. package/src/cli/diff-engine.js +52 -22
  72. package/src/cli/generate-docs-route.js +31 -0
  73. package/src/cli/generate-migration.js +356 -0
  74. package/src/cli/generate-route.js +52 -24
  75. package/src/cli/init/dependencies.js +3 -0
  76. package/src/cli/init/generators.js +1 -1
  77. package/src/cli/init.js +8 -8
  78. package/src/cockroachdb/db.js +90 -59
  79. package/src/commons/route.js +20 -20
  80. package/src/commons/validator.js +58 -1
  81. package/src/dynamodb/db.js +50 -27
  82. package/src/mongodb/db.js +1 -0
  83. package/src/mssql/db.js +89 -61
  84. package/src/mysql/db.js +1 -0
  85. package/src/oracle/db.js +1 -0
  86. package/src/postgres/db.js +61 -41
  87. package/src/redis/db.js +1 -0
  88. package/src/schema/schema-parser.js +43 -1
  89. package/src/schema/schema-printer.js +7 -0
  90. package/src/schema/schema-validator.js +17 -0
  91. package/src/sqlite3/db.js +1 -0
  92. package/docs/SKILL.md +0 -419
@@ -371,22 +371,30 @@ async function insert(table, data, uniqueKeys = []) {
371
371
  }
372
372
  }
373
373
 
374
- // Bulk insert via multi-row VALUES
374
+ // Bulk insert via multi-row VALUES in batches of 1000
375
+ const BATCH_SIZE = 1000;
376
+ const colList = columns.join(",");
375
377
  const client = await pool.connect();
376
378
  try {
377
- let paramIdx = 0;
378
- const allParams = [];
379
- const valuesClauses = array.map((row) => {
380
- const placeholders = columns.map((c) => {
381
- paramIdx++;
382
- allParams.push(sanitizeValue(row[c]));
383
- return "$" + paramIdx;
379
+ await client.query("BEGIN");
380
+ for (let offset = 0; offset < total; offset += BATCH_SIZE) {
381
+ const batch = array.slice(offset, offset + BATCH_SIZE);
382
+ let paramIdx = 0;
383
+ const allParams = [];
384
+ const valuesClauses = batch.map((row) => {
385
+ const placeholders = columns.map((c) => {
386
+ paramIdx++;
387
+ allParams.push(sanitizeValue(row[c]));
388
+ return "$" + paramIdx;
389
+ });
390
+ return "(" + placeholders.join(",") + ")";
384
391
  });
385
- return "(" + placeholders.join(",") + ")";
386
- });
387
- const sql = `INSERT INTO ${table} (${columns.join(",")}) VALUES ${valuesClauses.join(",")}`;
388
- await client.query(sql, allParams);
392
+ const sql = `INSERT INTO ${table} (${colList}) VALUES ${valuesClauses.join(",")}`;
393
+ await client.query(sql, allParams);
394
+ }
395
+ await client.query("COMMIT");
389
396
  } catch (e) {
397
+ await client.query("ROLLBACK").catch(() => {});
390
398
  throw mapPgError(e);
391
399
  } finally {
392
400
  client.release();
@@ -407,35 +415,40 @@ async function _insertOnConflict(table, array, columns, uniqueKeys, total) {
407
415
  let lastId = 0;
408
416
  const pk = await getPkColumn(table);
409
417
  const conflictCols = uniqueKeys.join(",");
418
+ const colList = columns.join(",");
419
+ const BATCH_SIZE = 1000;
410
420
 
411
- for (const row of array) {
412
- const vals = columns.map((c) => sanitizeValue(row[c]));
413
- const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
414
- let sql = `INSERT INTO ${table} (${columns.join(",")}) VALUES (${placeholders}) ON CONFLICT (${conflictCols}) DO NOTHING`;
415
- if (pk) sql += ` RETURNING ${pk}`;
421
+ const client = await pool.connect();
422
+ try {
423
+ await client.query("BEGIN");
424
+
425
+ for (let offset = 0; offset < total; offset += BATCH_SIZE) {
426
+ const batch = array.slice(offset, offset + BATCH_SIZE);
427
+ let paramIdx = 0;
428
+ const allParams = [];
429
+ const valuesClauses = batch.map((row) => {
430
+ const placeholders = columns.map((c) => {
431
+ paramIdx++;
432
+ allParams.push(sanitizeValue(row[c]));
433
+ return "$" + paramIdx;
434
+ });
435
+ return "(" + placeholders.join(",") + ")";
436
+ });
437
+ let sql = `INSERT INTO ${table} (${colList}) VALUES ${valuesClauses.join(",")} ON CONFLICT (${conflictCols}) DO NOTHING`;
438
+ if (pk) sql += ` RETURNING ${pk}`;
416
439
 
417
- const client = await pool.connect();
418
- try {
419
- const result = await client.query(sql, vals);
420
- if (result.rows && result.rows.length > 0 && pk) {
421
- lastId = result.rows[0][pk] || 0;
422
- } else if (total === 1 && pk) {
423
- // Row already existed, fetch its PK
424
- const whereClauses = uniqueKeys
425
- .map((k, i) => `${k} = $${i + 1}`)
426
- .join(" AND ");
427
- const whereVals = uniqueKeys.map((k) => row[k]);
428
- const fetched = await client.query(
429
- `SELECT ${pk} FROM ${table} WHERE ${whereClauses}`,
430
- whereVals,
431
- );
432
- if (fetched.rows.length > 0) lastId = fetched.rows[0][pk] || 0;
440
+ const result = await client.query(sql, allParams);
441
+ if (pk && result.rows && result.rows.length > 0) {
442
+ lastId = result.rows[result.rows.length - 1][pk] || lastId;
433
443
  }
434
- } catch (e) {
435
- throw mapPgError(e);
436
- } finally {
437
- client.release();
438
444
  }
445
+
446
+ await client.query("COMMIT");
447
+ } catch (e) {
448
+ await client.query("ROLLBACK").catch(() => {});
449
+ throw mapPgError(e);
450
+ } finally {
451
+ client.release();
439
452
  }
440
453
 
441
454
  return {
@@ -471,13 +484,13 @@ async function upsert(table, data, uniqueKeys = []) {
471
484
  const updateCols = columns.filter((c) => c !== keyCol);
472
485
  if (updateCols.length === 0) continue;
473
486
  const setClause = updateCols
474
- .map((c, i) => `${c} = $${i + 1}`)
487
+ .map((c, i) => `${c} = ${i + 1}`)
475
488
  .join(", ");
476
489
  const vals = [
477
490
  ...updateCols.map((c) => sanitizeValue(row[c])),
478
491
  row[keyCol],
479
492
  ];
480
- const sql = `UPDATE ${table} SET ${setClause} WHERE ${keyCol} = $${updateCols.length + 1}`;
493
+ const sql = `UPDATE ${table} SET ${setClause} WHERE ${keyCol} = ${updateCols.length + 1}`;
481
494
  await query(sql, vals);
482
495
  if (row[keyCol]) lastId = row[keyCol];
483
496
  }
@@ -497,34 +510,51 @@ async function upsert(table, data, uniqueKeys = []) {
497
510
  const nonUniqueColumns = columns.filter((c) => !uniqueKeys.includes(c));
498
511
  const pk = await getPkColumn(table);
499
512
  let lastId = 0;
513
+ const BATCH_SIZE = 1000;
514
+
515
+ const client = await pool.connect();
516
+ try {
517
+ await client.query("BEGIN");
500
518
 
501
- for (const row of array) {
502
- const vals = columns.map((c) => sanitizeValue(row[c]));
503
- const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
504
- const conflictCols = uniqueKeys.join(",");
505
519
  const updateSetSql = nonUniqueColumns
506
520
  .map((c) => `${c} = EXCLUDED.${c}`)
507
521
  .join(", ");
522
+ const conflictCols = uniqueKeys.join(",");
523
+ const colList = columns.join(",");
524
+
525
+ for (let offset = 0; offset < total; offset += BATCH_SIZE) {
526
+ const batch = array.slice(offset, offset + BATCH_SIZE);
527
+ let paramIdx = 0;
528
+ const allParams = [];
529
+ const valuesClauses = batch.map((row) => {
530
+ const placeholders = columns.map((c) => {
531
+ paramIdx++;
532
+ allParams.push(sanitizeValue(row[c]));
533
+ return "$" + paramIdx;
534
+ });
535
+ return "(" + placeholders.join(",") + ")";
536
+ });
508
537
 
509
- let sql = `INSERT INTO ${table} (${columns.join(",")}) VALUES (${placeholders}) ON CONFLICT (${conflictCols})`;
510
- if (updateSetSql) {
511
- sql += ` DO UPDATE SET ${updateSetSql}`;
512
- } else {
513
- sql += ` DO NOTHING`;
514
- }
515
- if (pk) sql += ` RETURNING ${pk}`;
538
+ let sql = `INSERT INTO ${table} (${colList}) VALUES ${valuesClauses.join(",")} ON CONFLICT (${conflictCols})`;
539
+ if (updateSetSql) {
540
+ sql += ` DO UPDATE SET ${updateSetSql}`;
541
+ } else {
542
+ sql += ` DO NOTHING`;
543
+ }
544
+ if (pk) sql += ` RETURNING ${pk}`;
516
545
 
517
- const client = await pool.connect();
518
- try {
519
- const result = await client.query(sql, vals);
520
- if (result.rows && result.rows.length > 0 && pk) {
521
- lastId = result.rows[0][pk] || 0;
546
+ const result = await client.query(sql, allParams);
547
+ if (pk && result.rows && result.rows.length > 0) {
548
+ lastId = result.rows[result.rows.length - 1][pk] || lastId;
522
549
  }
523
- } catch (e) {
524
- throw mapPgError(e);
525
- } finally {
526
- client.release();
527
550
  }
551
+
552
+ await client.query("COMMIT");
553
+ } catch (e) {
554
+ await client.query("ROLLBACK").catch(() => {});
555
+ throw mapPgError(e);
556
+ } finally {
557
+ client.release();
528
558
  }
529
559
 
530
560
  const response = {
@@ -554,6 +584,7 @@ module.exports = {
554
584
  query,
555
585
  qcount,
556
586
  remove,
587
+ delete: remove,
557
588
  upsert,
558
589
  change: upsert,
559
590
  insert,
@@ -157,12 +157,10 @@ module.exports = function route(model, override = {}) {
157
157
  })
158
158
  .post("/", (req, res) => {
159
159
  if (!req.body || !Array.isArray(req.body.data)) {
160
- return res
161
- .status(400)
162
- .send({
163
- type: "danger",
164
- message: "Request body must contain a 'data' array",
165
- });
160
+ return res.status(400).send({
161
+ type: "danger",
162
+ message: "Request body must contain a 'data' array",
163
+ });
166
164
  }
167
165
  let payload = payloadOverride(req.body.data, req, override);
168
166
  model
@@ -176,12 +174,10 @@ module.exports = function route(model, override = {}) {
176
174
  })
177
175
  .put("/", (req, res) => {
178
176
  if (!req.body || !Array.isArray(req.body.data)) {
179
- return res
180
- .status(400)
181
- .send({
182
- type: "danger",
183
- message: "Request body must contain a 'data' array",
184
- });
177
+ return res.status(400).send({
178
+ type: "danger",
179
+ message: "Request body must contain a 'data' array",
180
+ });
185
181
  }
186
182
  let payload = payloadOverride(req.body.data, req, override);
187
183
  model
@@ -194,15 +190,19 @@ module.exports = function route(model, override = {}) {
194
190
  });
195
191
  })
196
192
  .delete("/", (req, res) => {
197
- if (!req.body || !Array.isArray(req.body.data)) {
198
- return res
199
- .status(400)
200
- .send({
201
- type: "danger",
202
- message: "Request body must contain a 'data' array",
203
- });
193
+ if (!req.body || Object.keys(req.body).length === 0) {
194
+ return res.status(400).send({
195
+ type: "danger",
196
+ message: "Request body must contain filter criteria",
197
+ });
198
+ }
199
+ // Accept { data: [...] } (legacy array) or plain filter object in body
200
+ let payload;
201
+ if (req.body.data && Array.isArray(req.body.data)) {
202
+ payload = payloadOverride(req.body.data, req, override);
203
+ } else {
204
+ payload = payloadOverride(req.body, req, override);
204
205
  }
205
- let payload = payloadOverride(req.body.data, req, override);
206
206
  model
207
207
  .remove(payload)
208
208
  .then((response) => {
@@ -107,10 +107,66 @@ function getErrorMessage(errors) {
107
107
  }
108
108
  return message;
109
109
  }
110
+ function parseFilterValue(value) {
111
+ if (typeof value !== "string") {
112
+ return ["=", value];
113
+ }
114
+
115
+ // !in(john,snow,ram) -> not in
116
+ if (/^!in\((.+)\)$/i.test(value)) {
117
+ const items = value.match(/^!in\((.+)\)$/i)[1].split(",");
118
+ return ["not in", items];
119
+ }
120
+
121
+ // in(john,snow,ram) -> in
122
+ if (/^in\((.+)\)$/i.test(value)) {
123
+ const items = value.match(/^in\((.+)\)$/i)[1].split(",");
124
+ return ["in", items];
125
+ }
126
+
127
+ // !%john% -> not like
128
+ if (value.startsWith("!") && value.slice(1).includes("%")) {
129
+ return ["not like", value.slice(1)];
130
+ }
131
+
132
+ // %john% -> like
133
+ if (value.includes("%")) {
134
+ return ["like", value];
135
+ }
136
+
137
+ // >= (value arrives as >=xxx after URL decoding of >%3Dxxx)
138
+ if (value.startsWith(">=")) {
139
+ return [">=", value.slice(2)];
140
+ }
141
+
142
+ // <= (value arrives as <=xxx after URL decoding of <%3Dxxx)
143
+ if (value.startsWith("<=")) {
144
+ return ["<=", value.slice(2)];
145
+ }
146
+
147
+ // > greater than
148
+ if (value.startsWith(">")) {
149
+ return [">", value.slice(1)];
150
+ }
151
+
152
+ // < less than
153
+ if (value.startsWith("<")) {
154
+ return ["<", value.slice(1)];
155
+ }
156
+
157
+ // !value -> not equal
158
+ if (value.startsWith("!")) {
159
+ return ["!=", value.slice(1)];
160
+ }
161
+
162
+ return ["=", value];
163
+ }
164
+
110
165
  function objectToFilter(obj) {
111
166
  let filterArray = [];
112
167
  for (let key in obj) {
113
- filterArray.push([key, "=", obj[key]]);
168
+ let [operator, value] = parseFilterValue(obj[key]);
169
+ filterArray.push([key, operator, value]);
114
170
  }
115
171
  return [filterArray];
116
172
  }
@@ -150,6 +206,7 @@ module.exports = {
150
206
  errorResponse,
151
207
  validateInput,
152
208
  getErrorMessage,
209
+ parseFilterValue,
153
210
  objectToFilter,
154
211
  dataToFilter,
155
212
  };
@@ -295,27 +295,43 @@ async function remove(table, filter, safeDelete = null) {
295
295
  const items = await getAllMatchingItems(table, whereData);
296
296
 
297
297
  if (safeDelete != null) {
298
- // Soft delete: update each item
299
- for (const item of items) {
300
- const key = extractKey(item);
301
- await docClient.send(
302
- new UpdateCommand({
303
- TableName: tableName(table),
304
- Key: key,
305
- UpdateExpression: `SET #sd = :sdVal`,
306
- ExpressionAttributeNames: { "#sd": safeDelete },
307
- ExpressionAttributeValues: { ":sdVal": 1 },
308
- }),
298
+ // Soft delete: update each item in parallel batches
299
+ const CONCURRENCY = 25;
300
+ for (let i = 0; i < items.length; i += CONCURRENCY) {
301
+ const batch = items.slice(i, i + CONCURRENCY);
302
+ await Promise.all(
303
+ batch.map((item) =>
304
+ docClient.send(
305
+ new UpdateCommand({
306
+ TableName: tableName(table),
307
+ Key: extractKey(item),
308
+ UpdateExpression: `SET #sd = :sdVal`,
309
+ ExpressionAttributeNames: { "#sd": safeDelete },
310
+ ExpressionAttributeValues: { ":sdVal": 1 },
311
+ }),
312
+ ),
313
+ ),
309
314
  );
310
315
  }
311
316
  } else {
312
- // Hard delete
313
- for (const item of items) {
314
- const key = extractKey(item);
315
- await docClient.send(
316
- new DeleteCommand({
317
- TableName: tableName(table),
318
- Key: key,
317
+ // Hard delete in batches of 25, run in parallel groups
318
+ const PARALLEL = 40;
319
+ const deleteBatches = [];
320
+ for (let i = 0; i < items.length; i += 25) {
321
+ deleteBatches.push(items.slice(i, i + 25));
322
+ }
323
+ for (let i = 0; i < deleteBatches.length; i += PARALLEL) {
324
+ const group = deleteBatches.slice(i, i + PARALLEL);
325
+ await Promise.all(
326
+ group.map((batch) => {
327
+ const requestItems = {
328
+ [tableName(table)]: batch.map((item) => ({
329
+ DeleteRequest: { Key: extractKey(item) },
330
+ })),
331
+ };
332
+ return docClient.send(
333
+ new BatchWriteCommand({ RequestItems: requestItems }),
334
+ );
319
335
  }),
320
336
  );
321
337
  }
@@ -437,20 +453,26 @@ async function insert(table, data, uniqueKeys = []) {
437
453
  }
438
454
  return response;
439
455
  } else {
440
- // Batch write (max 25 items per batch)
456
+ // Batch write (max 25 items per batch), run in parallel groups
441
457
  const batches = [];
442
458
  for (let i = 0; i < array.length; i += 25) {
443
459
  batches.push(array.slice(i, i + 25));
444
460
  }
445
461
 
446
- for (const batch of batches) {
447
- const requestItems = {
448
- [tableName(table)]: batch.map((item) => ({
449
- PutRequest: { Item: item },
450
- })),
451
- };
452
- await docClient.send(
453
- new BatchWriteCommand({ RequestItems: requestItems }),
462
+ const PARALLEL = 40;
463
+ for (let i = 0; i < batches.length; i += PARALLEL) {
464
+ const group = batches.slice(i, i + PARALLEL);
465
+ await Promise.all(
466
+ group.map((batch) => {
467
+ const requestItems = {
468
+ [tableName(table)]: batch.map((item) => ({
469
+ PutRequest: { Item: item },
470
+ })),
471
+ };
472
+ return docClient.send(
473
+ new BatchWriteCommand({ RequestItems: requestItems }),
474
+ );
475
+ }),
454
476
  );
455
477
  }
456
478
 
@@ -544,6 +566,7 @@ module.exports = {
544
566
  where,
545
567
  qcount,
546
568
  remove,
569
+ delete: remove,
547
570
  upsert,
548
571
  change: upsert,
549
572
  insert,
package/src/mongodb/db.js CHANGED
@@ -373,6 +373,7 @@ module.exports = {
373
373
  query,
374
374
  qcount,
375
375
  remove,
376
+ delete: remove,
376
377
  upsert,
377
378
  change: upsert,
378
379
  insert,
package/src/mssql/db.js CHANGED
@@ -280,51 +280,61 @@ async function upsert(table, data, uniqueKeys = []) {
280
280
  const columns = Object.keys(array[0]);
281
281
  const nonUniqueColumns = columns.filter((c) => !uniqueKeys.includes(c));
282
282
  let lastId = 0;
283
+ // MSSQL has a 2100 parameter limit per request
284
+ const MAX_PARAMS = 2000;
285
+ const BATCH_SIZE = Math.max(1, Math.floor(MAX_PARAMS / columns.length));
283
286
 
284
- for (const row of array) {
285
- const request = pool.request();
286
- let paramIdx = 0;
287
-
288
- // Build source VALUES
289
- const valuePlaceholders = columns.map((c) => {
290
- const p = "@param" + paramIdx;
291
- request.input("param" + paramIdx, row[c]);
292
- paramIdx++;
293
- return p;
294
- });
287
+ const transaction = new sql.Transaction(pool);
288
+ await transaction.begin();
289
+ try {
290
+ for (let offset = 0; offset < total; offset += BATCH_SIZE) {
291
+ const batch = array.slice(offset, offset + BATCH_SIZE);
292
+ const request = new sql.Request(transaction);
293
+ let paramIdx = 0;
294
+
295
+ const valueRows = batch.map((row) => {
296
+ const placeholders = columns.map((c) => {
297
+ const p = "@p" + paramIdx;
298
+ request.input("p" + paramIdx, row[c]);
299
+ paramIdx++;
300
+ return p;
301
+ });
302
+ return "(" + placeholders.join(",") + ")";
303
+ });
304
+
305
+ const colList = columns.map(escapeId).join(",");
306
+ const onClause = uniqueKeys
307
+ .map((k) => `target.${escapeId(k)} = source.${escapeId(k)}`)
308
+ .join(" AND ");
295
309
 
296
- const colList = columns.map(escapeId).join(",");
297
- const onClause = uniqueKeys
298
- .map((k) => `target.${escapeId(k)} = source.${escapeId(k)}`)
299
- .join(" AND ");
310
+ let mergeSql = `MERGE INTO ${escapeId(table)} AS target`;
311
+ mergeSql += ` USING (VALUES ${valueRows.join(",")}) AS source (${colList})`;
312
+ mergeSql += ` ON ${onClause}`;
300
313
 
301
- let mergeSql = `MERGE INTO ${escapeId(table)} AS target`;
302
- mergeSql += ` USING (VALUES (${valuePlaceholders.join(",")})) AS source (${colList})`;
303
- mergeSql += ` ON ${onClause}`;
314
+ if (nonUniqueColumns.length > 0) {
315
+ const updateSet = nonUniqueColumns
316
+ .map((c) => `target.${escapeId(c)} = source.${escapeId(c)}`)
317
+ .join(",");
318
+ mergeSql += ` WHEN MATCHED THEN UPDATE SET ${updateSet}`;
319
+ }
304
320
 
305
- if (nonUniqueColumns.length > 0) {
306
- const updateSet = nonUniqueColumns
307
- .map((c) => `target.${escapeId(c)} = source.${escapeId(c)}`)
321
+ const insertCols = nonUniqueColumns.map((c) => escapeId(c)).join(",");
322
+ const insertVals = nonUniqueColumns
323
+ .map((c) => `source.${escapeId(c)}`)
308
324
  .join(",");
309
- mergeSql += ` WHEN MATCHED THEN UPDATE SET ${updateSet}`;
310
- }
325
+ mergeSql += ` WHEN NOT MATCHED THEN INSERT (${insertCols}) VALUES (${insertVals})`;
326
+ mergeSql += ` OUTPUT INSERTED.*;`;
311
327
 
312
- const insertCols = nonUniqueColumns.map((c) => escapeId(c)).join(",");
313
- const insertVals = nonUniqueColumns
314
- .map((c) => `source.${escapeId(c)}`)
315
- .join(",");
316
- mergeSql += ` WHEN NOT MATCHED THEN INSERT (${insertCols}) VALUES (${insertVals})`;
317
- mergeSql += ` OUTPUT INSERTED.*;`;
318
-
319
- try {
320
328
  const result = await request.query(mergeSql);
321
329
  if (result.recordset && result.recordset.length > 0) {
322
- const firstRow = result.recordset[0];
323
- lastId = firstRow.id || firstRow[Object.keys(firstRow)[0]] || 0;
330
+ const lastRow = result.recordset[result.recordset.length - 1];
331
+ lastId = lastRow.id || lastRow[Object.keys(lastRow)[0]] || lastId;
324
332
  }
325
- } catch (e) {
326
- throw mapMssqlError(e);
327
333
  }
334
+ await transaction.commit();
335
+ } catch (e) {
336
+ await transaction.rollback().catch(() => {});
337
+ throw mapMssqlError(e);
328
338
  }
329
339
 
330
340
  const response = {
@@ -395,36 +405,53 @@ async function insert(table, data, uniqueKeys = []) {
395
405
  }
396
406
  }
397
407
 
398
- // Bulk insert
399
- for (const row of array) {
400
- const request = pool.request();
401
- const colList = columns.map(escapeId).join(",");
402
- const valuePlaceholders = columns.map((c, i) => {
403
- request.input("param" + i, row[c]);
404
- return "@param" + i;
405
- });
406
-
407
- let sqlStr;
408
- if (hasAllUniqueKeys) {
409
- const onClause = uniqueKeys
410
- .map((k) => `target.${escapeId(k)} = source.${escapeId(k)}`)
411
- .join(" AND ");
412
- const insertCols = columns.map(escapeId).join(",");
413
- const insertVals = columns.map((c) => `source.${escapeId(c)}`).join(",");
408
+ // Bulk insert in batches within a transaction
409
+ // MSSQL has a 2100 parameter limit per request
410
+ const MAX_PARAMS = 2000;
411
+ const BATCH_SIZE = Math.max(1, Math.floor(MAX_PARAMS / columns.length));
412
+ const transaction = new sql.Transaction(pool);
413
+ await transaction.begin();
414
+ try {
415
+ for (let offset = 0; offset < total; offset += BATCH_SIZE) {
416
+ const batch = array.slice(offset, offset + BATCH_SIZE);
417
+ const request = new sql.Request(transaction);
418
+ let paramIdx = 0;
419
+
420
+ const valueRows = batch.map((row) => {
421
+ const placeholders = columns.map((c) => {
422
+ const p = "@p" + paramIdx;
423
+ request.input("p" + paramIdx, row[c]);
424
+ paramIdx++;
425
+ return p;
426
+ });
427
+ return "(" + placeholders.join(",") + ")";
428
+ });
429
+
430
+ const colList = columns.map(escapeId).join(",");
431
+ let sqlStr;
432
+ if (hasAllUniqueKeys) {
433
+ const onClause = uniqueKeys
434
+ .map((k) => `target.${escapeId(k)} = source.${escapeId(k)}`)
435
+ .join(" AND ");
436
+ const insertCols = columns.map(escapeId).join(",");
437
+ const insertVals = columns
438
+ .map((c) => `source.${escapeId(c)}`)
439
+ .join(",");
414
440
 
415
- sqlStr = `MERGE INTO ${escapeId(table)} AS target`;
416
- sqlStr += ` USING (VALUES (${valuePlaceholders.join(",")})) AS source (${colList})`;
417
- sqlStr += ` ON ${onClause}`;
418
- sqlStr += ` WHEN NOT MATCHED THEN INSERT (${insertCols}) VALUES (${insertVals});`;
419
- } else {
420
- sqlStr = `INSERT INTO ${escapeId(table)} (${colList}) VALUES (${valuePlaceholders.join(",")})`;
421
- }
441
+ sqlStr = `MERGE INTO ${escapeId(table)} AS target`;
442
+ sqlStr += ` USING (VALUES ${valueRows.join(",")}) AS source (${colList})`;
443
+ sqlStr += ` ON ${onClause}`;
444
+ sqlStr += ` WHEN NOT MATCHED THEN INSERT (${insertCols}) VALUES (${insertVals});`;
445
+ } else {
446
+ sqlStr = `INSERT INTO ${escapeId(table)} (${colList}) VALUES ${valueRows.join(",")}`;
447
+ }
422
448
 
423
- try {
424
449
  await request.query(sqlStr);
425
- } catch (e) {
426
- throw mapMssqlError(e);
427
450
  }
451
+ await transaction.commit();
452
+ } catch (e) {
453
+ await transaction.rollback().catch(() => {});
454
+ throw mapMssqlError(e);
428
455
  }
429
456
 
430
457
  return {
@@ -453,6 +480,7 @@ module.exports = {
453
480
  query,
454
481
  qcount,
455
482
  remove,
483
+ delete: remove,
456
484
  upsert,
457
485
  change: upsert,
458
486
  insert,
package/src/mysql/db.js CHANGED
@@ -375,6 +375,7 @@ module.exports = {
375
375
  query,
376
376
  qcount,
377
377
  remove,
378
+ delete: remove,
378
379
  upsert,
379
380
  change: upsert,
380
381
  pool,
package/src/oracle/db.js CHANGED
@@ -846,6 +846,7 @@ module.exports = {
846
846
  query,
847
847
  qcount,
848
848
  remove,
849
+ delete: remove,
849
850
  upsert,
850
851
  change: upsert,
851
852
  insert,