inibase 3.5.0 → 3.7.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/dist/file.d.ts +15 -1
- package/dist/file.js +106 -23
- package/dist/index.d.ts +15 -9
- package/dist/index.js +155 -134
- package/package.json +1 -1
package/dist/file.d.ts
CHANGED
|
@@ -88,6 +88,20 @@ export declare function get(filePath: string, lineNumbers: undefined | number |
|
|
|
88
88
|
Record<number, string | number | boolean | null | undefined | (string | number | boolean | (string | number | boolean)[] | null)[]> | null,
|
|
89
89
|
number
|
|
90
90
|
]>;
|
|
91
|
+
/**
|
|
92
|
+
* Reads only the requested line numbers of one column file and decodes each
|
|
93
|
+
* with `field`. Unlike the shell `sed -n` reader this stops as soon as the last
|
|
94
|
+
* requested line has been seen, so a point lookup never scans the column to
|
|
95
|
+
* EOF, and it decodes each column independently (no tab-delimited `paste`
|
|
96
|
+
* alignment to break on values that contain a literal tab). Compression is
|
|
97
|
+
* handled transparently: `.gz` columns are gunzipped on the fly.
|
|
98
|
+
*
|
|
99
|
+
* Every resource (readline, gunzip/source stream, file handle) is released on
|
|
100
|
+
* both the normal and the early-exit path.
|
|
101
|
+
*/
|
|
102
|
+
export declare const getLines: (filePath: string, lineNumbers: number[], field?: Field & {
|
|
103
|
+
databasePath?: string;
|
|
104
|
+
}) => Promise<Record<number, ReturnType<typeof decode>>>;
|
|
91
105
|
/**
|
|
92
106
|
* Asynchronously replaces specific lines in a file based on the provided replacements map or string.
|
|
93
107
|
*
|
|
@@ -146,7 +160,7 @@ export declare const remove: (filePath: string, linesToDelete: number | number[]
|
|
|
146
160
|
*/
|
|
147
161
|
export declare const search: (filePath: string, operator: ComparisonOperator | ComparisonOperator[], comparedAtValue: string | number | boolean | null | (string | number | boolean | null)[], logicalOperator?: "and" | "or", searchIn?: Set<number>, field?: Field & {
|
|
148
162
|
databasePath?: string;
|
|
149
|
-
}, limit?: number, offset?: number, readWholeFile?: boolean) => Promise<[Record<number, string | number | boolean | null | undefined | (string | number | boolean | null)[]> | null, number, Set<number> | null]>;
|
|
163
|
+
}, limit?: number, offset?: number, readWholeFile?: boolean, earlyStop?: boolean) => Promise<[Record<number, string | number | boolean | null | undefined | (string | number | boolean | null)[]> | null, number, Set<number> | null]>;
|
|
150
164
|
export declare const sum: (fp: string, ln?: number | number[]) => Promise<number>;
|
|
151
165
|
export declare const avg: (fp: string, ln?: number | number[]) => Promise<number>;
|
|
152
166
|
export declare const min: (fp: string, ln?: number | number[]) => Promise<number>;
|
package/dist/file.js
CHANGED
|
@@ -343,6 +343,16 @@ export function escapeShellPath(filePath) {
|
|
|
343
343
|
*/
|
|
344
344
|
let tempSeq = 0;
|
|
345
345
|
const tempPathFor = (filePath) => filePath.replace(/([^/]+)\/?$/, `.tmp/$1.${process.pid}-${(tempSeq++).toString(36)}`);
|
|
346
|
+
/**
|
|
347
|
+
* Cleanup-and-rethrow for a failed column write. A partial temp file must never
|
|
348
|
+
* be left behind, and the caller must observe the failure so it can abort the
|
|
349
|
+
* operation instead of publishing a column that is missing a line — which would
|
|
350
|
+
* desync the pagination row count from the column files.
|
|
351
|
+
*/
|
|
352
|
+
const failWrite = async (fileTempPath, error) => {
|
|
353
|
+
await unlink(fileTempPath).catch(() => { });
|
|
354
|
+
throw error;
|
|
355
|
+
};
|
|
346
356
|
const _pipeline = async (filePath, rl, writeStream, transform) => {
|
|
347
357
|
if (filePath.endsWith(".gz"))
|
|
348
358
|
await pipeline(rl, transform, createGzip(), writeStream);
|
|
@@ -361,6 +371,30 @@ const createReadLineInternface = (filePath, fileHandle) => createInterface({
|
|
|
361
371
|
: fileHandle.createReadStream(),
|
|
362
372
|
crlfDelay: Number.POSITIVE_INFINITY,
|
|
363
373
|
});
|
|
374
|
+
/**
|
|
375
|
+
* Like {@link createReadLineInternface} but also hands back a disposer that
|
|
376
|
+
* tears down the readline interface and both the source and (for `.gz`
|
|
377
|
+
* columns) gunzip streams. Sparse readers call the disposer in their `finally`
|
|
378
|
+
* so an early `break` — the whole point of a point lookup — cannot leave a read
|
|
379
|
+
* stream (and its libuv handle) dangling, which would keep the process alive.
|
|
380
|
+
*/
|
|
381
|
+
const openReadline = (filePath, fileHandle) => {
|
|
382
|
+
const source = fileHandle.createReadStream();
|
|
383
|
+
const input = filePath.endsWith(".gz") ? source.pipe(createGunzip()) : source;
|
|
384
|
+
const rl = createInterface({
|
|
385
|
+
input,
|
|
386
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
rl,
|
|
390
|
+
destroy: () => {
|
|
391
|
+
rl.close();
|
|
392
|
+
input.destroy();
|
|
393
|
+
if (input !== source)
|
|
394
|
+
source.destroy();
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
};
|
|
364
398
|
/**
|
|
365
399
|
* Checks if a file or directory exists at the specified path.
|
|
366
400
|
*
|
|
@@ -619,6 +653,45 @@ export async function get(filePath, lineNumbers, field, readWholeFile = false) {
|
|
|
619
653
|
await fileHandle?.close();
|
|
620
654
|
}
|
|
621
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Reads only the requested line numbers of one column file and decodes each
|
|
658
|
+
* with `field`. Unlike the shell `sed -n` reader this stops as soon as the last
|
|
659
|
+
* requested line has been seen, so a point lookup never scans the column to
|
|
660
|
+
* EOF, and it decodes each column independently (no tab-delimited `paste`
|
|
661
|
+
* alignment to break on values that contain a literal tab). Compression is
|
|
662
|
+
* handled transparently: `.gz` columns are gunzipped on the fly.
|
|
663
|
+
*
|
|
664
|
+
* Every resource (readline, gunzip/source stream, file handle) is released on
|
|
665
|
+
* both the normal and the early-exit path.
|
|
666
|
+
*/
|
|
667
|
+
export const getLines = async (filePath, lineNumbers, field) => {
|
|
668
|
+
const lines = {};
|
|
669
|
+
if (!lineNumbers.length)
|
|
670
|
+
return lines;
|
|
671
|
+
const wanted = new Set(lineNumbers);
|
|
672
|
+
const last = Math.max(...lineNumbers);
|
|
673
|
+
const config = field ?? {
|
|
674
|
+
key: "BLABLA",
|
|
675
|
+
type: "string",
|
|
676
|
+
};
|
|
677
|
+
const fileHandle = await open(filePath, "r");
|
|
678
|
+
const { rl, destroy } = openReadline(filePath, fileHandle);
|
|
679
|
+
try {
|
|
680
|
+
let linesCount = 0;
|
|
681
|
+
for await (const line of rl) {
|
|
682
|
+
linesCount++;
|
|
683
|
+
if (wanted.has(linesCount))
|
|
684
|
+
lines[linesCount] = decode(line, config);
|
|
685
|
+
if (linesCount >= last)
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
return lines;
|
|
689
|
+
}
|
|
690
|
+
finally {
|
|
691
|
+
destroy();
|
|
692
|
+
await fileHandle.close().catch(() => { });
|
|
693
|
+
}
|
|
694
|
+
};
|
|
622
695
|
/**
|
|
623
696
|
* Asynchronously replaces specific lines in a file based on the provided replacements map or string.
|
|
624
697
|
*
|
|
@@ -675,8 +748,8 @@ export const replace = async (filePath, replacements, totalItems) => {
|
|
|
675
748
|
}));
|
|
676
749
|
return [fileTempPath, filePath];
|
|
677
750
|
}
|
|
678
|
-
catch {
|
|
679
|
-
return
|
|
751
|
+
catch (error) {
|
|
752
|
+
return failWrite(fileTempPath, error);
|
|
680
753
|
}
|
|
681
754
|
finally {
|
|
682
755
|
// Ensure that file handles are closed, even if an error occurred
|
|
@@ -695,8 +768,8 @@ export const replace = async (filePath, replacements, totalItems) => {
|
|
|
695
768
|
await exec(command);
|
|
696
769
|
return [fileTempPath, filePath];
|
|
697
770
|
}
|
|
698
|
-
catch {
|
|
699
|
-
return
|
|
771
|
+
catch (error) {
|
|
772
|
+
return failWrite(fileTempPath, error);
|
|
700
773
|
}
|
|
701
774
|
}
|
|
702
775
|
}
|
|
@@ -736,8 +809,8 @@ export const replace = async (filePath, replacements, totalItems) => {
|
|
|
736
809
|
}
|
|
737
810
|
return [fileTempPath, filePath];
|
|
738
811
|
}
|
|
739
|
-
catch {
|
|
740
|
-
return
|
|
812
|
+
catch (error) {
|
|
813
|
+
return failWrite(fileTempPath, error);
|
|
741
814
|
}
|
|
742
815
|
}
|
|
743
816
|
return [];
|
|
@@ -759,10 +832,12 @@ export const append = async (filePath, data) => {
|
|
|
759
832
|
await appendFile(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
760
833
|
}
|
|
761
834
|
else {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
835
|
+
// Append a complete gzip member. Readers gunzip every member in
|
|
836
|
+
// sequence (multi-member stream), and this avoids the shell's
|
|
837
|
+
// argument-length limits and single-quote escaping the old
|
|
838
|
+
// `echo … | gzip - >> file` pipeline suffered from.
|
|
839
|
+
const text = `${Array.isArray(data) ? data.join("\n") : data}\n`;
|
|
840
|
+
await appendFile(fileTempPath, await gzip(text));
|
|
766
841
|
}
|
|
767
842
|
}
|
|
768
843
|
else
|
|
@@ -775,8 +850,8 @@ export const append = async (filePath, data) => {
|
|
|
775
850
|
: `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
776
851
|
return [fileTempPath, filePath];
|
|
777
852
|
}
|
|
778
|
-
catch {
|
|
779
|
-
return
|
|
853
|
+
catch (error) {
|
|
854
|
+
return failWrite(fileTempPath, error);
|
|
780
855
|
}
|
|
781
856
|
};
|
|
782
857
|
/**
|
|
@@ -808,8 +883,8 @@ export const prepend = async (filePath, data) => {
|
|
|
808
883
|
},
|
|
809
884
|
}));
|
|
810
885
|
}
|
|
811
|
-
catch {
|
|
812
|
-
return
|
|
886
|
+
catch (error) {
|
|
887
|
+
return failWrite(fileTempPath, error);
|
|
813
888
|
}
|
|
814
889
|
finally {
|
|
815
890
|
// Ensure that file handles are closed, even if an error occurred
|
|
@@ -826,8 +901,8 @@ export const prepend = async (filePath, data) => {
|
|
|
826
901
|
const escapedFileChildTempPath = escapeShellPath(fileChildTempPath);
|
|
827
902
|
await exec(`cat ${escapedFileChildTempPath} ${escapedFilePath} > ${escapedFileTempPath}`);
|
|
828
903
|
}
|
|
829
|
-
catch {
|
|
830
|
-
return
|
|
904
|
+
catch (error) {
|
|
905
|
+
return failWrite(fileTempPath, error);
|
|
831
906
|
}
|
|
832
907
|
finally {
|
|
833
908
|
await unlink(fileChildTempPath);
|
|
@@ -843,8 +918,8 @@ export const prepend = async (filePath, data) => {
|
|
|
843
918
|
? await gzip(`${Array.isArray(data) ? data.join("\n") : data}\n`)
|
|
844
919
|
: `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
845
920
|
}
|
|
846
|
-
catch {
|
|
847
|
-
return
|
|
921
|
+
catch (error) {
|
|
922
|
+
return failWrite(fileTempPath, error);
|
|
848
923
|
}
|
|
849
924
|
}
|
|
850
925
|
return [fileTempPath, filePath];
|
|
@@ -874,8 +949,8 @@ export const remove = async (filePath, linesToDelete) => {
|
|
|
874
949
|
await exec(command);
|
|
875
950
|
return [fileTempPath, filePath];
|
|
876
951
|
}
|
|
877
|
-
catch {
|
|
878
|
-
return
|
|
952
|
+
catch (error) {
|
|
953
|
+
return failWrite(fileTempPath, error);
|
|
879
954
|
}
|
|
880
955
|
};
|
|
881
956
|
/**
|
|
@@ -1094,7 +1169,7 @@ async function searchEqualsNative(filePath, patterns, field, searchIn, comparedA
|
|
|
1094
1169
|
*
|
|
1095
1170
|
* Note: Decodes each line for comparison and can handle complex queries with multiple conditions.
|
|
1096
1171
|
*/
|
|
1097
|
-
export const search = async (filePath, operator, comparedAtValue, logicalOperator, searchIn, field, limit, offset, readWholeFile) => {
|
|
1172
|
+
export const search = async (filePath, operator, comparedAtValue, logicalOperator, searchIn, field, limit, offset, readWholeFile, earlyStop) => {
|
|
1098
1173
|
// Native fast path for exact-equality searches (whole-line `grep -x -F`).
|
|
1099
1174
|
if (operator === "=" &&
|
|
1100
1175
|
!Array.isArray(operator) &&
|
|
@@ -1116,6 +1191,7 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
|
|
|
1116
1191
|
let linesCount = 0;
|
|
1117
1192
|
const linesNumbers = new Set();
|
|
1118
1193
|
let fileHandle = null;
|
|
1194
|
+
let destroyStreams = () => { };
|
|
1119
1195
|
const config = field ?? {
|
|
1120
1196
|
key: "BLABLA",
|
|
1121
1197
|
type: "string",
|
|
@@ -1131,7 +1207,8 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
|
|
|
1131
1207
|
// Open the file for reading.
|
|
1132
1208
|
fileHandle = await open(filePath, "r");
|
|
1133
1209
|
// Create a Readline interface to read the file line by line.
|
|
1134
|
-
const rl =
|
|
1210
|
+
const { rl, destroy } = openReadline(filePath, fileHandle);
|
|
1211
|
+
destroyStreams = destroy;
|
|
1135
1212
|
// Iterate through each line in the file.
|
|
1136
1213
|
for await (const line of rl) {
|
|
1137
1214
|
// Increment the line count for each line.
|
|
@@ -1162,6 +1239,10 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
|
|
|
1162
1239
|
}
|
|
1163
1240
|
// Store the decoded line in the result object.
|
|
1164
1241
|
matchingLines[linesCount] = decodedLine;
|
|
1242
|
+
// Every requested match is now in hand: stop instead of
|
|
1243
|
+
// scanning the rest of the column (id -> line resolution).
|
|
1244
|
+
if (earlyStop && limit && linesNumbers.size >= limit)
|
|
1245
|
+
break;
|
|
1165
1246
|
}
|
|
1166
1247
|
}
|
|
1167
1248
|
// Convert the Map to an object using Object.fromEntries and return the result.
|
|
@@ -1173,7 +1254,9 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
|
|
|
1173
1254
|
return [null, 0, null];
|
|
1174
1255
|
}
|
|
1175
1256
|
finally {
|
|
1176
|
-
//
|
|
1257
|
+
// Tear down the read streams (an early `break` must not leave a handle
|
|
1258
|
+
// open) and then the file handle, even if an error occurred.
|
|
1259
|
+
destroyStreams();
|
|
1177
1260
|
await fileHandle?.close();
|
|
1178
1261
|
}
|
|
1179
1262
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -130,7 +130,7 @@ export default class Inibase {
|
|
|
130
130
|
* per-table writer lock of every table it mutates, so mutations stage into
|
|
131
131
|
* the database journal and publish only on commit().
|
|
132
132
|
*/
|
|
133
|
-
private
|
|
133
|
+
private activeTransaction;
|
|
134
134
|
constructor(database: string, mainFolder?: string, language?: ErrorLang);
|
|
135
135
|
createError(name: ErrorCode, variable?: string | number | (string | number)[]): Error;
|
|
136
136
|
private validateName;
|
|
@@ -252,14 +252,6 @@ export default class Inibase {
|
|
|
252
252
|
private sameCellValue;
|
|
253
253
|
private isSimpleField;
|
|
254
254
|
private processSimpleField;
|
|
255
|
-
/**
|
|
256
|
-
* Batched read for top-level simple columns: a single `paste | sed` child
|
|
257
|
-
* process returns every requested line of every column at once instead of
|
|
258
|
-
* spawning one `sed`/`gunzip|sed` child process per column. Returning null
|
|
259
|
-
* (e.g. when any column file is missing, or the shell command fails) makes
|
|
260
|
-
* the caller fall back to the existing per-column reads.
|
|
261
|
-
*/
|
|
262
|
-
private processSimpleFieldsBatch;
|
|
263
255
|
private isArrayField;
|
|
264
256
|
private processArrayField;
|
|
265
257
|
private isObjectField;
|
|
@@ -350,6 +342,20 @@ export default class Inibase {
|
|
|
350
342
|
* live file is touched (nothing is published before commit()).
|
|
351
343
|
*/
|
|
352
344
|
rollback(): Promise<void>;
|
|
345
|
+
/**
|
|
346
|
+
* Run `callback` inside a transaction, committing when it resolves and
|
|
347
|
+
* rolling back when it throws. Prefer this over a bare begin/commit pair:
|
|
348
|
+
* the database lock taken by begin() is held for the transaction's whole
|
|
349
|
+
* lifetime, so an exception between begin() and commit() would otherwise
|
|
350
|
+
* block every later write on this database for the full lock-wait timeout.
|
|
351
|
+
*
|
|
352
|
+
* @param tablesOrCallback Pre-lock list (sorted, deadlock-free) or the callback
|
|
353
|
+
* @param maybeCallback The callback, when tables were passed first
|
|
354
|
+
* @param tables Pre-lock list when the callback was passed first
|
|
355
|
+
* @return {*} Whatever the callback resolves to
|
|
356
|
+
*/
|
|
357
|
+
transaction<T>(tablesOrCallback: string[] | ((db: Inibase) => Promise<T>), maybeCallback?: (db: Inibase) => Promise<T>, tables?: string[]): Promise<T>;
|
|
358
|
+
private _transaction;
|
|
353
359
|
/**
|
|
354
360
|
* Snapshot the identity (dev:inode:mtime:size) of every column file and
|
|
355
361
|
* the pagination file. Reading data and then re-verifying this snapshot
|
package/dist/index.js
CHANGED
|
@@ -110,6 +110,18 @@ function astHasHops(node) {
|
|
|
110
110
|
return astHasHops(node.arg);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Await a batch of column-write promises and surface the first rejection.
|
|
115
|
+
* `Promise.allSettled` never short-circuits, so a swallowed failure would let
|
|
116
|
+
* the caller publish a partial column set — the pagination row count and the
|
|
117
|
+
* column line counts would diverge. Every batch that fills a `renameList` must
|
|
118
|
+
* be inspected through here before the journal/pagination is touched.
|
|
119
|
+
*/
|
|
120
|
+
async function settleWrites(results) {
|
|
121
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
122
|
+
if (failed)
|
|
123
|
+
throw failed.reason;
|
|
124
|
+
}
|
|
113
125
|
/**
|
|
114
126
|
* @param {string} database - Database name
|
|
115
127
|
* @param {string} [mainFolder="."] - Main folder path
|
|
@@ -140,7 +152,7 @@ export default class Inibase {
|
|
|
140
152
|
* per-table writer lock of every table it mutates, so mutations stage into
|
|
141
153
|
* the database journal and publish only on commit().
|
|
142
154
|
*/
|
|
143
|
-
|
|
155
|
+
activeTransaction = null;
|
|
144
156
|
constructor(database, mainFolder = ".", language = "en") {
|
|
145
157
|
this.language = language;
|
|
146
158
|
this.validateName(database);
|
|
@@ -226,7 +238,7 @@ export default class Inibase {
|
|
|
226
238
|
this.validateName(tableName);
|
|
227
239
|
// DDL does not participate in the write-ahead journal: schema surgery
|
|
228
240
|
// inside a transaction would escape the atomic publish/rollback scope.
|
|
229
|
-
if (this.
|
|
241
|
+
if (this.activeTransaction)
|
|
230
242
|
throw this.createError("INVALID_PARAMETERS");
|
|
231
243
|
await this.ensureDatabaseRecovered();
|
|
232
244
|
if (schema)
|
|
@@ -300,7 +312,7 @@ export default class Inibase {
|
|
|
300
312
|
this.validateName(tableName);
|
|
301
313
|
// DDL does not participate in the write-ahead journal: schema surgery
|
|
302
314
|
// inside a transaction would escape the atomic publish/rollback scope.
|
|
303
|
-
if (this.
|
|
315
|
+
if (this.activeTransaction)
|
|
304
316
|
throw this.createError("INVALID_PARAMETERS");
|
|
305
317
|
await this.ensureDatabaseRecovered();
|
|
306
318
|
if (config?.name)
|
|
@@ -982,19 +994,39 @@ export default class Inibase {
|
|
|
982
994
|
}
|
|
983
995
|
async processSchemaData(tableName, schema, linesNumber, options, prefix) {
|
|
984
996
|
const RETURN = {};
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
//
|
|
997
|
+
// Sparse path: for an explicit finite selection, stream each top-level
|
|
998
|
+
// simple column only up to the last requested line (never to EOF) and
|
|
999
|
+
// decode it independently, instead of pasting every column through a
|
|
1000
|
+
// shell pipeline that always scans the whole table and can misalign on
|
|
1001
|
+
// values containing a literal tab. Nested (prefixed) schemas keep the
|
|
1002
|
+
// per-column path below.
|
|
988
1003
|
let batchedKeys = null;
|
|
989
1004
|
if (!prefix && linesNumber?.length) {
|
|
990
1005
|
const simpleFields = schema.filter((field) => this.isSimpleField(field.type));
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1006
|
+
const decodeID = globalConfig[this.databasePath].tables?.get(tableName)?.config
|
|
1007
|
+
.decodeID === true;
|
|
1008
|
+
const extension = this.getFileExtension(tableName);
|
|
1009
|
+
const reads = await Promise.all(simpleFields.map(async (field) => {
|
|
1010
|
+
const fieldPath = join(this.databasePath, tableName, `${field.key}${extension}`);
|
|
1011
|
+
if (!(await File.isExists(fieldPath)))
|
|
1012
|
+
return null;
|
|
1013
|
+
const items = await File.getLines(fieldPath, linesNumber, {
|
|
1014
|
+
...field,
|
|
1015
|
+
type: field.key === "id" && decodeID ? "number" : field.type,
|
|
1016
|
+
databasePath: this.databasePath,
|
|
1017
|
+
});
|
|
1018
|
+
return { key: field.key, items };
|
|
1019
|
+
}));
|
|
1020
|
+
batchedKeys = new Set(simpleFields.map((field) => field.key));
|
|
1021
|
+
for (const read of reads) {
|
|
1022
|
+
if (!read)
|
|
1023
|
+
continue;
|
|
1024
|
+
for (const [line, value] of Object.entries(read.items)) {
|
|
1025
|
+
if (typeof value === "undefined")
|
|
1026
|
+
continue;
|
|
995
1027
|
if (!RETURN[line])
|
|
996
1028
|
RETURN[line] = {};
|
|
997
|
-
Object.assign(RETURN[line],
|
|
1029
|
+
Object.assign(RETURN[line], { [read.key]: value });
|
|
998
1030
|
}
|
|
999
1031
|
}
|
|
1000
1032
|
}
|
|
@@ -1428,7 +1460,7 @@ export default class Inibase {
|
|
|
1428
1460
|
return null;
|
|
1429
1461
|
// A transaction cannot read a dependency table it has already
|
|
1430
1462
|
// staged (no read-your-writes past its commit point).
|
|
1431
|
-
if (this.
|
|
1463
|
+
if (this.activeTransaction?.tables.has(table))
|
|
1432
1464
|
throw this.createError("INVALID_PARAMETERS");
|
|
1433
1465
|
// Batched link-hop reads: during the collection pass a hop
|
|
1434
1466
|
// only records its (table, column, id) triple; the real pass
|
|
@@ -1658,78 +1690,6 @@ export default class Inibase {
|
|
|
1658
1690
|
}
|
|
1659
1691
|
}
|
|
1660
1692
|
}
|
|
1661
|
-
/**
|
|
1662
|
-
* Batched read for top-level simple columns: a single `paste | sed` child
|
|
1663
|
-
* process returns every requested line of every column at once instead of
|
|
1664
|
-
* spawning one `sed`/`gunzip|sed` child process per column. Returning null
|
|
1665
|
-
* (e.g. when any column file is missing, or the shell command fails) makes
|
|
1666
|
-
* the caller fall back to the existing per-column reads.
|
|
1667
|
-
*/
|
|
1668
|
-
async processSimpleFieldsBatch(tableName, fields, linesNumber) {
|
|
1669
|
-
if (!fields.length || !linesNumber.length)
|
|
1670
|
-
return null;
|
|
1671
|
-
const decodeID = globalConfig[this.databasePath].tables?.get(tableName)?.config
|
|
1672
|
-
.decodeID === true;
|
|
1673
|
-
// Decode configs are computed once per column (the per-column read path
|
|
1674
|
-
// builds one config per file too), not once per cell.
|
|
1675
|
-
const cols = [];
|
|
1676
|
-
for (const field of fields) {
|
|
1677
|
-
const path = join(this.databasePath, tableName, `${field.key}${this.getFileExtension(tableName)}`);
|
|
1678
|
-
if (!(await File.isExists(path)))
|
|
1679
|
-
return null;
|
|
1680
|
-
cols.push({
|
|
1681
|
-
path,
|
|
1682
|
-
key: field.key,
|
|
1683
|
-
config: {
|
|
1684
|
-
...field,
|
|
1685
|
-
type: field.key === "id" && decodeID ? "number" : field.type,
|
|
1686
|
-
databasePath: this.databasePath,
|
|
1687
|
-
},
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
const sortedLines = [...linesNumber].sort((a, b) => a - b);
|
|
1691
|
-
const range = File._groupIntoRanges(sortedLines);
|
|
1692
|
-
const files = cols.map(({ path }) => File.escapeShellPath(path)).join(" ");
|
|
1693
|
-
const isGz = this.getFileExtension(tableName).endsWith(".gz");
|
|
1694
|
-
// Each compressed column needs its own process substitution so the
|
|
1695
|
-
// streams stay aligned column-by-column inside `paste`.
|
|
1696
|
-
const pasteInputs = isGz
|
|
1697
|
-
? cols
|
|
1698
|
-
.map(({ path }) => `<(gunzip -c ${File.escapeShellPath(path)})`)
|
|
1699
|
-
.join(" ")
|
|
1700
|
-
: files;
|
|
1701
|
-
const command = isGz
|
|
1702
|
-
? `bash -c 'paste -d "\\t" ${pasteInputs} | sed -n "${range}"'`
|
|
1703
|
-
: `paste -d'\\t' ${files} | sed -n '${range}'`;
|
|
1704
|
-
let output;
|
|
1705
|
-
try {
|
|
1706
|
-
output = (await UtilsServer.exec(command)).stdout;
|
|
1707
|
-
}
|
|
1708
|
-
catch {
|
|
1709
|
-
return null;
|
|
1710
|
-
}
|
|
1711
|
-
const outLines = output.trimEnd().split("\n");
|
|
1712
|
-
const RETURN = {};
|
|
1713
|
-
for (let i = 0; i < outLines.length && i < sortedLines.length; i++) {
|
|
1714
|
-
const lineNo = sortedLines[i];
|
|
1715
|
-
const cells = outLines[i].split("\t");
|
|
1716
|
-
const row = {};
|
|
1717
|
-
let added = false;
|
|
1718
|
-
for (let c = 0; c < cols.length; c++) {
|
|
1719
|
-
const raw = cells[c];
|
|
1720
|
-
if (raw === undefined)
|
|
1721
|
-
continue;
|
|
1722
|
-
const value = File.decode(raw, cols[c].config);
|
|
1723
|
-
if (value !== undefined) {
|
|
1724
|
-
row[cols[c].key] = value;
|
|
1725
|
-
added = true;
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
if (added)
|
|
1729
|
-
RETURN[lineNo] = row;
|
|
1730
|
-
}
|
|
1731
|
-
return Object.keys(RETURN).length ? RETURN : null;
|
|
1732
|
-
}
|
|
1733
1693
|
// Helper function to check if the field type is array
|
|
1734
1694
|
isArrayField(fieldType) {
|
|
1735
1695
|
return ((Array.isArray(fieldType) &&
|
|
@@ -2210,7 +2170,7 @@ export default class Inibase {
|
|
|
2210
2170
|
}
|
|
2211
2171
|
/** Staged per-table entry of the open transaction, or null when none. */
|
|
2212
2172
|
txnTableEntry(tableName) {
|
|
2213
|
-
const txn = this.
|
|
2173
|
+
const txn = this.activeTransaction;
|
|
2214
2174
|
if (!txn)
|
|
2215
2175
|
return null;
|
|
2216
2176
|
let entry = txn.tables.get(tableName);
|
|
@@ -2272,7 +2232,7 @@ export default class Inibase {
|
|
|
2272
2232
|
* would need read-your-writes composition).
|
|
2273
2233
|
*/
|
|
2274
2234
|
async stageTxnOp(tableName, renameList, pagination) {
|
|
2275
|
-
const txn = this.
|
|
2235
|
+
const txn = this.activeTransaction;
|
|
2276
2236
|
const entry = this.txnTableEntry(tableName);
|
|
2277
2237
|
if (!txn || !entry)
|
|
2278
2238
|
throw this.createError("INVALID_PARAMETERS");
|
|
@@ -2317,7 +2277,7 @@ export default class Inibase {
|
|
|
2317
2277
|
return File.runWithLockStore(() => this._begin(tables));
|
|
2318
2278
|
}
|
|
2319
2279
|
async _begin(tables = []) {
|
|
2320
|
-
if (this.
|
|
2280
|
+
if (this.activeTransaction)
|
|
2321
2281
|
throw this.createError("INVALID_PARAMETERS");
|
|
2322
2282
|
await this.ensureDatabaseTmpDir();
|
|
2323
2283
|
// The database lock is the transaction mutex: it serializes
|
|
@@ -2333,16 +2293,16 @@ export default class Inibase {
|
|
|
2333
2293
|
await this.getTable(name); // throws TABLE_NOT_EXISTS
|
|
2334
2294
|
}
|
|
2335
2295
|
const id = randomUUID();
|
|
2336
|
-
this.
|
|
2296
|
+
this.activeTransaction = {
|
|
2337
2297
|
id,
|
|
2338
2298
|
journal: new DatabaseJournal(this.databasePath, id),
|
|
2339
2299
|
tables: new Map(),
|
|
2340
2300
|
};
|
|
2341
|
-
await this.
|
|
2301
|
+
await this.activeTransaction.journal.begin(uniqueTables);
|
|
2342
2302
|
for (const name of uniqueTables) {
|
|
2343
2303
|
await File.lock(join(this.databasePath, name, ".tmp"));
|
|
2344
2304
|
acquired.push(name);
|
|
2345
|
-
this.
|
|
2305
|
+
this.activeTransaction.tables.set(name, {
|
|
2346
2306
|
locked: true,
|
|
2347
2307
|
paginationFrom: "",
|
|
2348
2308
|
lastId: 0,
|
|
@@ -2357,7 +2317,7 @@ export default class Inibase {
|
|
|
2357
2317
|
// lock file).
|
|
2358
2318
|
for (const name of acquired)
|
|
2359
2319
|
await File.unlock(join(this.databasePath, name, ".tmp")).catch(() => { });
|
|
2360
|
-
this.
|
|
2320
|
+
this.activeTransaction = null;
|
|
2361
2321
|
await File.unlock(join(this.databasePath, ".tmp")).catch(() => { });
|
|
2362
2322
|
throw error;
|
|
2363
2323
|
}
|
|
@@ -2371,7 +2331,7 @@ export default class Inibase {
|
|
|
2371
2331
|
* tables, marker -> roll forward all tables).
|
|
2372
2332
|
*/
|
|
2373
2333
|
async commit() {
|
|
2374
|
-
const txn = this.
|
|
2334
|
+
const txn = this.activeTransaction;
|
|
2375
2335
|
if (!txn)
|
|
2376
2336
|
throw this.createError("INVALID_PARAMETERS");
|
|
2377
2337
|
try {
|
|
@@ -2404,6 +2364,25 @@ export default class Inibase {
|
|
|
2404
2364
|
await File.syncDir(join(this.databasePath, tableName));
|
|
2405
2365
|
await File.syncDir(join(this.databasePath, tableName, ".tmp"));
|
|
2406
2366
|
}
|
|
2367
|
+
// A staged table's row count and derived caches were deliberately
|
|
2368
|
+
// left untouched while the transaction was open (nothing is
|
|
2369
|
+
// published before commit), so they must be refreshed here — the
|
|
2370
|
+
// single-table fast path does the same right after commitFiles().
|
|
2371
|
+
// Without this a committed `put` (row count unchanged) would keep
|
|
2372
|
+
// serving sorted/criteria .cache entries built before the write.
|
|
2373
|
+
for (const tableName of txn.tables.keys()) {
|
|
2374
|
+
const entry = txn.tables.get(tableName);
|
|
2375
|
+
if (!entry?.staged.length)
|
|
2376
|
+
continue; // pre-locked but untouched
|
|
2377
|
+
// Only trust entry.total when resolvePagination() recorded it.
|
|
2378
|
+
// A `put` selected by `where` never reads the pagination file
|
|
2379
|
+
// (its row count cannot change), so entry.total is still its
|
|
2380
|
+
// initial 0 — publishing that would corrupt the live counter.
|
|
2381
|
+
if (entry.paginationFrom)
|
|
2382
|
+
this.totalItems.set(`${tableName}-*`, entry.total);
|
|
2383
|
+
if (globalConfig[this.databasePath].tables?.get(tableName)?.config.cache)
|
|
2384
|
+
await this.clearCache(tableName);
|
|
2385
|
+
}
|
|
2407
2386
|
}
|
|
2408
2387
|
catch (error) {
|
|
2409
2388
|
await txn.journal.rollback().catch(() => { });
|
|
@@ -2413,7 +2392,7 @@ export default class Inibase {
|
|
|
2413
2392
|
for (const tableName of [...txn.tables.keys()].sort().reverse())
|
|
2414
2393
|
await File.unlock(join(this.databasePath, tableName, ".tmp"));
|
|
2415
2394
|
await File.unlock(join(this.databasePath, ".tmp"));
|
|
2416
|
-
this.
|
|
2395
|
+
this.activeTransaction = null;
|
|
2417
2396
|
}
|
|
2418
2397
|
}
|
|
2419
2398
|
/**
|
|
@@ -2421,7 +2400,7 @@ export default class Inibase {
|
|
|
2421
2400
|
* live file is touched (nothing is published before commit()).
|
|
2422
2401
|
*/
|
|
2423
2402
|
async rollback() {
|
|
2424
|
-
const txn = this.
|
|
2403
|
+
const txn = this.activeTransaction;
|
|
2425
2404
|
if (!txn)
|
|
2426
2405
|
throw this.createError("INVALID_PARAMETERS");
|
|
2427
2406
|
try {
|
|
@@ -2431,7 +2410,44 @@ export default class Inibase {
|
|
|
2431
2410
|
for (const tableName of [...txn.tables.keys()].sort().reverse())
|
|
2432
2411
|
await File.unlock(join(this.databasePath, tableName, ".tmp"));
|
|
2433
2412
|
await File.unlock(join(this.databasePath, ".tmp"));
|
|
2434
|
-
this.
|
|
2413
|
+
this.activeTransaction = null;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Run `callback` inside a transaction, committing when it resolves and
|
|
2418
|
+
* rolling back when it throws. Prefer this over a bare begin/commit pair:
|
|
2419
|
+
* the database lock taken by begin() is held for the transaction's whole
|
|
2420
|
+
* lifetime, so an exception between begin() and commit() would otherwise
|
|
2421
|
+
* block every later write on this database for the full lock-wait timeout.
|
|
2422
|
+
*
|
|
2423
|
+
* @param tablesOrCallback Pre-lock list (sorted, deadlock-free) or the callback
|
|
2424
|
+
* @param maybeCallback The callback, when tables were passed first
|
|
2425
|
+
* @param tables Pre-lock list when the callback was passed first
|
|
2426
|
+
* @return {*} Whatever the callback resolves to
|
|
2427
|
+
*/
|
|
2428
|
+
async transaction(tablesOrCallback, maybeCallback, tables = []) {
|
|
2429
|
+
return File.runWithLockStore(() => this._transaction(tablesOrCallback, maybeCallback, tables));
|
|
2430
|
+
}
|
|
2431
|
+
async _transaction(tablesOrCallback, maybeCallback, tables = []) {
|
|
2432
|
+
const callback = typeof tablesOrCallback === "function" ? tablesOrCallback : maybeCallback;
|
|
2433
|
+
if (!callback)
|
|
2434
|
+
throw this.createError("INVALID_PARAMETERS");
|
|
2435
|
+
if (Array.isArray(tablesOrCallback))
|
|
2436
|
+
tables = tablesOrCallback;
|
|
2437
|
+
await this.begin(tables);
|
|
2438
|
+
let committed = false;
|
|
2439
|
+
try {
|
|
2440
|
+
const result = await callback(this);
|
|
2441
|
+
await this.commit();
|
|
2442
|
+
committed = true;
|
|
2443
|
+
return result;
|
|
2444
|
+
}
|
|
2445
|
+
finally {
|
|
2446
|
+
// commit() already rolled back and cleared the transaction when it
|
|
2447
|
+
// failed, so this only fires for a throwing callback (or one that
|
|
2448
|
+
// never committed) — the case that would otherwise leak the lock.
|
|
2449
|
+
if (!committed && this.activeTransaction)
|
|
2450
|
+
await this.rollback().catch(() => { });
|
|
2435
2451
|
}
|
|
2436
2452
|
}
|
|
2437
2453
|
/**
|
|
@@ -2712,7 +2728,11 @@ export default class Inibase {
|
|
|
2712
2728
|
}
|
|
2713
2729
|
}
|
|
2714
2730
|
if (!lineNumbers) {
|
|
2715
|
-
[lineNumbers, countItems] = await File.search(join(tablePath, `id${this.getFileExtension(tableName)}`), "[]", Ids.map((id) => Utils.isNumber(id) ? Number(id) : UtilsServer.decodeID(id)), undefined, undefined, { key: "BLABLA", type: "number" }, Ids.length, 0,
|
|
2731
|
+
[lineNumbers, countItems] = await File.search(join(tablePath, `id${this.getFileExtension(tableName)}`), "[]", Ids.map((id) => Utils.isNumber(id) ? Number(id) : UtilsServer.decodeID(id)), undefined, undefined, { key: "BLABLA", type: "number" }, Ids.length, 0,
|
|
2732
|
+
// The caller only needs the matching line numbers, not a
|
|
2733
|
+
// whole-table count: stop as soon as every requested id has
|
|
2734
|
+
// been found instead of scanning the column to EOF.
|
|
2735
|
+
false, true);
|
|
2716
2736
|
}
|
|
2717
2737
|
if (!lineNumbers)
|
|
2718
2738
|
return null;
|
|
@@ -2805,7 +2825,7 @@ export default class Inibase {
|
|
|
2805
2825
|
? options.columns
|
|
2806
2826
|
: [options.columns]));
|
|
2807
2827
|
const tablePath = join(this.databasePath, tableName);
|
|
2808
|
-
if (!this.
|
|
2828
|
+
if (!this.activeTransaction)
|
|
2809
2829
|
await this.ensureDatabaseRecovered();
|
|
2810
2830
|
await this.getTable(tableName);
|
|
2811
2831
|
if (!globalConfig[this.databasePath].tables?.get(tableName)?.schema)
|
|
@@ -2819,13 +2839,13 @@ export default class Inibase {
|
|
|
2819
2839
|
try {
|
|
2820
2840
|
// Inside a transaction the table lock is taken once per txn (and
|
|
2821
2841
|
// held until commit/rollback); otherwise the usual writer lock.
|
|
2822
|
-
if (this.
|
|
2842
|
+
if (this.activeTransaction)
|
|
2823
2843
|
await this.ensureTxnLock(tableName);
|
|
2824
2844
|
else
|
|
2825
2845
|
await File.lock(join(tablePath, ".tmp"));
|
|
2826
2846
|
const { filePath: paginationFilePath, lastId, total: _totalItems, } = await this.resolvePagination(tableName);
|
|
2827
2847
|
let lastIdValue = lastId;
|
|
2828
|
-
if (!this.
|
|
2848
|
+
if (!this.activeTransaction)
|
|
2829
2849
|
this.totalItems.set(`${tableName}-*`, _totalItems);
|
|
2830
2850
|
if (Utils.isArrayOfObjects(clonedData))
|
|
2831
2851
|
for (let index = 0; index < clonedData.length; index++) {
|
|
@@ -2859,16 +2879,16 @@ export default class Inibase {
|
|
|
2859
2879
|
? clonedData.toReversed()
|
|
2860
2880
|
: clonedData
|
|
2861
2881
|
: clonedData);
|
|
2862
|
-
await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(globalConfig[this.databasePath].tables?.get(tableName)?.config
|
|
2882
|
+
await settleWrites(await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(globalConfig[this.databasePath].tables?.get(tableName)?.config
|
|
2863
2883
|
.prepend
|
|
2864
2884
|
? await File.prepend(path, content)
|
|
2865
|
-
: await File.append(path, content))));
|
|
2885
|
+
: await File.append(path, content)))));
|
|
2866
2886
|
const newTotal = _totalItems + (Array.isArray(data) ? data.length : 1);
|
|
2867
2887
|
const pagination = {
|
|
2868
2888
|
from: paginationFilePath,
|
|
2869
2889
|
to: join(tablePath, `${lastIdValue}-${newTotal}.pagination`),
|
|
2870
2890
|
};
|
|
2871
|
-
if (this.
|
|
2891
|
+
if (this.activeTransaction) {
|
|
2872
2892
|
// Stage: journal the intent (fsynced op entry); the live files
|
|
2873
2893
|
// only change when commit() publishes.
|
|
2874
2894
|
await this.stageTxnOp(tableName, renameList, pagination);
|
|
@@ -2887,7 +2907,7 @@ export default class Inibase {
|
|
|
2887
2907
|
await this.clearCache(tableName);
|
|
2888
2908
|
}
|
|
2889
2909
|
if (returnPostedData) {
|
|
2890
|
-
if (this.
|
|
2910
|
+
if (this.activeTransaction)
|
|
2891
2911
|
// No read-your-writes yet: return the formatted staged rows
|
|
2892
2912
|
// (ids + defaults) instead of a committed-state read.
|
|
2893
2913
|
return (Array.isArray(clonedData) ? clonedData : clonedData);
|
|
@@ -2908,15 +2928,13 @@ export default class Inibase {
|
|
|
2908
2928
|
// not encode it here. Every other table keeps returning the
|
|
2909
2929
|
// encrypted id.
|
|
2910
2930
|
const tableConfig = globalConfig[this.databasePath].tables?.get(tableName)?.config;
|
|
2911
|
-
const postedID = (id) => tableConfig?.decodeID === true
|
|
2912
|
-
? Number(id)
|
|
2913
|
-
: UtilsServer.encodeID(id);
|
|
2931
|
+
const postedID = (id) => tableConfig?.decodeID === true ? Number(id) : UtilsServer.encodeID(id);
|
|
2914
2932
|
return Array.isArray(clonedData)
|
|
2915
2933
|
? (tableConfig?.prepend ? clonedData.toReversed() : clonedData).map(({ id }) => postedID(id))
|
|
2916
2934
|
: postedID(clonedData.id);
|
|
2917
2935
|
}
|
|
2918
2936
|
finally {
|
|
2919
|
-
if (this.
|
|
2937
|
+
if (this.activeTransaction) {
|
|
2920
2938
|
// Staged temps belong to the journal op; commit()/rollback()
|
|
2921
2939
|
// owns them. Temps from a failed pre-stage attempt are cleaned
|
|
2922
2940
|
// here so nothing leaks.
|
|
@@ -2952,7 +2970,7 @@ export default class Inibase {
|
|
|
2952
2970
|
? options.columns
|
|
2953
2971
|
: [options.columns]));
|
|
2954
2972
|
const tablePath = join(this.databasePath, tableName);
|
|
2955
|
-
if (!this.
|
|
2973
|
+
if (!this.activeTransaction)
|
|
2956
2974
|
await this.ensureDatabaseRecovered();
|
|
2957
2975
|
await this.throwErrorIfTableEmpty(tableName);
|
|
2958
2976
|
let clonedData = structuredClone(data);
|
|
@@ -2980,7 +2998,7 @@ export default class Inibase {
|
|
|
2980
2998
|
// row overlaid with the payload. The plan itself is schema-only.
|
|
2981
2999
|
const computedPlan = await this.buildComputedPlan(tableName);
|
|
2982
3000
|
try {
|
|
2983
|
-
if (this.
|
|
3001
|
+
if (this.activeTransaction)
|
|
2984
3002
|
await this.ensureTxnLock(tableName);
|
|
2985
3003
|
else
|
|
2986
3004
|
await File.lock(join(tablePath, ".tmp"));
|
|
@@ -3006,8 +3024,8 @@ export default class Inibase {
|
|
|
3006
3024
|
const evaluated = await this.evaluateComputedRows(tableName, computedPlan, mergedRows);
|
|
3007
3025
|
this.mergeComputedLineRecords(tableName, computedPlan, evaluated, pathesContents, existing);
|
|
3008
3026
|
}
|
|
3009
|
-
await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content, total))));
|
|
3010
|
-
if (this.
|
|
3027
|
+
await settleWrites(await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content, total)))));
|
|
3028
|
+
if (this.activeTransaction) {
|
|
3011
3029
|
// Stage instead of publishing: row count is unchanged so
|
|
3012
3030
|
// there is no pagination rename to journal.
|
|
3013
3031
|
await this.stageTxnOp(tableName, renameList, null);
|
|
@@ -3020,7 +3038,7 @@ export default class Inibase {
|
|
|
3020
3038
|
await this.clearCache(tableName);
|
|
3021
3039
|
}
|
|
3022
3040
|
if (returnUpdatedData) {
|
|
3023
|
-
if (this.
|
|
3041
|
+
if (this.activeTransaction)
|
|
3024
3042
|
// Reading the committed state would miss the staged
|
|
3025
3043
|
// write (no read-your-writes yet).
|
|
3026
3044
|
throw this.createError("INVALID_PARAMETERS");
|
|
@@ -3028,7 +3046,7 @@ export default class Inibase {
|
|
|
3028
3046
|
}
|
|
3029
3047
|
}
|
|
3030
3048
|
finally {
|
|
3031
|
-
if (this.
|
|
3049
|
+
if (this.activeTransaction) {
|
|
3032
3050
|
// Staged temps belong to the journal op; commit()/rollback()
|
|
3033
3051
|
// owns them.
|
|
3034
3052
|
if (!txnStaged && renameList.length)
|
|
@@ -3075,7 +3093,7 @@ export default class Inibase {
|
|
|
3075
3093
|
try {
|
|
3076
3094
|
// One global lock per table serializes every writer; inside a
|
|
3077
3095
|
// transaction the lock is held for the whole txn.
|
|
3078
|
-
if (this.
|
|
3096
|
+
if (this.activeTransaction)
|
|
3079
3097
|
await this.ensureTxnLock(tableName);
|
|
3080
3098
|
else
|
|
3081
3099
|
await File.lock(join(tablePath, ".tmp"));
|
|
@@ -3103,8 +3121,8 @@ export default class Inibase {
|
|
|
3103
3121
|
}
|
|
3104
3122
|
this.mergeComputedLineRecords(tableName, computedPlan, evaluated, pathesContents, existing);
|
|
3105
3123
|
}
|
|
3106
|
-
await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content))));
|
|
3107
|
-
if (this.
|
|
3124
|
+
await settleWrites(await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content)))));
|
|
3125
|
+
if (this.activeTransaction) {
|
|
3108
3126
|
await this.stageTxnOp(tableName, renameList, null);
|
|
3109
3127
|
txnStaged = true;
|
|
3110
3128
|
}
|
|
@@ -3114,7 +3132,7 @@ export default class Inibase {
|
|
|
3114
3132
|
await this.clearCache(tableName);
|
|
3115
3133
|
}
|
|
3116
3134
|
if (returnUpdatedData) {
|
|
3117
|
-
if (this.
|
|
3135
|
+
if (this.activeTransaction)
|
|
3118
3136
|
throw this.createError("INVALID_PARAMETERS");
|
|
3119
3137
|
// AWAITED readback: must complete inside the writer lock. An
|
|
3120
3138
|
// async-tail `return this.get(...)` lets the finally below
|
|
@@ -3123,7 +3141,7 @@ export default class Inibase {
|
|
|
3123
3141
|
}
|
|
3124
3142
|
}
|
|
3125
3143
|
finally {
|
|
3126
|
-
if (this.
|
|
3144
|
+
if (this.activeTransaction) {
|
|
3127
3145
|
if (!txnStaged && renameList.length)
|
|
3128
3146
|
await Promise.allSettled(renameList
|
|
3129
3147
|
.filter((pair) => Boolean(pair[0]))
|
|
@@ -3150,7 +3168,7 @@ export default class Inibase {
|
|
|
3150
3168
|
// the lock is held continuously from resolution to publish.
|
|
3151
3169
|
let inlineLock = false;
|
|
3152
3170
|
try {
|
|
3153
|
-
if (this.
|
|
3171
|
+
if (this.activeTransaction)
|
|
3154
3172
|
await this.ensureTxnLock(tableName);
|
|
3155
3173
|
else {
|
|
3156
3174
|
await File.lock(join(tablePath, ".tmp"));
|
|
@@ -3194,7 +3212,7 @@ export default class Inibase {
|
|
|
3194
3212
|
}
|
|
3195
3213
|
async _delete(tableName, where, _whereIsLinesNumbers, _cascadeGuard) {
|
|
3196
3214
|
this.validateName(tableName);
|
|
3197
|
-
if (!this.
|
|
3215
|
+
if (!this.activeTransaction)
|
|
3198
3216
|
await this.ensureDatabaseRecovered();
|
|
3199
3217
|
const tablePath = join(this.databasePath, tableName);
|
|
3200
3218
|
await this.throwErrorIfTableEmpty(tableName);
|
|
@@ -3204,7 +3222,7 @@ export default class Inibase {
|
|
|
3204
3222
|
// ops) and publish the empty row count in one journaled commit.
|
|
3205
3223
|
const renameList = [];
|
|
3206
3224
|
try {
|
|
3207
|
-
if (this.
|
|
3225
|
+
if (this.activeTransaction)
|
|
3208
3226
|
await this.ensureTxnLock(tableName);
|
|
3209
3227
|
else
|
|
3210
3228
|
await File.lock(join(tablePath, ".tmp"));
|
|
@@ -3217,7 +3235,7 @@ export default class Inibase {
|
|
|
3217
3235
|
from: paginationFilePath,
|
|
3218
3236
|
to: join(tablePath, `${lastId}-0.pagination`),
|
|
3219
3237
|
};
|
|
3220
|
-
if (this.
|
|
3238
|
+
if (this.activeTransaction) {
|
|
3221
3239
|
await this.stageTxnOp(tableName, renameList, pagination);
|
|
3222
3240
|
txnStaged = true;
|
|
3223
3241
|
const stagedEntry = this.txnTableEntry(tableName);
|
|
@@ -3238,7 +3256,7 @@ export default class Inibase {
|
|
|
3238
3256
|
return true;
|
|
3239
3257
|
}
|
|
3240
3258
|
finally {
|
|
3241
|
-
if (this.
|
|
3259
|
+
if (this.activeTransaction) {
|
|
3242
3260
|
if (!txnStaged && renameList.length)
|
|
3243
3261
|
await Promise.allSettled(renameList
|
|
3244
3262
|
.filter((pair) => Boolean(pair[0]))
|
|
@@ -3268,7 +3286,7 @@ export default class Inibase {
|
|
|
3268
3286
|
const renameList = [];
|
|
3269
3287
|
let txnStaged = false;
|
|
3270
3288
|
try {
|
|
3271
|
-
if (this.
|
|
3289
|
+
if (this.activeTransaction)
|
|
3272
3290
|
await this.ensureTxnLock(tableName);
|
|
3273
3291
|
else
|
|
3274
3292
|
await File.lock(join(tablePath, ".tmp"));
|
|
@@ -3276,12 +3294,12 @@ export default class Inibase {
|
|
|
3276
3294
|
const remaining = total - (Array.isArray(where) ? where.length : 1);
|
|
3277
3295
|
if (total && remaining > 0) {
|
|
3278
3296
|
this.idDensity.set(tableName, false);
|
|
3279
|
-
await Promise.allSettled(files.map(async (file) => renameList.push(await File.remove(join(tablePath, file), where))));
|
|
3297
|
+
await settleWrites(await Promise.allSettled(files.map(async (file) => renameList.push(await File.remove(join(tablePath, file), where)))));
|
|
3280
3298
|
const pagination = {
|
|
3281
3299
|
from: paginationFilePath,
|
|
3282
3300
|
to: join(tablePath, `${lastId}-${remaining}.pagination`),
|
|
3283
3301
|
};
|
|
3284
|
-
if (this.
|
|
3302
|
+
if (this.activeTransaction) {
|
|
3285
3303
|
await this.stageTxnOp(tableName, renameList, pagination);
|
|
3286
3304
|
txnStaged = true;
|
|
3287
3305
|
const stagedEntry = this.txnTableEntry(tableName);
|
|
@@ -3302,7 +3320,7 @@ export default class Inibase {
|
|
|
3302
3320
|
from: paginationFilePath,
|
|
3303
3321
|
to: join(tablePath, `${lastId}-0.pagination`),
|
|
3304
3322
|
};
|
|
3305
|
-
if (this.
|
|
3323
|
+
if (this.activeTransaction) {
|
|
3306
3324
|
await this.stageTxnOp(tableName, truncateList, pagination);
|
|
3307
3325
|
txnStaged = true;
|
|
3308
3326
|
const stagedEntry = this.txnTableEntry(tableName);
|
|
@@ -3315,7 +3333,7 @@ export default class Inibase {
|
|
|
3315
3333
|
}
|
|
3316
3334
|
// Cache still describes the committed state while a
|
|
3317
3335
|
// transaction is open, so only clear it outside one.
|
|
3318
|
-
if (!this.
|
|
3336
|
+
if (!this.activeTransaction &&
|
|
3319
3337
|
globalConfig[this.databasePath].tables?.get(tableName)?.config.cache)
|
|
3320
3338
|
await this.clearCache(tableName);
|
|
3321
3339
|
// Cascade: rows in other tables referencing the deleted rows
|
|
@@ -3324,7 +3342,7 @@ export default class Inibase {
|
|
|
3324
3342
|
return true;
|
|
3325
3343
|
}
|
|
3326
3344
|
finally {
|
|
3327
|
-
if (this.
|
|
3345
|
+
if (this.activeTransaction) {
|
|
3328
3346
|
if (!txnStaged && renameList.length)
|
|
3329
3347
|
await Promise.allSettled(renameList
|
|
3330
3348
|
.filter((pair) => Boolean(pair[0]))
|
|
@@ -3350,7 +3368,7 @@ export default class Inibase {
|
|
|
3350
3368
|
// lines of a prepend table and remove the wrong row).
|
|
3351
3369
|
let inlineLock = false;
|
|
3352
3370
|
try {
|
|
3353
|
-
if (this.
|
|
3371
|
+
if (this.activeTransaction)
|
|
3354
3372
|
await this.ensureTxnLock(tableName);
|
|
3355
3373
|
else {
|
|
3356
3374
|
await File.lock(join(tablePath, ".tmp"));
|
|
@@ -3424,7 +3442,7 @@ export default class Inibase {
|
|
|
3424
3442
|
// Unreadable/unsupported column -> skip this reference.
|
|
3425
3443
|
// Inside a transaction a broken reference must abort the
|
|
3426
3444
|
// whole cascade (all-or-nothing).
|
|
3427
|
-
if (this.
|
|
3445
|
+
if (this.activeTransaction)
|
|
3428
3446
|
throw error;
|
|
3429
3447
|
}
|
|
3430
3448
|
}
|
|
@@ -3444,7 +3462,7 @@ export default class Inibase {
|
|
|
3444
3462
|
// Cascade is best-effort outside a transaction: never break
|
|
3445
3463
|
// the parent delete. Inside a transaction a cascade failure
|
|
3446
3464
|
// must abort the whole txn (all-or-nothing).
|
|
3447
|
-
if (this.
|
|
3465
|
+
if (this.activeTransaction)
|
|
3448
3466
|
throw error;
|
|
3449
3467
|
}
|
|
3450
3468
|
}
|
|
@@ -3503,7 +3521,9 @@ export default class Inibase {
|
|
|
3503
3521
|
const targetFields = new Map();
|
|
3504
3522
|
for (const column of columns) {
|
|
3505
3523
|
const dot = column.indexOf(".");
|
|
3506
|
-
if (dot <= 0 ||
|
|
3524
|
+
if (dot <= 0 ||
|
|
3525
|
+
dot === column.length - 1 ||
|
|
3526
|
+
column.indexOf(".", dot + 1) !== -1)
|
|
3507
3527
|
throw this.createError("INVALID_PARAMETERS", [
|
|
3508
3528
|
`sum nested: '${column}' must be a single-level child path of an array-of-objects column`,
|
|
3509
3529
|
]);
|
|
@@ -3629,7 +3649,8 @@ export default class Inibase {
|
|
|
3629
3649
|
let matches = true;
|
|
3630
3650
|
for (const p of elementPredicates) {
|
|
3631
3651
|
const pv = predCells.get(p.field.key)?.[Number(lineStr)]?.[i];
|
|
3632
|
-
if (pv === undefined ||
|
|
3652
|
+
if (pv === undefined ||
|
|
3653
|
+
!UtilsServer.compare(p.operator, pv, p.comparedValue, p.field.type)) {
|
|
3633
3654
|
matches = false;
|
|
3634
3655
|
break;
|
|
3635
3656
|
}
|