w-orm-reladb 1.0.47 → 1.0.49

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/README.md CHANGED
@@ -11,6 +11,62 @@ An operator for relational database in nodejs.
11
11
  ## Documentation
12
12
  To view documentation or get support, visit [docs](https://yuda-lyu.github.io/w-orm-reladb/WOrm.html).
13
13
 
14
+ ### `insert` with `option.returnList`
15
+
16
+ An aggregate count answers "how many rows were new", but not **which ones** — and the latter is what deduplication actually needs, when only new rows should trigger an expensive downstream action. Setting `returnList` hands back the per row verdict that `insert` already computes internally:
17
+
18
+ ```alias
19
+ let rs = await w.insert(data, { returnList: true })
20
+ // => [ { n: 1, nInserted: 1, ok: 1 }, { n: 1, nInserted: 0, ok: 1 }, ... ]
21
+ let fresh = data.filter((v, k) => rs[k].nInserted === 1)
22
+ ```
23
+
24
+ The returned array is the same length as the input and in the same order. Each element carries `n: 1` and `ok: 1` — any failure in `insert` is a batch level error and rejects, so a per row element never reports a failure. `rs.filter(v => v.nInserted === 1).length` equals the `nInserted` of the default aggregate form, and invalid input returns `[]` instead of the aggregate empty result.
25
+
26
+ The switch is static: the shape is decided solely by the value you write at the call site, never by the data or by what happened at run time. Treat the two values as two separate contracts and do not share result handling code between call sites that use different ones.
27
+
28
+ ### `insert` and `insertBulk`
29
+
30
+ Both write rows that do not yet exist, and both return `{ n, nInserted, ok }`. They differ in what happens on a conflict, so `insertBulk` is **not** a faster `insert`:
31
+
32
+ | Situation | `insert` | `insertBulk` |
33
+ |---|---|---|
34
+ | Primary key already exists | that row is skipped, the batch still returns `ok: 1` | the whole call **rejects, and not a single row is written** |
35
+ | Duplicate primary keys inside one batch | only the first one counts toward `nInserted` | treated as a conflict, the whole call rejects |
36
+ | `nInserted` | rows actually inserted, `0 ≤ nInserted ≤ n` | equals `n` whenever the call succeeds |
37
+ | Use it for | ordinary writes against a table that may already hold data | bulk import where no conflict is expected |
38
+
39
+ With no conflict the two are indistinguishable, so you may swap them under that assumption — and if the assumption is wrong, `insertBulk` tells you by rejecting rather than silently skipping.
40
+
41
+ `insertBulk` is considerably faster here, because `insert` has to write row by row to report an exact `nInserted` (measured on this machine, Windows 11 / Node 24 / sequelize 6):
42
+
43
+ | Rows | sqlite `insert` → `insertBulk` | mssql `insert` → `insertBulk` |
44
+ |---|---|---|
45
+ | 1000 | 5549 ms → 25 ms (226x) | 6802 ms → 190 ms (36x) |
46
+ | 5000 | 29825 ms → 50 ms (592x) | 44116 ms → 489 ms (90x) |
47
+
48
+ The all-or-nothing guarantee is provided by a transaction, not by the batch statement alone: mssql splits a large batch into several statements because of its bind parameter limit, so without one an interrupted batch would leave earlier rows behind. When you pass `option.transaction`, a SAVEPOINT is used instead, so a failure rolls back only this call and leaves the rest of your transaction untouched.
49
+
50
+ ### Closed instances
51
+
52
+ `init()` hands you an instance you can share across calls through `option.instance`, and `instance.close()` ends it. Once closed, **every function rejects immediately** with a message naming the closed state — including `selectByPk`, which does not fall back to its usual `null`. This matters because a closed instance and "no such row" would otherwise look identical, and a deduplication caller reading that as "not present yet" would re-run the whole batch downstream. Do not keep using an instance after closing it; call `init()` again for a new one.
53
+
54
+ Note also that a plain call made without `option.instance` opens and closes its own connection, so it ends any instance you are holding. Do not mix the two styles against the same `WOrmReladb` object.
55
+
56
+ ### Concurrency
57
+
58
+ The scope of the atomicity guarantee, by range:
59
+
60
+ | Range | Guaranteed | Provided by |
61
+ |---|---|---|
62
+ | Cross process — several processes against the same database | Yes | The database itself. `insert` and `save` write through single conditional statements, and the primary key unique constraint decides the winner. Independent of `opt.useStable`. |
63
+ | Single process — several parallel calls in one process | Yes **when `opt.useStable` is `true`** (the default) | A built-in queue that runs one operation at a time. |
64
+ | Single process, with `opt.useStable` set to `false` | **No** | — |
65
+
66
+ **Do not issue parallel calls from one process while `opt.useStable` is `false`.** Each instance holds one shared connection, opened at the start of a call and closed at its end, so a second call running in parallel closes the connection the first one is still using. Measured on 30 parallel calls in a single process: sqlite raised `SQLITE_MISUSE: Database handle is closed` and rejected 12 of the batches, mssql reported `ConnectionManager.getConnection was called after the connection manager was closed!` as a per row failure; both ended with 29 of the 30 rows written, so writes are silently lost. Keep the default `true`, or serialize the calls yourself (for example `await` them one by one).
67
+
68
+ Cross process was measured with 2 independent processes against the same 20 primary keys: `nInserted` summed to exactly 20 with 20 rows in the table, and with each process writing a different column to those keys all 40 operations returned `ok: 1` with both columns preserved on all 20 rows. sqlite was repeated over 6 rounds to rule out timing dependent `SQLITE_BUSY`. Platform: Windows 11, Node 24, sequelize 6, MSSQL 2022.
69
+
14
70
  ## Before Installation
15
71
  > If you need to use encrypted sqlite, you need to manually install `@journeyapps/sqlcipher`, as follows:
16
72
  >1. Open visual studio code by system administrator.
@@ -42,7 +98,7 @@ let opt = {
42
98
  cl: 'users',
43
99
  fdModels: './models',
44
100
  // modelType: 'json',
45
- // autoGenPK: false,
101
+ // autoGenPk: false,
46
102
  }
47
103
 
48
104
  let rs = [
@@ -106,8 +162,8 @@ async function test() {
106
162
  w.on('change', function(mode, data, res) {
107
163
  console.log('change', mode)
108
164
  })
109
- w.on('error', function(err) {
110
- console.log('error', err)
165
+ w.on('error', function(mode, data, err) {
166
+ console.log('error', mode, err)
111
167
  })
112
168
 
113
169
 
@@ -196,10 +252,10 @@ test()
196
252
  // insert then { n: 3, nInserted: 3, ok: 1 }
197
253
  // change save
198
254
  // save then [
199
- // { n: 1, nModified: 1, ok: 1 },
200
- // { n: 1, nModified: 1, ok: 1 },
201
- // { n: 0, nModified: 0, ok: 1 } //autoInsert=false
202
- // { n: 1, nInserted: 1, ok: 1 } //autoInsert=true
255
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
256
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
257
+ // { n: 0, nInserted: 0, nModified: 0, ok: 1 } //autoInsert=false
258
+ // { n: 1, nInserted: 1, nModified: 0, ok: 1 } //autoInsert=true
203
259
  // ]
204
260
  // select all [
205
261
  // { id: 'id-peter', name: 'peter(modify)', value: 123 },
@@ -243,7 +299,7 @@ let opt = {
243
299
  cl: 'users',
244
300
  fdModels: './models',
245
301
  // modelType: 'json',
246
- // autoGenPK: false,
302
+ // autoGenPk: false,
247
303
  }
248
304
 
249
305
  let rs = [
@@ -307,8 +363,8 @@ async function testCommit() {
307
363
  w.on('change', function(mode, data, res) {
308
364
  console.log('change', mode)
309
365
  })
310
- w.on('error', function(err) {
311
- console.log('error', err)
366
+ w.on('error', function(mode, data, err) {
367
+ console.log('error', mode, err)
312
368
  })
313
369
 
314
370
 
@@ -424,10 +480,10 @@ testCommit()
424
480
  // insert then { n: 3, nInserted: 3, ok: 1 }
425
481
  // change save
426
482
  // save then [
427
- // { n: 1, nModified: 1, ok: 1 },
428
- // { n: 1, nModified: 1, ok: 1 },
429
- // { n: 0, nModified: 0, ok: 1 } //autoInsert=false
430
- // { n: 1, nInserted: 1, ok: 1 } //autoInsert=true
483
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
484
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
485
+ // { n: 0, nInserted: 0, nModified: 0, ok: 1 } //autoInsert=false
486
+ // { n: 1, nInserted: 1, nModified: 0, ok: 1 } //autoInsert=true
431
487
  // ]
432
488
  // change del
433
489
  // del then [
@@ -460,7 +516,7 @@ let opt = {
460
516
  cl: 'users',
461
517
  fdModels: './models',
462
518
  // modelType: 'json',
463
- // autoGenPK: false,
519
+ // autoGenPk: false,
464
520
  }
465
521
 
466
522
  let rs = [
@@ -524,8 +580,8 @@ async function testRollback() {
524
580
  w.on('change', function(mode, data, res) {
525
581
  console.log('change', mode)
526
582
  })
527
- w.on('error', function(err) {
528
- console.log('error', err)
583
+ w.on('error', function(mode, data, err) {
584
+ console.log('error', mode, err)
529
585
  })
530
586
 
531
587
 
@@ -638,10 +694,10 @@ testRollback()
638
694
  // insert then { n: 3, nInserted: 3, ok: 1 }
639
695
  // change save
640
696
  // save then [
641
- // { n: 1, nModified: 1, ok: 1 },
642
- // { n: 1, nModified: 1, ok: 1 },
643
- // { n: 0, nModified: 0, ok: 1 } //autoInsert=false
644
- // { n: 1, nInserted: 1, ok: 1 } //autoInsert=true
697
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
698
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
699
+ // { n: 0, nInserted: 0, nModified: 0, ok: 1 } //autoInsert=false
700
+ // { n: 1, nInserted: 1, nModified: 0, ok: 1 } //autoInsert=true
645
701
  // ]
646
702
  // change del
647
703
  // del then [
@@ -672,7 +728,7 @@ let opt = {
672
728
  cl: 'users',
673
729
  fdModels: './models',
674
730
  // modelType: 'json',
675
- // autoGenPK: false,
731
+ // autoGenPk: false,
676
732
  storage: './worm.sqlite',
677
733
  }
678
734
 
@@ -743,8 +799,8 @@ async function test() {
743
799
  w.on('change', function(mode, data, res) {
744
800
  console.log('change', mode)
745
801
  })
746
- w.on('error', function(err) {
747
- console.log('error', err)
802
+ w.on('error', function(mode, data, err) {
803
+ console.log('error', mode, err)
748
804
  })
749
805
 
750
806
 
@@ -833,10 +889,10 @@ test()
833
889
  // insert then { n: 3, nInserted: 3, ok: 1 }
834
890
  // change save
835
891
  // save then [
836
- // { n: 1, nModified: 1, ok: 1 },
837
- // { n: 1, nModified: 1, ok: 1 },
838
- // { n: 0, nModified: 0, ok: 1 } //autoInsert=false
839
- // { n: 1, nInserted: 1, ok: 1 } //autoInsert=true
892
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
893
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
894
+ // { n: 0, nInserted: 0, nModified: 0, ok: 1 } //autoInsert=false
895
+ // { n: 1, nInserted: 1, nModified: 0, ok: 1 } //autoInsert=true
840
896
  // ]
841
897
  // select all [
842
898
  // { id: 'id-peter', name: 'peter(modify)', value: 123 },
@@ -881,7 +937,7 @@ let opt = {
881
937
  cl: 'users',
882
938
  fdModels: './models',
883
939
  // modelType: 'json',
884
- // autoGenPK: false,
940
+ // autoGenPk: false,
885
941
  storage: './worm.sqlite',
886
942
  }
887
943
 
@@ -951,8 +1007,8 @@ async function testCommit() {
951
1007
  w.on('change', function(mode, data, res) {
952
1008
  console.log('change', mode)
953
1009
  })
954
- w.on('error', function(err) {
955
- console.log('error', err)
1010
+ w.on('error', function(mode, data, err) {
1011
+ console.log('error', mode, err)
956
1012
  })
957
1013
 
958
1014
 
@@ -1068,10 +1124,10 @@ testCommit()
1068
1124
  // insert then { n: 3, nInserted: 3, ok: 1 }
1069
1125
  // change save
1070
1126
  // save then [
1071
- // { n: 1, nModified: 1, ok: 1 },
1072
- // { n: 1, nModified: 1, ok: 1 },
1073
- // { n: 0, nModified: 0, ok: 1 } //autoInsert=false
1074
- // { n: 1, nInserted: 1, ok: 1 } //autoInsert=true
1127
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
1128
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
1129
+ // { n: 0, nInserted: 0, nModified: 0, ok: 1 } //autoInsert=false
1130
+ // { n: 1, nInserted: 1, nModified: 0, ok: 1 } //autoInsert=true
1075
1131
  // ]
1076
1132
  // change del
1077
1133
  // del then [
@@ -1105,7 +1161,7 @@ let opt = {
1105
1161
  cl: 'users',
1106
1162
  fdModels: './models',
1107
1163
  // modelType: 'json',
1108
- // autoGenPK: false,
1164
+ // autoGenPk: false,
1109
1165
  storage: './worm.sqlite',
1110
1166
  }
1111
1167
 
@@ -1175,8 +1231,8 @@ async function testRollback() {
1175
1231
  w.on('change', function(mode, data, res) {
1176
1232
  console.log('change', mode)
1177
1233
  })
1178
- w.on('error', function(err) {
1179
- console.log('error', err)
1234
+ w.on('error', function(mode, data, err) {
1235
+ console.log('error', mode, err)
1180
1236
  })
1181
1237
 
1182
1238
 
@@ -1289,10 +1345,10 @@ testRollback()
1289
1345
  // insert then { n: 3, nInserted: 3, ok: 1 }
1290
1346
  // change save
1291
1347
  // save then [
1292
- // { n: 1, nModified: 1, ok: 1 },
1293
- // { n: 1, nModified: 1, ok: 1 },
1294
- // { n: 0, nModified: 0, ok: 1 } //autoInsert=false
1295
- // { n: 1, nInserted: 1, ok: 1 } //autoInsert=true
1348
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
1349
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
1350
+ // { n: 0, nInserted: 0, nModified: 0, ok: 1 } //autoInsert=false
1351
+ // { n: 1, nInserted: 1, nModified: 0, ok: 1 } //autoInsert=true
1296
1352
  // ]
1297
1353
  // change del
1298
1354
  // del then [
@@ -1323,7 +1379,7 @@ let opt = {
1323
1379
  cl: 'users',
1324
1380
  fdModels: './models',
1325
1381
  // modelType: 'json',
1326
- // autoGenPK: false,
1382
+ // autoGenPk: false,
1327
1383
  storage: './worm.sqlite',
1328
1384
  useEncryption: true,
1329
1385
  }
@@ -1401,8 +1457,8 @@ async function test() {
1401
1457
  w.on('change', function(mode, data, res) {
1402
1458
  console.log('change', mode)
1403
1459
  })
1404
- w.on('error', function(err) {
1405
- console.log('error', err)
1460
+ w.on('error', function(mode, data, err) {
1461
+ console.log('error', mode, err)
1406
1462
  })
1407
1463
 
1408
1464
 
@@ -1491,10 +1547,10 @@ test()
1491
1547
  // insert then { n: 3, nInserted: 3, ok: 1 }
1492
1548
  // change save
1493
1549
  // save then [
1494
- // { n: 1, nModified: 1, ok: 1 },
1495
- // { n: 1, nModified: 1, ok: 1 },
1496
- // { n: 0, nModified: 0, ok: 1 } //autoInsert=false
1497
- // { n: 1, nInserted: 1, ok: 1 } //autoInsert=true
1550
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
1551
+ // { n: 1, nInserted: 0, nModified: 1, ok: 1 },
1552
+ // { n: 0, nInserted: 0, nModified: 0, ok: 1 } //autoInsert=false
1553
+ // { n: 1, nInserted: 1, nModified: 0, ok: 1 } //autoInsert=true
1498
1554
  // ]
1499
1555
  // select all [
1500
1556
  // { id: 'id-peter', name: 'peter(modify)', value: 123 },
@@ -1538,7 +1594,7 @@ let opt = {
1538
1594
  cl: 'users',
1539
1595
  fdModels: './models',
1540
1596
  // modelType: 'json',
1541
- // autoGenPK: false,
1597
+ // autoGenPk: false,
1542
1598
  }
1543
1599
 
1544
1600
  let fd = opt.fdModels