inibase 3.6.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 +0 -8
- package/dist/index.js +50 -86
- 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
|
@@ -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;
|
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
|
|
@@ -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
|
}
|
|
@@ -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) &&
|
|
@@ -2768,7 +2728,11 @@ export default class Inibase {
|
|
|
2768
2728
|
}
|
|
2769
2729
|
}
|
|
2770
2730
|
if (!lineNumbers) {
|
|
2771
|
-
[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);
|
|
2772
2736
|
}
|
|
2773
2737
|
if (!lineNumbers)
|
|
2774
2738
|
return null;
|
|
@@ -2915,10 +2879,10 @@ export default class Inibase {
|
|
|
2915
2879
|
? clonedData.toReversed()
|
|
2916
2880
|
: clonedData
|
|
2917
2881
|
: clonedData);
|
|
2918
|
-
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
|
|
2919
2883
|
.prepend
|
|
2920
2884
|
? await File.prepend(path, content)
|
|
2921
|
-
: await File.append(path, content))));
|
|
2885
|
+
: await File.append(path, content)))));
|
|
2922
2886
|
const newTotal = _totalItems + (Array.isArray(data) ? data.length : 1);
|
|
2923
2887
|
const pagination = {
|
|
2924
2888
|
from: paginationFilePath,
|
|
@@ -3060,7 +3024,7 @@ export default class Inibase {
|
|
|
3060
3024
|
const evaluated = await this.evaluateComputedRows(tableName, computedPlan, mergedRows);
|
|
3061
3025
|
this.mergeComputedLineRecords(tableName, computedPlan, evaluated, pathesContents, existing);
|
|
3062
3026
|
}
|
|
3063
|
-
await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content, total))));
|
|
3027
|
+
await settleWrites(await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content, total)))));
|
|
3064
3028
|
if (this.activeTransaction) {
|
|
3065
3029
|
// Stage instead of publishing: row count is unchanged so
|
|
3066
3030
|
// there is no pagination rename to journal.
|
|
@@ -3157,7 +3121,7 @@ export default class Inibase {
|
|
|
3157
3121
|
}
|
|
3158
3122
|
this.mergeComputedLineRecords(tableName, computedPlan, evaluated, pathesContents, existing);
|
|
3159
3123
|
}
|
|
3160
|
-
await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content))));
|
|
3124
|
+
await settleWrites(await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content)))));
|
|
3161
3125
|
if (this.activeTransaction) {
|
|
3162
3126
|
await this.stageTxnOp(tableName, renameList, null);
|
|
3163
3127
|
txnStaged = true;
|
|
@@ -3330,7 +3294,7 @@ export default class Inibase {
|
|
|
3330
3294
|
const remaining = total - (Array.isArray(where) ? where.length : 1);
|
|
3331
3295
|
if (total && remaining > 0) {
|
|
3332
3296
|
this.idDensity.set(tableName, false);
|
|
3333
|
-
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)))));
|
|
3334
3298
|
const pagination = {
|
|
3335
3299
|
from: paginationFilePath,
|
|
3336
3300
|
to: join(tablePath, `${lastId}-${remaining}.pagination`),
|