db-model-router 1.0.3 → 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 (97) hide show
  1. package/README.md +283 -25
  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 +16 -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 +185 -0
  69. package/src/cli/commands/init.js +42 -14
  70. package/src/cli/commands/inspect.js +21 -3
  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-model.js +5 -4
  75. package/src/cli/generate-route.js +79 -45
  76. package/src/cli/init/dependencies.js +17 -5
  77. package/src/cli/init/generators.js +1073 -64
  78. package/src/cli/init/prompt.js +37 -5
  79. package/src/cli/init.js +148 -25
  80. package/src/cli/main.js +90 -10
  81. package/src/cockroachdb/db.js +90 -59
  82. package/src/commons/route.js +20 -20
  83. package/src/commons/validator.js +58 -1
  84. package/src/dynamodb/db.js +50 -27
  85. package/src/index.js +2 -0
  86. package/src/mongodb/db.js +1 -0
  87. package/src/mssql/db.js +89 -61
  88. package/src/mysql/db.js +1 -0
  89. package/src/oracle/db.js +1 -0
  90. package/src/postgres/db.js +61 -41
  91. package/src/redis/db.js +1 -0
  92. package/src/schema/schema-parser.js +43 -1
  93. package/src/schema/schema-printer.js +8 -5
  94. package/src/schema/schema-to-meta.js +4 -0
  95. package/src/schema/schema-validator.js +20 -1
  96. package/src/sqlite3/db.js +1 -0
  97. package/docs/SKILL.md +0 -374
@@ -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/index.js CHANGED
@@ -2,6 +2,7 @@ const model = require("./commons/model.js");
2
2
  const route = require("./commons/route.js");
3
3
  const routers = {
4
4
  mysql: "./mysql/db.js",
5
+ mariadb: "./mysql/db.js",
5
6
  postgresql: "./postgres/db.js",
6
7
  postgres: "./postgres/db.js",
7
8
  oracle: "./oracle/db.js",
@@ -29,6 +30,7 @@ function init(DB_TYPE) {
29
30
  if (err.code === "MODULE_NOT_FOUND") {
30
31
  const driverMap = {
31
32
  mysql: "mysql2",
33
+ mariadb: "mysql2",
32
34
  postgresql: "pg",
33
35
  postgres: "pg",
34
36
  oracle: "oracledb",
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,
@@ -462,22 +462,30 @@ async function insert(table, data, uniqueKeys = []) {
462
462
  }
463
463
  }
464
464
 
465
- // Bulk insert via multi-row VALUES
465
+ // Bulk insert via multi-row VALUES in batches of 1000
466
+ const BATCH_SIZE = 1000;
467
+ const colList = columns.map(escapeId).join(",");
466
468
  const client = await pool.connect();
467
469
  try {
468
- let paramIdx = 0;
469
- const allParams = [];
470
- const valuesClauses = array.map((row) => {
471
- const placeholders = columns.map((c) => {
472
- paramIdx++;
473
- allParams.push(sanitizeValue(row[c]));
474
- return "$" + paramIdx;
470
+ await client.query("BEGIN");
471
+ for (let offset = 0; offset < total; offset += BATCH_SIZE) {
472
+ const batch = array.slice(offset, offset + BATCH_SIZE);
473
+ let paramIdx = 0;
474
+ const allParams = [];
475
+ const valuesClauses = batch.map((row) => {
476
+ const placeholders = columns.map((c) => {
477
+ paramIdx++;
478
+ allParams.push(sanitizeValue(row[c]));
479
+ return "$" + paramIdx;
480
+ });
481
+ return "(" + placeholders.join(",") + ")";
475
482
  });
476
- return "(" + placeholders.join(",") + ")";
477
- });
478
- const sql = `INSERT INTO ${escapeId(table)} (${columns.map(escapeId).join(",")}) VALUES ${valuesClauses.join(",")}`;
479
- await client.query(sql, allParams);
483
+ const sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES ${valuesClauses.join(",")}`;
484
+ await client.query(sql, allParams);
485
+ }
486
+ await client.query("COMMIT");
480
487
  } catch (e) {
488
+ await client.query("ROLLBACK").catch(() => {});
481
489
  throw mapPgError(e);
482
490
  } finally {
483
491
  client.release();
@@ -499,31 +507,30 @@ async function _insertOnConflict(table, array, columns, uniqueKeys, total) {
499
507
  const pk = await getPkColumn(table);
500
508
  const conflictCols = uniqueKeys.map(escapeId).join(",");
501
509
  const colList = columns.map(escapeId).join(",");
510
+ const BATCH_SIZE = 1000;
502
511
 
503
512
  const client = await pool.connect();
504
513
  try {
505
514
  await client.query("BEGIN");
506
515
 
507
- for (const row of array) {
508
- const vals = columns.map((c) => sanitizeValue(row[c]));
509
- const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
510
- let sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES (${placeholders}) ON CONFLICT (${conflictCols}) DO NOTHING`;
516
+ for (let offset = 0; offset < total; offset += BATCH_SIZE) {
517
+ const batch = array.slice(offset, offset + BATCH_SIZE);
518
+ let paramIdx = 0;
519
+ const allParams = [];
520
+ const valuesClauses = batch.map((row) => {
521
+ const placeholders = columns.map((c) => {
522
+ paramIdx++;
523
+ allParams.push(sanitizeValue(row[c]));
524
+ return "$" + paramIdx;
525
+ });
526
+ return "(" + placeholders.join(",") + ")";
527
+ });
528
+ let sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES ${valuesClauses.join(",")} ON CONFLICT (${conflictCols}) DO NOTHING`;
511
529
  if (pk) sql += ` RETURNING ${escapeId(pk)}`;
512
530
 
513
- const result = await client.query(sql, vals);
514
- if (result.rows && result.rows.length > 0 && pk) {
515
- lastId = result.rows[0][pk] || 0;
516
- } else if (total === 1 && pk) {
517
- // Row already existed, fetch its PK
518
- const whereClauses = uniqueKeys
519
- .map((k, i) => `${escapeId(k)} = ${i + 1}`)
520
- .join(" AND ");
521
- const whereVals = uniqueKeys.map((k) => row[k]);
522
- const fetched = await client.query(
523
- `SELECT ${escapeId(pk)} FROM ${escapeId(table)} WHERE ${whereClauses}`,
524
- whereVals,
525
- );
526
- if (fetched.rows.length > 0) lastId = fetched.rows[0][pk] || 0;
531
+ const result = await client.query(sql, allParams);
532
+ if (pk && result.rows && result.rows.length > 0) {
533
+ lastId = result.rows[result.rows.length - 1][pk] || lastId;
527
534
  }
528
535
  }
529
536
 
@@ -594,20 +601,32 @@ async function upsert(table, data, uniqueKeys = []) {
594
601
  const nonUniqueColumns = columns.filter((c) => !uniqueKeys.includes(c));
595
602
  const pk = await getPkColumn(table);
596
603
  let lastId = 0;
604
+ const BATCH_SIZE = 1000;
597
605
 
598
606
  const client = await pool.connect();
599
607
  try {
600
608
  await client.query("BEGIN");
601
609
 
602
- for (const row of array) {
603
- const vals = columns.map((c) => sanitizeValue(row[c]));
604
- const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
605
- const conflictCols = uniqueKeys.map(escapeId).join(",");
606
- const updateSetSql = nonUniqueColumns
607
- .map((c) => `${escapeId(c)} = EXCLUDED.${escapeId(c)}`)
608
- .join(", ");
610
+ const updateSetSql = nonUniqueColumns
611
+ .map((c) => `${escapeId(c)} = EXCLUDED.${escapeId(c)}`)
612
+ .join(", ");
613
+ const conflictCols = uniqueKeys.map(escapeId).join(",");
614
+ const colList = columns.map(escapeId).join(",");
615
+
616
+ for (let offset = 0; offset < total; offset += BATCH_SIZE) {
617
+ const batch = array.slice(offset, offset + BATCH_SIZE);
618
+ let paramIdx = 0;
619
+ const allParams = [];
620
+ const valuesClauses = batch.map((row) => {
621
+ const placeholders = columns.map((c) => {
622
+ paramIdx++;
623
+ allParams.push(sanitizeValue(row[c]));
624
+ return "$" + paramIdx;
625
+ });
626
+ return "(" + placeholders.join(",") + ")";
627
+ });
609
628
 
610
- let sql = `INSERT INTO ${escapeId(table)} (${columns.map(escapeId).join(",")}) VALUES (${placeholders}) ON CONFLICT (${conflictCols})`;
629
+ let sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES ${valuesClauses.join(",")} ON CONFLICT (${conflictCols})`;
611
630
  if (updateSetSql) {
612
631
  sql += ` DO UPDATE SET ${updateSetSql}`;
613
632
  } else {
@@ -615,9 +634,9 @@ async function upsert(table, data, uniqueKeys = []) {
615
634
  }
616
635
  if (pk) sql += ` RETURNING ${escapeId(pk)}`;
617
636
 
618
- const result = await client.query(sql, vals);
619
- if (result.rows && result.rows.length > 0 && pk) {
620
- lastId = result.rows[0][pk] || 0;
637
+ const result = await client.query(sql, allParams);
638
+ if (pk && result.rows && result.rows.length > 0) {
639
+ lastId = result.rows[result.rows.length - 1][pk] || lastId;
621
640
  }
622
641
  }
623
642
 
@@ -656,6 +675,7 @@ module.exports = {
656
675
  query,
657
676
  qcount,
658
677
  remove,
678
+ delete: remove,
659
679
  upsert,
660
680
  change: upsert,
661
681
  insert,
package/src/redis/db.js CHANGED
@@ -440,6 +440,7 @@ module.exports = {
440
440
  where,
441
441
  qcount,
442
442
  remove,
443
+ delete: remove,
443
444
  upsert,
444
445
  change: upsert,
445
446
  insert,