inibase 3.2.0 → 3.4.0
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 +25 -1
- package/dist/file.d.ts +16 -0
- package/dist/file.js +151 -25
- package/dist/index.d.ts +15 -4
- package/dist/index.js +128 -50
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -206,7 +206,7 @@ interface {
|
|
|
206
206
|
|
|
207
207
|
```ts
|
|
208
208
|
interface Field {
|
|
209
|
-
id: number; //
|
|
209
|
+
id: number; // position in the schema; 0 = the row id, -1 = createdAt, -2 = updatedAt
|
|
210
210
|
key: string;
|
|
211
211
|
required?: boolean;
|
|
212
212
|
unique?: boolean | string; // boolean for simple uniqueness, string for grouped uniqueness
|
|
@@ -402,6 +402,30 @@ const userTableSchema = [
|
|
|
402
402
|
await db.createTable("user", userTableSchema, userTableConfig);
|
|
403
403
|
```
|
|
404
404
|
|
|
405
|
+
#### `decodeID`
|
|
406
|
+
|
|
407
|
+
By default a row's `id` is stored as a number but only ever handed back as an
|
|
408
|
+
encrypted (`aes-256-cbc`, hex) string, so ids can't be guessed by incrementing.
|
|
409
|
+
Set `decodeID: true` and the id is exposed as the **plain number** everywhere —
|
|
410
|
+
`get` rows, `get`/`put`/`delete` by id, relation lookups, and the id returned by
|
|
411
|
+
`post`:
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
const db = new Inibase("/databaseName");
|
|
415
|
+
await db.createTable("item", [{ key: "name", type: "string" }], {
|
|
416
|
+
decodeID: true,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const id = await db.post("item", { name: "widget" }); // => 1 (a number)
|
|
420
|
+
await db.get("item", 1); // works with the raw number
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
So `post` returns a `number` on a `decodeID` table and an encrypted `string` on
|
|
424
|
+
every other table — in TypeScript its return type is `string | number`
|
|
425
|
+
(`(string | number)[]` for a bulk post). Encrypted ids stay valid inputs on a
|
|
426
|
+
`decodeID` table too, so existing clients that round-trip an id keep working
|
|
427
|
+
either way. `decodeID` can be toggled later with `updateTable`.
|
|
428
|
+
|
|
405
429
|
</blockquote>
|
|
406
430
|
</details>
|
|
407
431
|
|
package/dist/file.d.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { type ComparisonOperator, type Field } from "./index.js";
|
|
2
2
|
export declare const DURABLE: boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Run `fn` with a per-logical-task lock-owner store on its AsyncLocalStorage
|
|
5
|
+
* context. Every await continuation below `fn` then shares that store, so
|
|
6
|
+
* nested/reentrant acquisitions (put/delete id->line recursion, journal
|
|
7
|
+
* recovery re-locks, transaction flows) see the SAME store the outermost
|
|
8
|
+
* acquisition recorded as owner.
|
|
9
|
+
*
|
|
10
|
+
* Do NOT replace this with an enterWith() from inside lock(): that store is
|
|
11
|
+
* created in lock()'s own frame and (a) never propagates back to the caller's
|
|
12
|
+
* await continuation — every recursion then waits on its OWN lock until the
|
|
13
|
+
* watchdog (LOCK_WAIT_TIMEOUT) — and (b) can leak into sibling tasks, which
|
|
14
|
+
* then pass the writer lock as "reentrant" and re-race the Sep-2026 commit
|
|
15
|
+
* interleave (ENOENT renames, duplicate ids). `run()` gives clean per-op
|
|
16
|
+
* isolation and correct same-chain propagation.
|
|
17
|
+
*/
|
|
18
|
+
export declare const runWithLockStore: <T>(fn: () => T) => T;
|
|
3
19
|
export declare const lock: (folderPath: string, prefix?: string, ttl?: number) => Promise<void>;
|
|
4
20
|
/**
|
|
5
21
|
* Non-blocking lock acquisition (used by the read path and the open-time
|
package/dist/file.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { access, appendFile, copyFile, constants as fsConstants, open, readFile, stat, unlink, } from "node:fs/promises";
|
|
2
3
|
import { hostname } from "node:os";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
@@ -31,6 +32,87 @@ const LOCK_RETRY_MS = 13;
|
|
|
31
32
|
// multiple times (e.g. post() -> get() -> sort-cache). Depth 1 is a real
|
|
32
33
|
// filesystem acquisition; deeper acquisitions just bump the counter.
|
|
33
34
|
const lockDepths = new Map();
|
|
35
|
+
// In-process serialization: sibling tasks (e.g. two concurrent HTTP requests
|
|
36
|
+
// posting to the same table) must QUEUE behind the holder instead of passing
|
|
37
|
+
// straight through the depth counter. That bypass is exactly what let
|
|
38
|
+
// concurrent writers share the fixed `.tmp/<column>` staging path and race the
|
|
39
|
+
// pagination rename (diverged column files, duplicate ids, lost rows in the
|
|
40
|
+
// Sep-2026 pteam incident). Reentrancy from the *same* logical task (post() ->
|
|
41
|
+
// get() -> ensureTableRecovered re-lock, transaction flows) still bumps the
|
|
42
|
+
// depth without touching the filesystem, proven by the AsyncLocalStorage
|
|
43
|
+
// context recorded at acquisition time.
|
|
44
|
+
const lockReleaseWaiters = new Map();
|
|
45
|
+
const lockOwners = new Map();
|
|
46
|
+
// Upper bound on in-process wait time: converts a wedged or undetectable
|
|
47
|
+
// holder into a visible error instead of a silent hang. 0 disables it.
|
|
48
|
+
const LOCK_WAIT_TIMEOUT_MS = Number(process.env.INIBASE_LOCK_WAIT_TIMEOUT_MS ?? 120_000);
|
|
49
|
+
const lockContext = new AsyncLocalStorage();
|
|
50
|
+
// Resolve once the holder releases so the waiter can re-race for the lock.
|
|
51
|
+
const waitForLockRelease = (resolvedPath) => new Promise((resolveWaiter, rejectWaiter) => {
|
|
52
|
+
let waiters = lockReleaseWaiters.get(resolvedPath);
|
|
53
|
+
if (!waiters) {
|
|
54
|
+
waiters = new Set();
|
|
55
|
+
lockReleaseWaiters.set(resolvedPath, waiters);
|
|
56
|
+
}
|
|
57
|
+
let timer = null;
|
|
58
|
+
const wake = (error) => {
|
|
59
|
+
waiters.delete(wake);
|
|
60
|
+
if (timer)
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
error ? rejectWaiter(error) : resolveWaiter();
|
|
63
|
+
};
|
|
64
|
+
waiters.add(wake);
|
|
65
|
+
if (LOCK_WAIT_TIMEOUT_MS > 0)
|
|
66
|
+
timer = setTimeout(() => wake(new Error(`LOCK_WAIT_TIMEOUT: ${resolvedPath}`)), LOCK_WAIT_TIMEOUT_MS);
|
|
67
|
+
});
|
|
68
|
+
// The AsyncLocalStorage store of this task chain (created on first acquire).
|
|
69
|
+
const currentLockStore = () => {
|
|
70
|
+
let store = lockContext.getStore();
|
|
71
|
+
if (!store) {
|
|
72
|
+
store = new Set();
|
|
73
|
+
lockContext.enterWith(store);
|
|
74
|
+
}
|
|
75
|
+
return store;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Run `fn` with a per-logical-task lock-owner store on its AsyncLocalStorage
|
|
79
|
+
* context. Every await continuation below `fn` then shares that store, so
|
|
80
|
+
* nested/reentrant acquisitions (put/delete id->line recursion, journal
|
|
81
|
+
* recovery re-locks, transaction flows) see the SAME store the outermost
|
|
82
|
+
* acquisition recorded as owner.
|
|
83
|
+
*
|
|
84
|
+
* Do NOT replace this with an enterWith() from inside lock(): that store is
|
|
85
|
+
* created in lock()'s own frame and (a) never propagates back to the caller's
|
|
86
|
+
* await continuation — every recursion then waits on its OWN lock until the
|
|
87
|
+
* watchdog (LOCK_WAIT_TIMEOUT) — and (b) can leak into sibling tasks, which
|
|
88
|
+
* then pass the writer lock as "reentrant" and re-race the Sep-2026 commit
|
|
89
|
+
* interleave (ENOENT renames, duplicate ids). `run()` gives clean per-op
|
|
90
|
+
* isolation and correct same-chain propagation.
|
|
91
|
+
*/
|
|
92
|
+
export const runWithLockStore = (fn) => {
|
|
93
|
+
if (lockContext.getStore())
|
|
94
|
+
return fn();
|
|
95
|
+
const store = new Set();
|
|
96
|
+
return lockContext.run(store, fn);
|
|
97
|
+
};
|
|
98
|
+
// True when this task already owns the lock (depth > 0 and the recorded owner
|
|
99
|
+
// context is this task's context). Bumps the depth and returns.
|
|
100
|
+
const isReentrant = (resolvedPath) => {
|
|
101
|
+
if (!lockDepths.get(resolvedPath))
|
|
102
|
+
return false;
|
|
103
|
+
if (lockOwners.get(resolvedPath) !== lockContext.getStore())
|
|
104
|
+
return false;
|
|
105
|
+
lockDepths.set(resolvedPath, lockDepths.get(resolvedPath) + 1);
|
|
106
|
+
return true;
|
|
107
|
+
};
|
|
108
|
+
const notifyLockReleased = (resolvedPath) => {
|
|
109
|
+
const waiters = lockReleaseWaiters.get(resolvedPath);
|
|
110
|
+
if (!waiters)
|
|
111
|
+
return;
|
|
112
|
+
lockReleaseWaiters.delete(resolvedPath);
|
|
113
|
+
for (const wake of waiters)
|
|
114
|
+
wake();
|
|
115
|
+
};
|
|
34
116
|
const lockFilePathFor = (folderPath, prefix) => join(folderPath, `${prefix ?? ""}.locked`);
|
|
35
117
|
const lockOwnerState = async (lockFilePath) => {
|
|
36
118
|
try {
|
|
@@ -67,12 +149,16 @@ const stealableLock = async (lockFilePath, mtimeMs, ttl) => {
|
|
|
67
149
|
export const lock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) => {
|
|
68
150
|
const lockFilePath = lockFilePathFor(folderPath, prefix);
|
|
69
151
|
const resolvedPath = resolve(lockFilePath);
|
|
70
|
-
const depth = lockDepths.get(resolvedPath);
|
|
71
|
-
if (depth) {
|
|
72
|
-
lockDepths.set(resolvedPath, depth + 1);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
152
|
for (;;) {
|
|
153
|
+
// Same logical task already holds it -> reentrant depth bump.
|
|
154
|
+
if (isReentrant(resolvedPath))
|
|
155
|
+
return;
|
|
156
|
+
// A different in-process task holds it -> wait for the release, then
|
|
157
|
+
// re-race for the filesystem lock like any process.
|
|
158
|
+
if (lockDepths.get(resolvedPath)) {
|
|
159
|
+
await waitForLockRelease(resolvedPath);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
76
162
|
try {
|
|
77
163
|
const lockFile = await open(lockFilePath, "wx");
|
|
78
164
|
try {
|
|
@@ -100,6 +186,7 @@ export const lock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) => {
|
|
|
100
186
|
}
|
|
101
187
|
}
|
|
102
188
|
lockDepths.set(resolvedPath, 1);
|
|
189
|
+
lockOwners.set(resolvedPath, currentLockStore());
|
|
103
190
|
return;
|
|
104
191
|
}
|
|
105
192
|
catch (error) {
|
|
@@ -129,11 +216,10 @@ export const lock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) => {
|
|
|
129
216
|
export const tryLock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) => {
|
|
130
217
|
const lockFilePath = lockFilePathFor(folderPath, prefix);
|
|
131
218
|
const resolvedPath = resolve(lockFilePath);
|
|
132
|
-
|
|
133
|
-
if (depth) {
|
|
134
|
-
lockDepths.set(resolvedPath, depth + 1);
|
|
219
|
+
if (isReentrant(resolvedPath))
|
|
135
220
|
return true;
|
|
136
|
-
|
|
221
|
+
if (lockDepths.get(resolvedPath))
|
|
222
|
+
return false; // held by another in-process task: non-blocking
|
|
137
223
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
138
224
|
try {
|
|
139
225
|
const lockFile = await open(lockFilePath, "wx");
|
|
@@ -157,6 +243,7 @@ export const tryLock = async (folderPath, prefix, ttl = DEFAULT_LOCK_TTL_MS) =>
|
|
|
157
243
|
}
|
|
158
244
|
}
|
|
159
245
|
lockDepths.set(resolvedPath, 1);
|
|
246
|
+
lockOwners.set(resolvedPath, currentLockStore());
|
|
160
247
|
return true;
|
|
161
248
|
}
|
|
162
249
|
catch (error) {
|
|
@@ -183,12 +270,15 @@ export const unlock = async (folderPath, prefix) => {
|
|
|
183
270
|
return;
|
|
184
271
|
}
|
|
185
272
|
lockDepths.delete(resolvedPath);
|
|
273
|
+
lockOwners.delete(resolvedPath);
|
|
186
274
|
try {
|
|
187
275
|
await unlink(lockFilePath);
|
|
188
276
|
}
|
|
189
277
|
catch {
|
|
190
278
|
// Already released (stolen by a foreign-host stealer or cleaned up).
|
|
191
279
|
}
|
|
280
|
+
// Wake in-process waiters so they re-race for the released lock.
|
|
281
|
+
notifyLockReleased(resolvedPath);
|
|
192
282
|
};
|
|
193
283
|
export const write = async (filePath, data) => {
|
|
194
284
|
const handle = await open(filePath, "w");
|
|
@@ -242,6 +332,17 @@ export function escapeShellPath(filePath) {
|
|
|
242
332
|
// Escape double quotes and special shell characters
|
|
243
333
|
return `"${resolvedPath.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
244
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Unique-per-operation staging path: concurrent writers must never share the
|
|
337
|
+
* `.tmp/<column>` file. The fixed name let sibling operations clobber each
|
|
338
|
+
* other's in-progress temp and publish a temp that only contained one writer's
|
|
339
|
+
* content (column files following different winners -> diverged line counts).
|
|
340
|
+
* A pid + per-process counter suffix keeps every temp private; each pair is
|
|
341
|
+
* carried through the rename list and the journal, so commit/rollback/recovery
|
|
342
|
+
* still know exactly which files to move.
|
|
343
|
+
*/
|
|
344
|
+
let tempSeq = 0;
|
|
345
|
+
const tempPathFor = (filePath) => filePath.replace(/([^/]+)\/?$/, `.tmp/$1.${process.pid}-${(tempSeq++).toString(36)}`);
|
|
245
346
|
const _pipeline = async (filePath, rl, writeStream, transform) => {
|
|
246
347
|
if (filePath.endsWith(".gz"))
|
|
247
348
|
await pipeline(rl, transform, createGzip(), writeStream);
|
|
@@ -505,7 +606,7 @@ export async function get(filePath, lineNumbers, field, readWholeFile = false) {
|
|
|
505
606
|
* Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
|
|
506
607
|
*/
|
|
507
608
|
export const replace = async (filePath, replacements, totalItems) => {
|
|
508
|
-
const fileTempPath = filePath
|
|
609
|
+
const fileTempPath = tempPathFor(filePath);
|
|
509
610
|
const isReplacementsObject = isObject(replacements);
|
|
510
611
|
const isReplacementsLineNumbered = isReplacementsObject && !Number.isNaN(Number(Object.keys(replacements)[0]));
|
|
511
612
|
if (await isExists(filePath)) {
|
|
@@ -581,19 +682,33 @@ export const replace = async (filePath, replacements, totalItems) => {
|
|
|
581
682
|
const replacementsKeys = Object.keys(replacements)
|
|
582
683
|
.map(Number)
|
|
583
684
|
.toSorted((a, b) => a - b);
|
|
584
|
-
await write(fileTempPath,
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
685
|
+
await write(fileTempPath,
|
|
686
|
+
// First-write for a compressed column: see append() — the
|
|
687
|
+
// unique temp path hides the ".gz" suffix from write().
|
|
688
|
+
filePath.endsWith(".gz")
|
|
689
|
+
? await gzip(`${"\n".repeat(replacementsKeys[0] - 1) +
|
|
690
|
+
replacementsKeys
|
|
691
|
+
.map((lineNumber, index) => index === 0 ||
|
|
692
|
+
lineNumber - replacementsKeys[index - 1] - 1 === 0
|
|
693
|
+
? replacements[lineNumber]
|
|
694
|
+
: "\n".repeat(lineNumber - replacementsKeys[index - 1] - 1) + replacements[lineNumber])
|
|
695
|
+
.join("\n")}\n`)
|
|
696
|
+
: `${"\n".repeat(replacementsKeys[0] - 1) +
|
|
697
|
+
replacementsKeys
|
|
698
|
+
.map((lineNumber, index) => index === 0 ||
|
|
699
|
+
lineNumber - replacementsKeys[index - 1] - 1 === 0
|
|
700
|
+
? replacements[lineNumber]
|
|
701
|
+
: "\n".repeat(lineNumber - replacementsKeys[index - 1] - 1) + replacements[lineNumber])
|
|
702
|
+
.join("\n")}\n`);
|
|
592
703
|
}
|
|
593
704
|
else {
|
|
594
705
|
if (!totalItems)
|
|
595
706
|
throw new Error("INVALID_PARAMETERS");
|
|
596
|
-
await write(fileTempPath,
|
|
707
|
+
await write(fileTempPath,
|
|
708
|
+
// First-write for a compressed column: see append().
|
|
709
|
+
filePath.endsWith(".gz")
|
|
710
|
+
? await gzip(`${`${replacements}\n`.repeat(totalItems)}\n`)
|
|
711
|
+
: `${`${replacements}\n`.repeat(totalItems)}\n`);
|
|
597
712
|
}
|
|
598
713
|
return [fileTempPath, filePath];
|
|
599
714
|
}
|
|
@@ -612,7 +727,7 @@ export const replace = async (filePath, replacements, totalItems) => {
|
|
|
612
727
|
*
|
|
613
728
|
*/
|
|
614
729
|
export const append = async (filePath, data) => {
|
|
615
|
-
const fileTempPath = filePath
|
|
730
|
+
const fileTempPath = tempPathFor(filePath);
|
|
616
731
|
try {
|
|
617
732
|
if (await isExists(filePath)) {
|
|
618
733
|
await copyFile(filePath, fileTempPath);
|
|
@@ -627,7 +742,13 @@ export const append = async (filePath, data) => {
|
|
|
627
742
|
}
|
|
628
743
|
}
|
|
629
744
|
else
|
|
630
|
-
await write(fileTempPath,
|
|
745
|
+
await write(fileTempPath,
|
|
746
|
+
// Unique temp paths no longer end with the original extension
|
|
747
|
+
// (.tmp/<col>.<pid>-<seq>), so write()'s suffix check can't
|
|
748
|
+
// gzip a first-write for a compressed column. Do it here.
|
|
749
|
+
filePath.endsWith(".gz")
|
|
750
|
+
? await gzip(`${Array.isArray(data) ? data.join("\n") : data}\n`)
|
|
751
|
+
: `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
631
752
|
return [fileTempPath, filePath];
|
|
632
753
|
}
|
|
633
754
|
catch {
|
|
@@ -643,7 +764,7 @@ export const append = async (filePath, data) => {
|
|
|
643
764
|
*
|
|
644
765
|
*/
|
|
645
766
|
export const prepend = async (filePath, data) => {
|
|
646
|
-
const fileTempPath = filePath
|
|
767
|
+
const fileTempPath = tempPathFor(filePath);
|
|
647
768
|
if (await isExists(filePath)) {
|
|
648
769
|
if (!filePath.endsWith(".gz")) {
|
|
649
770
|
let fileHandle = null;
|
|
@@ -673,7 +794,7 @@ export const prepend = async (filePath, data) => {
|
|
|
673
794
|
}
|
|
674
795
|
}
|
|
675
796
|
else {
|
|
676
|
-
const fileChildTempPath = filePath
|
|
797
|
+
const fileChildTempPath = `${tempPathFor(filePath)}.child`;
|
|
677
798
|
try {
|
|
678
799
|
await write(fileChildTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
679
800
|
const escapedFilePath = escapeShellPath(filePath);
|
|
@@ -691,7 +812,12 @@ export const prepend = async (filePath, data) => {
|
|
|
691
812
|
}
|
|
692
813
|
else {
|
|
693
814
|
try {
|
|
694
|
-
await write(fileTempPath,
|
|
815
|
+
await write(fileTempPath,
|
|
816
|
+
// First-write for a compressed column: see append() — the
|
|
817
|
+
// unique temp path hides the ".gz" suffix from write().
|
|
818
|
+
filePath.endsWith(".gz")
|
|
819
|
+
? await gzip(`${Array.isArray(data) ? data.join("\n") : data}\n`)
|
|
820
|
+
: `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
695
821
|
}
|
|
696
822
|
catch {
|
|
697
823
|
return [fileTempPath, null];
|
|
@@ -714,7 +840,7 @@ export const remove = async (filePath, linesToDelete) => {
|
|
|
714
840
|
: [Number(linesToDelete)];
|
|
715
841
|
if (linesToDelete.some(Number.isNaN))
|
|
716
842
|
throw new Error("UNVALID_LINE_NUMBERS");
|
|
717
|
-
const fileTempPath = filePath
|
|
843
|
+
const fileTempPath = tempPathFor(filePath);
|
|
718
844
|
try {
|
|
719
845
|
const escapedFilePath = escapeShellPath(filePath);
|
|
720
846
|
const escapedFileTempPath = escapeShellPath(fileTempPath);
|
package/dist/index.d.ts
CHANGED
|
@@ -155,6 +155,7 @@ export default class Inibase {
|
|
|
155
155
|
* @param {TableConfig} [config]
|
|
156
156
|
*/
|
|
157
157
|
createTable(tableName: string, schema?: Schema, config?: TableConfig): Promise<void>;
|
|
158
|
+
private _createTable;
|
|
158
159
|
private replaceStringInFile;
|
|
159
160
|
/**
|
|
160
161
|
* Update table schema or config
|
|
@@ -166,6 +167,7 @@ export default class Inibase {
|
|
|
166
167
|
updateTable(tableName: string, schema?: Schema, config?: TableConfig & {
|
|
167
168
|
name?: string;
|
|
168
169
|
}): Promise<void>;
|
|
170
|
+
private _updateTable;
|
|
169
171
|
private updateTableLocked;
|
|
170
172
|
/**
|
|
171
173
|
* Get table schema and config
|
|
@@ -333,6 +335,7 @@ export default class Inibase {
|
|
|
333
335
|
* locked on first touch, in first-touch order.
|
|
334
336
|
*/
|
|
335
337
|
begin(tables?: string[]): Promise<void>;
|
|
338
|
+
private _begin;
|
|
336
339
|
/**
|
|
337
340
|
* Publish every staged mutation atomically: per table (sorted), the
|
|
338
341
|
* pagination rename comes first (the atomic publication point readers
|
|
@@ -371,6 +374,7 @@ export default class Inibase {
|
|
|
371
374
|
get<TData extends Record<string, any> & Partial<Data>>(tableName: string, where?: string | number | (string | number)[] | Criteria, options?: Options, onlyOne?: boolean, onlyLinesNumbers?: false, _whereIsLinesNumbers?: boolean): Promise<(Data & TData)[] | null>;
|
|
372
375
|
get<_TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: false | undefined, onlyLinesNumbers: true, _whereIsLinesNumbers?: boolean): Promise<number[] | null>;
|
|
373
376
|
get<_TData extends Record<string, any> & Partial<Data>>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: true, onlyLinesNumbers: true, _whereIsLinesNumbers?: boolean): Promise<number | null>;
|
|
377
|
+
private _get;
|
|
374
378
|
private getOnce;
|
|
375
379
|
/**
|
|
376
380
|
* Create new item(s) in a table
|
|
@@ -380,11 +384,16 @@ export default class Inibase {
|
|
|
380
384
|
* @param {Options} [options] Pagination options, useful when the returnPostedData param is true
|
|
381
385
|
* @param {boolean} [returnPostedData] By default function returns void, if you want to get the posted data, set this param to true
|
|
382
386
|
* @return {*} {Promise<Data | Data[] | null | void>}
|
|
387
|
+
*
|
|
388
|
+
* @remarks Without `returnPostedData` the return value is the new id: a
|
|
389
|
+
* `number` on a table created with `decodeID: true`, an encrypted
|
|
390
|
+
* `string` on every other table.
|
|
383
391
|
*/
|
|
384
|
-
post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: Data & TData, options?: Options, returnPostedData?: boolean): Promise<string>;
|
|
385
|
-
post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], options?: Options, returnPostedData?: boolean): Promise<string[]>;
|
|
392
|
+
post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: Data & TData, options?: Options, returnPostedData?: boolean): Promise<string | number>;
|
|
393
|
+
post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], options?: Options, returnPostedData?: boolean): Promise<(string | number)[]>;
|
|
386
394
|
post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: Data & TData, options: Options | undefined, returnPostedData: true): Promise<(Data & TData) | null>;
|
|
387
395
|
post<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], options: Options | undefined, returnPostedData: true): Promise<(Data & TData)[] | null>;
|
|
396
|
+
private _post;
|
|
388
397
|
/**
|
|
389
398
|
* Update item(s) in a table
|
|
390
399
|
*
|
|
@@ -399,6 +408,7 @@ export default class Inibase {
|
|
|
399
408
|
put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: Data & TData, where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | null>;
|
|
400
409
|
put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData)[], where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData)[] | null>;
|
|
401
410
|
put<TData extends Record<string, any> & Partial<Data>>(tableName: string, data: (Data & TData) | (Data & TData)[], where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | (Data & TData)[] | null>;
|
|
411
|
+
private _put;
|
|
402
412
|
/**
|
|
403
413
|
* Delete item(s) in a table
|
|
404
414
|
*
|
|
@@ -407,6 +417,7 @@ export default class Inibase {
|
|
|
407
417
|
* @return {boolean | null} {(Promise<boolean | null>)}
|
|
408
418
|
*/
|
|
409
419
|
delete(tableName: string, where?: number | string | (number | string)[] | Criteria, _whereIsLinesNumbers?: boolean, _cascadeGuard?: Set<string>): Promise<boolean | null>;
|
|
420
|
+
private _delete;
|
|
410
421
|
/**
|
|
411
422
|
* Cascade delete: remove rows in other tables whose `table`-typed schema
|
|
412
423
|
* fields reference the given rows. Reference columns store the numeric
|
|
@@ -424,8 +435,8 @@ export default class Inibase {
|
|
|
424
435
|
* @param {(number | string | (number | string)[] | Criteria)} [where]
|
|
425
436
|
* @return {*} {Promise<number | Record<string, number>>}
|
|
426
437
|
*/
|
|
427
|
-
sum(tableName: string, columns: string, where?: number | string | (number | string)[] | Criteria): Promise<number>;
|
|
428
|
-
sum(tableName: string, columns: string[], where?: number | string | (number | string)[] | Criteria): Promise<Record<string, number>>;
|
|
438
|
+
sum(tableName: string, columns: string, where?: number | string | (number | string)[] | Criteria, options?: AggregateOptions): Promise<number>;
|
|
439
|
+
sum(tableName: string, columns: string[], where?: number | string | (number | string)[] | Criteria, options?: AggregateOptions): Promise<Record<string, number>>;
|
|
429
440
|
/**
|
|
430
441
|
* Element-wise aggregation over an array-of-objects column (`nested` mode).
|
|
431
442
|
*
|
package/dist/index.js
CHANGED
|
@@ -220,6 +220,9 @@ export default class Inibase {
|
|
|
220
220
|
* @param {TableConfig} [config]
|
|
221
221
|
*/
|
|
222
222
|
async createTable(tableName, schema, config) {
|
|
223
|
+
return File.runWithLockStore(() => this._createTable(tableName, schema, config));
|
|
224
|
+
}
|
|
225
|
+
async _createTable(tableName, schema, config) {
|
|
223
226
|
this.validateName(tableName);
|
|
224
227
|
// DDL does not participate in the write-ahead journal: schema surgery
|
|
225
228
|
// inside a transaction would escape the atomic publish/rollback scope.
|
|
@@ -291,6 +294,9 @@ export default class Inibase {
|
|
|
291
294
|
* @param {(TableConfig&{name?: string})} [config]
|
|
292
295
|
*/
|
|
293
296
|
async updateTable(tableName, schema, config) {
|
|
297
|
+
return File.runWithLockStore(() => this._updateTable(tableName, schema, config));
|
|
298
|
+
}
|
|
299
|
+
async _updateTable(tableName, schema, config) {
|
|
294
300
|
this.validateName(tableName);
|
|
295
301
|
// DDL does not participate in the write-ahead journal: schema surgery
|
|
296
302
|
// inside a transaction would escape the atomic publish/rollback scope.
|
|
@@ -2308,6 +2314,9 @@ export default class Inibase {
|
|
|
2308
2314
|
* locked on first touch, in first-touch order.
|
|
2309
2315
|
*/
|
|
2310
2316
|
async begin(tables = []) {
|
|
2317
|
+
return File.runWithLockStore(() => this._begin(tables));
|
|
2318
|
+
}
|
|
2319
|
+
async _begin(tables = []) {
|
|
2311
2320
|
if (this.transaction)
|
|
2312
2321
|
throw this.createError("INVALID_PARAMETERS");
|
|
2313
2322
|
await this.ensureDatabaseTmpDir();
|
|
@@ -2459,6 +2468,12 @@ export default class Inibase {
|
|
|
2459
2468
|
async get(tableName, where, options = {
|
|
2460
2469
|
page: 1,
|
|
2461
2470
|
perPage: 15,
|
|
2471
|
+
}, onlyOne, onlyLinesNumbers, _whereIsLinesNumbers) {
|
|
2472
|
+
return File.runWithLockStore(() => this._get(tableName, where, options, onlyOne, onlyLinesNumbers, _whereIsLinesNumbers));
|
|
2473
|
+
}
|
|
2474
|
+
async _get(tableName, where, options = {
|
|
2475
|
+
page: 1,
|
|
2476
|
+
perPage: 15,
|
|
2462
2477
|
}, onlyOne, onlyLinesNumbers, _whereIsLinesNumbers) {
|
|
2463
2478
|
this.validateName(tableName);
|
|
2464
2479
|
await this.ensureTableRecovered(tableName);
|
|
@@ -2631,9 +2646,13 @@ export default class Inibase {
|
|
|
2631
2646
|
}
|
|
2632
2647
|
else if (((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
2633
2648
|
Utils.isNumber(where)) &&
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2649
|
+
// Numeric where is a line number only through the internal
|
|
2650
|
+
// recursion (flag set). An external numeric where is an id — even
|
|
2651
|
+
// on non-decodeID tables, where ids are NOT line numbers (prepend
|
|
2652
|
+
// layout / rows deleted); using them as lines edited the wrong row
|
|
2653
|
+
// and padded the column file (Sep-2026 rename bug). Ids are
|
|
2654
|
+
// resolved by the next branch.
|
|
2655
|
+
_whereIsLinesNumbers) {
|
|
2637
2656
|
// "where" in this case, is the line(s) number(s) and not id(s)
|
|
2638
2657
|
let lineNumbers = where;
|
|
2639
2658
|
if (!Array.isArray(lineNumbers))
|
|
@@ -2647,8 +2666,6 @@ export default class Inibase {
|
|
|
2647
2666
|
RETURN = RETURN[0];
|
|
2648
2667
|
}
|
|
2649
2668
|
else if ((!_whereIsLinesNumbers &&
|
|
2650
|
-
globalConfig[this.databasePath].tables?.get(tableName)?.config
|
|
2651
|
-
.decodeID &&
|
|
2652
2669
|
((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
2653
2670
|
Utils.isNumber(where))) ||
|
|
2654
2671
|
(Array.isArray(where) && where.every(Utils.isValidID)) ||
|
|
@@ -2774,6 +2791,9 @@ export default class Inibase {
|
|
|
2774
2791
|
return onlyOne && Array.isArray(RETURN) ? RETURN[0] : RETURN;
|
|
2775
2792
|
}
|
|
2776
2793
|
async post(tableName, data, options, returnPostedData) {
|
|
2794
|
+
return File.runWithLockStore(() => this._post(tableName, data, options, returnPostedData));
|
|
2795
|
+
}
|
|
2796
|
+
async _post(tableName, data, options, returnPostedData) {
|
|
2777
2797
|
if (!options)
|
|
2778
2798
|
options = {
|
|
2779
2799
|
page: 1,
|
|
@@ -2871,7 +2891,7 @@ export default class Inibase {
|
|
|
2871
2891
|
// No read-your-writes yet: return the formatted staged rows
|
|
2872
2892
|
// (ids + defaults) instead of a committed-state read.
|
|
2873
2893
|
return (Array.isArray(clonedData) ? clonedData : clonedData);
|
|
2874
|
-
return this.get(tableName, globalConfig[this.databasePath].tables?.get(tableName)?.config.prepend
|
|
2894
|
+
return await this.get(tableName, globalConfig[this.databasePath].tables?.get(tableName)?.config.prepend
|
|
2875
2895
|
? Array.isArray(clonedData)
|
|
2876
2896
|
? clonedData.map((_, index) => index + 1).toReversed()
|
|
2877
2897
|
: 1
|
|
@@ -2882,12 +2902,18 @@ export default class Inibase {
|
|
|
2882
2902
|
: this.totalItems.get(`${tableName}-*`), options, !Utils.isArrayOfObjects(clonedData), // return only one item if data is not array of objects
|
|
2883
2903
|
undefined, true);
|
|
2884
2904
|
}
|
|
2905
|
+
// On a decodeID table the id *is* the plain number — that is the
|
|
2906
|
+
// whole point of the config: get(), put(), delete() and the
|
|
2907
|
+
// returnPostedData read all hand back raw numbers, so post() must
|
|
2908
|
+
// not encode it here. Every other table keeps returning the
|
|
2909
|
+
// encrypted id.
|
|
2910
|
+
const tableConfig = globalConfig[this.databasePath].tables?.get(tableName)?.config;
|
|
2911
|
+
const postedID = (id) => tableConfig?.decodeID === true
|
|
2912
|
+
? Number(id)
|
|
2913
|
+
: UtilsServer.encodeID(id);
|
|
2885
2914
|
return Array.isArray(clonedData)
|
|
2886
|
-
? (
|
|
2887
|
-
|
|
2888
|
-
? clonedData.toReversed()
|
|
2889
|
-
: clonedData).map(({ id }) => UtilsServer.encodeID(id))
|
|
2890
|
-
: UtilsServer.encodeID(clonedData.id);
|
|
2915
|
+
? (tableConfig?.prepend ? clonedData.toReversed() : clonedData).map(({ id }) => postedID(id))
|
|
2916
|
+
: postedID(clonedData.id);
|
|
2891
2917
|
}
|
|
2892
2918
|
finally {
|
|
2893
2919
|
if (this.transaction) {
|
|
@@ -2896,13 +2922,13 @@ export default class Inibase {
|
|
|
2896
2922
|
// here so nothing leaks.
|
|
2897
2923
|
if (!txnStaged && renameList.length)
|
|
2898
2924
|
await Promise.allSettled(renameList
|
|
2899
|
-
.filter((pair) => Boolean(pair[
|
|
2925
|
+
.filter((pair) => Boolean(pair[0]))
|
|
2900
2926
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
2901
2927
|
}
|
|
2902
2928
|
else {
|
|
2903
2929
|
if (renameList.length)
|
|
2904
2930
|
await Promise.allSettled(renameList
|
|
2905
|
-
.filter((pair) => Boolean(pair[
|
|
2931
|
+
.filter((pair) => Boolean(pair[0]))
|
|
2906
2932
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
2907
2933
|
await File.unlock(join(tablePath, ".tmp"));
|
|
2908
2934
|
}
|
|
@@ -2911,6 +2937,12 @@ export default class Inibase {
|
|
|
2911
2937
|
async put(tableName, data, where, options = {
|
|
2912
2938
|
page: 1,
|
|
2913
2939
|
perPage: 15,
|
|
2940
|
+
}, returnUpdatedData, _whereIsLinesNumbers) {
|
|
2941
|
+
return File.runWithLockStore(() => this._put(tableName, data, where, options, returnUpdatedData, _whereIsLinesNumbers));
|
|
2942
|
+
}
|
|
2943
|
+
async _put(tableName, data, where, options = {
|
|
2944
|
+
page: 1,
|
|
2945
|
+
perPage: 15,
|
|
2914
2946
|
}, returnUpdatedData, _whereIsLinesNumbers) {
|
|
2915
2947
|
const renameList = [];
|
|
2916
2948
|
let txnStaged = false;
|
|
@@ -3001,13 +3033,13 @@ export default class Inibase {
|
|
|
3001
3033
|
// owns them.
|
|
3002
3034
|
if (!txnStaged && renameList.length)
|
|
3003
3035
|
await Promise.allSettled(renameList
|
|
3004
|
-
.filter((pair) => Boolean(pair[
|
|
3036
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3005
3037
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3006
3038
|
}
|
|
3007
3039
|
else {
|
|
3008
3040
|
if (renameList.length)
|
|
3009
3041
|
await Promise.allSettled(renameList
|
|
3010
|
-
.filter((pair) => Boolean(pair[
|
|
3042
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3011
3043
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3012
3044
|
await File.unlock(join(tablePath, ".tmp"));
|
|
3013
3045
|
}
|
|
@@ -3015,9 +3047,13 @@ export default class Inibase {
|
|
|
3015
3047
|
}
|
|
3016
3048
|
else if (((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
3017
3049
|
Utils.isNumber(where)) &&
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3050
|
+
// Numeric where is a line number only through the internal
|
|
3051
|
+
// recursion (flag set). An external numeric where is an id — even
|
|
3052
|
+
// on non-decodeID tables, where ids are NOT line numbers (prepend
|
|
3053
|
+
// layout / rows deleted); using them as lines edited the wrong row
|
|
3054
|
+
// and padded the column file (Sep-2026 rename bug). Ids are
|
|
3055
|
+
// resolved by the next branch.
|
|
3056
|
+
_whereIsLinesNumbers) {
|
|
3021
3057
|
// "where" in this case, is the line(s) number(s) and not id(s)
|
|
3022
3058
|
await this.validateTableData(tableName, clonedData, true);
|
|
3023
3059
|
clonedData = this.formatData(clonedData, globalConfig[this.databasePath].tables?.get(tableName)?.schema ?? [], true);
|
|
@@ -3080,42 +3116,63 @@ export default class Inibase {
|
|
|
3080
3116
|
if (returnUpdatedData) {
|
|
3081
3117
|
if (this.transaction)
|
|
3082
3118
|
throw this.createError("INVALID_PARAMETERS");
|
|
3083
|
-
|
|
3119
|
+
// AWAITED readback: must complete inside the writer lock. An
|
|
3120
|
+
// async-tail `return this.get(...)` lets the finally below
|
|
3121
|
+
// unlock before the readback's file reads resume.
|
|
3122
|
+
return await this.get(tableName, where, options, !Array.isArray(where), undefined, true);
|
|
3084
3123
|
}
|
|
3085
3124
|
}
|
|
3086
3125
|
finally {
|
|
3087
3126
|
if (this.transaction) {
|
|
3088
3127
|
if (!txnStaged && renameList.length)
|
|
3089
3128
|
await Promise.allSettled(renameList
|
|
3090
|
-
.filter((pair) => Boolean(pair[
|
|
3129
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3091
3130
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3092
3131
|
}
|
|
3093
3132
|
else {
|
|
3094
3133
|
if (renameList.length)
|
|
3095
3134
|
await Promise.allSettled(renameList
|
|
3096
|
-
.filter((pair) => Boolean(pair[
|
|
3135
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3097
3136
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3098
3137
|
await File.unlock(join(tablePath, ".tmp"));
|
|
3099
3138
|
}
|
|
3100
3139
|
}
|
|
3101
3140
|
}
|
|
3102
3141
|
else if ((!_whereIsLinesNumbers &&
|
|
3103
|
-
globalConfig[this.databasePath].tables?.get(tableName)?.config
|
|
3104
|
-
.decodeID &&
|
|
3105
3142
|
((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
3106
3143
|
Utils.isNumber(where))) ||
|
|
3107
3144
|
(Array.isArray(where) && where.every(Utils.isValidID)) ||
|
|
3108
3145
|
Utils.isValidID(where)) {
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3146
|
+
// Resolve id -> line(s) WHILE holding the writer lock: a post
|
|
3147
|
+
// landing between the resolution and the write would shift every
|
|
3148
|
+
// line of a prepend table and the update would hit the wrong row.
|
|
3149
|
+
// The recursive call re-takes the lock reentrantly (same task), so
|
|
3150
|
+
// the lock is held continuously from resolution to publish.
|
|
3151
|
+
let inlineLock = false;
|
|
3152
|
+
try {
|
|
3153
|
+
if (this.transaction)
|
|
3154
|
+
await this.ensureTxnLock(tableName);
|
|
3155
|
+
else {
|
|
3156
|
+
await File.lock(join(tablePath, ".tmp"));
|
|
3157
|
+
inlineLock = true;
|
|
3158
|
+
}
|
|
3159
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
3160
|
+
if (lineNumbers)
|
|
3161
|
+
return await this.put(tableName, clonedData,
|
|
3162
|
+
// get() with onlyLinesNumbers always returns an array; a
|
|
3163
|
+
// single-id update must keep the scalar so the recursive
|
|
3164
|
+
// line-numbers branch returns a single row (matching the
|
|
3165
|
+
// shape of get(singleId)) instead of a one-element array.
|
|
3166
|
+
!Array.isArray(where) && Array.isArray(lineNumbers)
|
|
3167
|
+
? lineNumbers[0]
|
|
3168
|
+
: lineNumbers, options, returnUpdatedData, true);
|
|
3169
|
+
}
|
|
3170
|
+
finally {
|
|
3171
|
+
// The recursion released its own (reentrant) acquisition;
|
|
3172
|
+
// release ours too (transactions keep the lock until commit).
|
|
3173
|
+
if (inlineLock)
|
|
3174
|
+
await File.unlock(join(tablePath, ".tmp"));
|
|
3175
|
+
}
|
|
3119
3176
|
}
|
|
3120
3177
|
else if (Utils.isObject(where)) {
|
|
3121
3178
|
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
@@ -3133,6 +3190,9 @@ export default class Inibase {
|
|
|
3133
3190
|
* @return {boolean | null} {(Promise<boolean | null>)}
|
|
3134
3191
|
*/
|
|
3135
3192
|
async delete(tableName, where, _whereIsLinesNumbers, _cascadeGuard) {
|
|
3193
|
+
return File.runWithLockStore(() => this._delete(tableName, where, _whereIsLinesNumbers, _cascadeGuard));
|
|
3194
|
+
}
|
|
3195
|
+
async _delete(tableName, where, _whereIsLinesNumbers, _cascadeGuard) {
|
|
3136
3196
|
this.validateName(tableName);
|
|
3137
3197
|
if (!this.transaction)
|
|
3138
3198
|
await this.ensureDatabaseRecovered();
|
|
@@ -3181,13 +3241,13 @@ export default class Inibase {
|
|
|
3181
3241
|
if (this.transaction) {
|
|
3182
3242
|
if (!txnStaged && renameList.length)
|
|
3183
3243
|
await Promise.allSettled(renameList
|
|
3184
|
-
.filter((pair) => Boolean(pair[
|
|
3244
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3185
3245
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3186
3246
|
}
|
|
3187
3247
|
else {
|
|
3188
3248
|
if (renameList.length)
|
|
3189
3249
|
await Promise.allSettled(renameList
|
|
3190
|
-
.filter((pair) => Boolean(pair[
|
|
3250
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3191
3251
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3192
3252
|
await File.unlock(join(tablePath, ".tmp"));
|
|
3193
3253
|
}
|
|
@@ -3195,9 +3255,13 @@ export default class Inibase {
|
|
|
3195
3255
|
}
|
|
3196
3256
|
if (((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
3197
3257
|
Utils.isNumber(where)) &&
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3258
|
+
// Numeric where is a line number only through the internal
|
|
3259
|
+
// recursion (flag set). An external numeric where is an id — even
|
|
3260
|
+
// on non-decodeID tables, where ids are NOT line numbers (prepend
|
|
3261
|
+
// layout / rows deleted); using them as lines edited the wrong row
|
|
3262
|
+
// and padded the column file (Sep-2026 rename bug). Ids are
|
|
3263
|
+
// resolved by the next branch.
|
|
3264
|
+
_whereIsLinesNumbers) {
|
|
3201
3265
|
// "where" in this case, is the line(s) number(s) and not id(s)
|
|
3202
3266
|
const files = (await readdir(tablePath))?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)));
|
|
3203
3267
|
if (files.length) {
|
|
@@ -3263,13 +3327,13 @@ export default class Inibase {
|
|
|
3263
3327
|
if (this.transaction) {
|
|
3264
3328
|
if (!txnStaged && renameList.length)
|
|
3265
3329
|
await Promise.allSettled(renameList
|
|
3266
|
-
.filter((pair) => Boolean(pair[
|
|
3330
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3267
3331
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3268
3332
|
}
|
|
3269
3333
|
else {
|
|
3270
3334
|
if (renameList.length)
|
|
3271
3335
|
await Promise.allSettled(renameList
|
|
3272
|
-
.filter((pair) => Boolean(pair[
|
|
3336
|
+
.filter((pair) => Boolean(pair[0]))
|
|
3273
3337
|
.map(async ([tempPath, _]) => unlink(tempPath)));
|
|
3274
3338
|
await File.unlock(join(tablePath, ".tmp"));
|
|
3275
3339
|
}
|
|
@@ -3277,20 +3341,34 @@ export default class Inibase {
|
|
|
3277
3341
|
}
|
|
3278
3342
|
}
|
|
3279
3343
|
if ((!_whereIsLinesNumbers &&
|
|
3280
|
-
globalConfig[this.databasePath].tables?.get(tableName)?.config
|
|
3281
|
-
.decodeID &&
|
|
3282
3344
|
((Array.isArray(where) && where.every(Utils.isNumber)) ||
|
|
3283
3345
|
Utils.isNumber(where))) ||
|
|
3284
3346
|
(Array.isArray(where) && where.every(Utils.isValidID)) ||
|
|
3285
3347
|
Utils.isValidID(where)) {
|
|
3286
|
-
|
|
3287
|
-
//
|
|
3288
|
-
//
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3348
|
+
// Resolve id -> line(s) WHILE holding the writer lock (see put():
|
|
3349
|
+
// a concurrent post between resolution and delete would shift the
|
|
3350
|
+
// lines of a prepend table and remove the wrong row).
|
|
3351
|
+
let inlineLock = false;
|
|
3352
|
+
try {
|
|
3353
|
+
if (this.transaction)
|
|
3354
|
+
await this.ensureTxnLock(tableName);
|
|
3355
|
+
else {
|
|
3356
|
+
await File.lock(join(tablePath, ".tmp"));
|
|
3357
|
+
inlineLock = true;
|
|
3358
|
+
}
|
|
3359
|
+
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
|
3360
|
+
// Deleting a non-existent id must not fall through to the
|
|
3361
|
+
// "delete all rows" branch (this.delete(_, null, _) would truncate
|
|
3362
|
+
// the whole table), so resolve the id to line numbers first and
|
|
3363
|
+
// only delegate when something actually matched.
|
|
3364
|
+
if (lineNumbers)
|
|
3365
|
+
return await this.delete(tableName, lineNumbers, true, _cascadeGuard ?? new Set());
|
|
3366
|
+
return false;
|
|
3367
|
+
}
|
|
3368
|
+
finally {
|
|
3369
|
+
if (inlineLock)
|
|
3370
|
+
await File.unlock(join(tablePath, ".tmp"));
|
|
3371
|
+
}
|
|
3294
3372
|
}
|
|
3295
3373
|
if (Utils.isObject(where)) {
|
|
3296
3374
|
const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inibase",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Karim Amahtil",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"benchmark": "./benchmark/run.js",
|
|
89
89
|
"benchmark:durability": "tsx ./benchmark/durability.ts",
|
|
90
90
|
"benchmark:computed": "tsx ./benchmark/computed.ts",
|
|
91
|
-
"test": "tsx ./tests/inibase.test.ts && tsx ./tests/inibase.advanced.test.ts && tsx ./tests/transaction.test.ts && tsx ./tests/durability.test.ts && tsx ./tests/expression.test.ts && tsx ./tests/utils.test.ts && tsx ./tests/sum.nested.test.ts && tsx ./tests/computed.child.test.ts",
|
|
91
|
+
"test": "tsx ./tests/inibase.test.ts && tsx ./tests/inibase.advanced.test.ts && tsx ./tests/transaction.test.ts && tsx ./tests/durability.test.ts && tsx ./tests/expression.test.ts && tsx ./tests/utils.test.ts && tsx ./tests/sum.nested.test.ts && tsx ./tests/computed.child.test.ts && tsx ./tests/hardening.test.ts",
|
|
92
92
|
"test:durability": "tsx ./tests/durability.test.ts",
|
|
93
93
|
"test:transaction": "tsx ./tests/transaction.test.ts",
|
|
94
94
|
"test:utils": "tsx ./tests/utils.test.ts",
|