inibase 3.6.0 → 3.7.1
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 +21 -3
- package/dist/file.d.ts +26 -5
- package/dist/file.js +132 -32
- package/dist/format.d.ts +54 -0
- package/dist/format.js +108 -0
- package/dist/index.d.ts +114 -9
- package/dist/index.js +697 -108
- package/dist/overlay.d.ts +59 -0
- package/dist/overlay.js +106 -0
- package/dist/updatelog.d.ts +115 -0
- package/dist/updatelog.js +377 -0
- package/dist/utils.js +16 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,22 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
> A file-based & memory-efficient, serverless relational database with **crash-atomic ACID: single-table DML + multi-table transactions** :fire: — per-table writer locks, write-ahead journal + crash recovery, fsync-backed durability, and a `begin`/`commit`/`rollback` transaction API (exact scope in [Durability & crash safety](#durability--crash-safety)).
|
|
4
4
|
|
|
5
|
-
[](#usage)
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.org/package/inibase) [](./LICENSE) [](https://github.com/inicontent/inibase/pulse) [](https://github.com/inicontent/inibase)
|
|
8
8
|
|
|
9
|
+
**[Quick start](#usage) · [How it works](#how-it-works) · [Durability](#durability--crash-safety) · [Benchmarks](#benchmark)**
|
|
10
|
+
|
|
11
|
+
## Small footprint. Real benchmark results.
|
|
12
|
+
|
|
13
|
+
Inibase is a file-based TypeScript database designed for **fast data access and low memory usage**, with column selection, built-in caching, and optional gzip compression.
|
|
14
|
+
|
|
15
|
+
In the **100,000-record CMS benchmark**, Inibase **3.7.0 with gzip** used **1.64 MiB of allocated disk** — **89.4% less than LevelDB** and **98.7% less than SQLite**. Reading two selected columns took **44.3 ms** in the same workload.
|
|
16
|
+
|
|
17
|
+
[](https://github.com/inicontent/benchmarks/blob/a6c4a644aa5472192fefefe0f3d93b56edc9726a/results/large-cms/REPORT.md)
|
|
18
|
+
|
|
19
|
+
*Source: [inicontent/benchmarks](https://github.com/inicontent/benchmarks/blob/a6c4a644aa5472192fefefe0f3d93b56edc9726a/results/large-cms/REPORT.md), median of three trials on Apple M5 with repetitive synthetic CMS content and full durability. The chart measures disk storage, not RAM. Results depend on the workload; indexed competitors were faster for key lookups.*
|
|
20
|
+
|
|
21
|
+
[Explore the full results](https://github.com/inicontent/benchmarks/blob/a6c4a644aa5472192fefefe0f3d93b56edc9726a/results/large-cms/REPORT.md) · [Read the methodology](https://github.com/inicontent/benchmarks/blob/a6c4a644aa5472192fefefe0f3d93b56edc9726a/METHODOLOGY.md)
|
|
22
|
+
|
|
9
23
|
## Features
|
|
10
24
|
|
|
11
25
|
- **Lightweight** 🪶
|
|
12
26
|
- **Minimalist** :white_circle: (but powerful)
|
|
13
27
|
- **100% TypeScript** :large_blue_diamond:
|
|
14
|
-
- **
|
|
28
|
+
- **Built for fast access** :zap: Built-in caching and column projection
|
|
15
29
|
- **ATOMIC** :lock: Per-table writer locks, write-ahead journal + crash recovery, fsync-backed durability, and multi-table transactions (`begin`/`commit`/`rollback`) for atomic cascades (exact scope below)
|
|
16
30
|
- **Built-in** form validation (+unique values :new: ) :sunglasses:
|
|
17
31
|
- **Suitable for large data** :page_with_curl: (tested with 4M records)
|
|
18
32
|
- **Support Compression** :eight_spoked_asterisk: (using built-in nodejs zlib)
|
|
19
33
|
- **Support Table Joins** :link:
|
|
20
|
-
- **
|
|
34
|
+
- **Memory-conscious design** :chart_with_downwards_trend: File-based storage and streaming reads
|
|
21
35
|
- **Safe** :lock: (no sql or javascript injections)
|
|
22
36
|
- **Easy to use** :bread:
|
|
23
37
|
- **...** and much more :rocket:
|
|
@@ -1086,6 +1100,10 @@ are derived first, in dependency order.
|
|
|
1086
1100
|
|
|
1087
1101
|
### Computed fields
|
|
1088
1102
|
|
|
1103
|
+

|
|
1104
|
+
|
|
1105
|
+
*Charted from the results below, with fsync and journaling enabled. These are recorded sample measurements, not a new benchmark run; hardware and version were not recorded with this table.*
|
|
1106
|
+
|
|
1089
1107
|
Write-time evaluation cost (helpers `sum(3 * 4)`, `avg(4)`, `min(4)`, `max(4)`, `count(3)` over an `items` array with 3 lines, plus an arithmetic field `itemTotal × (314 / 100)`), compared against the identical table without computed fields:
|
|
1090
1108
|
|
|
1091
1109
|
| rows | POST bulk (plain / computed) | POST single (plain / computed) | PUT recompute (plain / computed) |
|
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,9 +160,16 @@ 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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
}, limit?: number, offset?: number, readWholeFile?: boolean, earlyStop?: boolean,
|
|
164
|
+
/**
|
|
165
|
+
* Per-line raw cell overrides (update-log overlay). When a line is present
|
|
166
|
+
* the overridden raw value is decoded and compared instead of the file's
|
|
167
|
+
* line, so a search reflects un-compacted `put`s. The native whole-line
|
|
168
|
+
* grep fast path is bypassed whenever overrides are supplied.
|
|
169
|
+
*/
|
|
170
|
+
overrides?: Map<number, string | number | boolean | null>) => Promise<[Record<number, string | number | boolean | null | undefined | (string | number | boolean | null)[]> | null, number, Set<number> | null]>;
|
|
171
|
+
export declare const sum: (fp: string, ln?: number | number[], overrides?: Map<number, string | number | boolean | null>) => Promise<number>;
|
|
172
|
+
export declare const avg: (fp: string, ln?: number | number[], overrides?: Map<number, string | number | boolean | null>) => Promise<number>;
|
|
173
|
+
export declare const min: (fp: string, ln?: number | number[], overrides?: Map<number, string | number | boolean | null>) => Promise<number>;
|
|
174
|
+
export declare const max: (fp: string, ln?: number | number[], overrides?: Map<number, string | number | boolean | null>) => Promise<number>;
|
|
154
175
|
export declare const getFileDate: (path: string) => Promise<Date>;
|
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,9 +1169,17 @@ 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,
|
|
1173
|
+
/**
|
|
1174
|
+
* Per-line raw cell overrides (update-log overlay). When a line is present
|
|
1175
|
+
* the overridden raw value is decoded and compared instead of the file's
|
|
1176
|
+
* line, so a search reflects un-compacted `put`s. The native whole-line
|
|
1177
|
+
* grep fast path is bypassed whenever overrides are supplied.
|
|
1178
|
+
*/
|
|
1179
|
+
overrides) => {
|
|
1098
1180
|
// Native fast path for exact-equality searches (whole-line `grep -x -F`).
|
|
1099
|
-
if (
|
|
1181
|
+
if (!overrides?.size &&
|
|
1182
|
+
operator === "=" &&
|
|
1100
1183
|
!Array.isArray(operator) &&
|
|
1101
1184
|
!logicalOperator &&
|
|
1102
1185
|
comparedAtValue !== null &&
|
|
@@ -1116,6 +1199,7 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
|
|
|
1116
1199
|
let linesCount = 0;
|
|
1117
1200
|
const linesNumbers = new Set();
|
|
1118
1201
|
let fileHandle = null;
|
|
1202
|
+
let destroyStreams = () => { };
|
|
1119
1203
|
const config = field ?? {
|
|
1120
1204
|
key: "BLABLA",
|
|
1121
1205
|
type: "string",
|
|
@@ -1131,7 +1215,8 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
|
|
|
1131
1215
|
// Open the file for reading.
|
|
1132
1216
|
fileHandle = await open(filePath, "r");
|
|
1133
1217
|
// Create a Readline interface to read the file line by line.
|
|
1134
|
-
const rl =
|
|
1218
|
+
const { rl, destroy } = openReadline(filePath, fileHandle);
|
|
1219
|
+
destroyStreams = destroy;
|
|
1135
1220
|
// Iterate through each line in the file.
|
|
1136
1221
|
for await (const line of rl) {
|
|
1137
1222
|
// Increment the line count for each line.
|
|
@@ -1140,8 +1225,11 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
|
|
|
1140
1225
|
if (searchIn?.size &&
|
|
1141
1226
|
(!searchIn.has(linesCount) || searchIn.has(-linesCount)))
|
|
1142
1227
|
continue;
|
|
1143
|
-
// Decode the line for comparison.
|
|
1144
|
-
|
|
1228
|
+
// Decode the line for comparison. A per-line overlay override (raw
|
|
1229
|
+
// cell) wins over the file content so searches see un-compacted puts.
|
|
1230
|
+
const decodedLine = decode(overrides?.has(linesCount)
|
|
1231
|
+
? overrides.get(linesCount)
|
|
1232
|
+
: line, config);
|
|
1145
1233
|
// Check if the line meets the specified conditions based on comparison and logical operators.
|
|
1146
1234
|
const doesMeetCondition = (Array.isArray(decodedLine) &&
|
|
1147
1235
|
operator !== "=" &&
|
|
@@ -1162,6 +1250,10 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
|
|
|
1162
1250
|
}
|
|
1163
1251
|
// Store the decoded line in the result object.
|
|
1164
1252
|
matchingLines[linesCount] = decodedLine;
|
|
1253
|
+
// Every requested match is now in hand: stop instead of
|
|
1254
|
+
// scanning the rest of the column (id -> line resolution).
|
|
1255
|
+
if (earlyStop && limit && linesNumbers.size >= limit)
|
|
1256
|
+
break;
|
|
1165
1257
|
}
|
|
1166
1258
|
}
|
|
1167
1259
|
// Convert the Map to an object using Object.fromEntries and return the result.
|
|
@@ -1173,7 +1265,9 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
|
|
|
1173
1265
|
return [null, 0, null];
|
|
1174
1266
|
}
|
|
1175
1267
|
finally {
|
|
1176
|
-
//
|
|
1268
|
+
// Tear down the read streams (an early `break` must not leave a handle
|
|
1269
|
+
// open) and then the file handle, even if an error occurred.
|
|
1270
|
+
destroyStreams();
|
|
1177
1271
|
await fileHandle?.close();
|
|
1178
1272
|
}
|
|
1179
1273
|
};
|
|
@@ -1187,7 +1281,7 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
|
|
|
1187
1281
|
*
|
|
1188
1282
|
* @returns Promise<number> The requested metric, or 0 if no numeric value found.
|
|
1189
1283
|
*/
|
|
1190
|
-
async function reduceNumbers(filePath, wanted = "sum", lineNumbers) {
|
|
1284
|
+
async function reduceNumbers(filePath, wanted = "sum", lineNumbers, overrides) {
|
|
1191
1285
|
/* optional subset */
|
|
1192
1286
|
const filter = lineNumbers
|
|
1193
1287
|
? new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers])
|
|
@@ -1207,7 +1301,13 @@ async function reduceNumbers(filePath, wanted = "sum", lineNumbers) {
|
|
|
1207
1301
|
/* skip unwanted lines */
|
|
1208
1302
|
if (filter && !filter.has(line))
|
|
1209
1303
|
continue;
|
|
1210
|
-
|
|
1304
|
+
// A per-line overlay override (raw cell) wins over the base file so
|
|
1305
|
+
// aggregates reflect un-compacted puts.
|
|
1306
|
+
const raw = overrides?.has(line) ? overrides.get(line) : txt;
|
|
1307
|
+
const num = Number(decode(raw, {
|
|
1308
|
+
key: "BLABLA",
|
|
1309
|
+
type: "number",
|
|
1310
|
+
}));
|
|
1211
1311
|
if (Number.isNaN(num))
|
|
1212
1312
|
continue;
|
|
1213
1313
|
processed++;
|
|
@@ -1241,10 +1341,10 @@ async function reduceNumbers(filePath, wanted = "sum", lineNumbers) {
|
|
|
1241
1341
|
: max;
|
|
1242
1342
|
}
|
|
1243
1343
|
/* Optional convenience wrappers (signatures unchanged) */
|
|
1244
|
-
export const sum = (fp, ln) => reduceNumbers(fp, "sum", ln);
|
|
1245
|
-
export const avg = (fp, ln) => reduceNumbers(fp, "avg", ln);
|
|
1246
|
-
export const min = (fp, ln) => reduceNumbers(fp, "min", ln);
|
|
1247
|
-
export const max = (fp, ln) => reduceNumbers(fp, "max", ln);
|
|
1344
|
+
export const sum = (fp, ln, overrides) => reduceNumbers(fp, "sum", ln, overrides);
|
|
1345
|
+
export const avg = (fp, ln, overrides) => reduceNumbers(fp, "avg", ln, overrides);
|
|
1346
|
+
export const min = (fp, ln, overrides) => reduceNumbers(fp, "min", ln, overrides);
|
|
1347
|
+
export const max = (fp, ln, overrides) => reduceNumbers(fp, "max", ln, overrides);
|
|
1248
1348
|
export const getFileDate = (path) => stat(path)
|
|
1249
1349
|
.then((s) => s.mtime || s.birthtime)
|
|
1250
1350
|
.catch(() => new Date());
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk format marker for a table directory.
|
|
3
|
+
*
|
|
4
|
+
* Format 1 is the implicit legacy format (no `.format` file): base columns plus
|
|
5
|
+
* pagination only. Format 2 opts the table into the append-only update log
|
|
6
|
+
* written by `put` (see `updatelog.ts`).
|
|
7
|
+
*
|
|
8
|
+
* The marker is the single source of truth for "does this table have a log?".
|
|
9
|
+
* It also carries the compatibility contract read by a later 3.7.1 detector:
|
|
10
|
+
* `minReader` is the oldest engine that may open the table, `detector` the
|
|
11
|
+
* release that hard-fails instead of silently mis-reading. `baseGen` is the
|
|
12
|
+
* generation the base columns were last compacted at; it is bumped by
|
|
13
|
+
* compaction (3.9) so a stale in-memory overlay can detect that its base moved
|
|
14
|
+
* and rebuild instead of replaying frames that are already merged.
|
|
15
|
+
*/
|
|
16
|
+
export declare const FORMAT_FILE = ".format";
|
|
17
|
+
export interface TableFormat {
|
|
18
|
+
format: number;
|
|
19
|
+
log: boolean;
|
|
20
|
+
baseGen: number;
|
|
21
|
+
/** Oldest engine that may open the table, as written into the marker. */
|
|
22
|
+
minReader?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const LEGACY_FORMAT: TableFormat;
|
|
25
|
+
export declare const CURRENT_FORMAT_VERSION = 2;
|
|
26
|
+
export declare const ENGINE_NAME = "inibase";
|
|
27
|
+
/** This build's engine version, compared against a marker's `minReader`. */
|
|
28
|
+
export declare const ENGINE_VERSION = "3.8.0";
|
|
29
|
+
/** Oldest reader that understands format 2. */
|
|
30
|
+
export declare const MIN_READER = "3.8.0";
|
|
31
|
+
/** Release expected to detect (and refuse) format 2 rather than mis-read it. */
|
|
32
|
+
export declare const DETECTOR = "3.7.1";
|
|
33
|
+
/** Compare dotted numeric versions: >0 when `a > b`, <0 when `a < b`, else 0. */
|
|
34
|
+
export declare const compareVersions: (a: string, b: string) => number;
|
|
35
|
+
/**
|
|
36
|
+
* Why this engine cannot open a marker, or null when it can.
|
|
37
|
+
*
|
|
38
|
+
* Refusing *before* any column is read keeps a newer-format table from being
|
|
39
|
+
* silently mis-read as format 2 (or legacy). Two independent conditions:
|
|
40
|
+
* a `format` above what this build writes, or a `minReader` above the running
|
|
41
|
+
* engine version.
|
|
42
|
+
*/
|
|
43
|
+
export declare const unsupportedFormatReason: (format: TableFormat) => string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Read a table's format marker. A missing or unreadable marker is legacy
|
|
46
|
+
* (format 1), which keeps every pre-3.8 table byte-for-byte compatible.
|
|
47
|
+
*/
|
|
48
|
+
export declare const readFormat: (tablePath: string) => Promise<TableFormat>;
|
|
49
|
+
/** The exact bytes of a format marker (shared by write and compaction commit). */
|
|
50
|
+
export declare const serializeFormat: (format: TableFormat) => string;
|
|
51
|
+
/** Write (or replace) the format marker, fsync-backed. */
|
|
52
|
+
export declare const writeFormat: (tablePath: string, format: TableFormat) => Promise<void>;
|
|
53
|
+
/** Remove the marker (used when downgrading a table back to legacy). */
|
|
54
|
+
export declare const removeFormat: (tablePath: string) => Promise<void>;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as File from "./file.js";
|
|
4
|
+
/**
|
|
5
|
+
* On-disk format marker for a table directory.
|
|
6
|
+
*
|
|
7
|
+
* Format 1 is the implicit legacy format (no `.format` file): base columns plus
|
|
8
|
+
* pagination only. Format 2 opts the table into the append-only update log
|
|
9
|
+
* written by `put` (see `updatelog.ts`).
|
|
10
|
+
*
|
|
11
|
+
* The marker is the single source of truth for "does this table have a log?".
|
|
12
|
+
* It also carries the compatibility contract read by a later 3.7.1 detector:
|
|
13
|
+
* `minReader` is the oldest engine that may open the table, `detector` the
|
|
14
|
+
* release that hard-fails instead of silently mis-reading. `baseGen` is the
|
|
15
|
+
* generation the base columns were last compacted at; it is bumped by
|
|
16
|
+
* compaction (3.9) so a stale in-memory overlay can detect that its base moved
|
|
17
|
+
* and rebuild instead of replaying frames that are already merged.
|
|
18
|
+
*/
|
|
19
|
+
export const FORMAT_FILE = ".format";
|
|
20
|
+
export const LEGACY_FORMAT = {
|
|
21
|
+
format: 1,
|
|
22
|
+
log: false,
|
|
23
|
+
baseGen: 0,
|
|
24
|
+
};
|
|
25
|
+
export const CURRENT_FORMAT_VERSION = 2;
|
|
26
|
+
export const ENGINE_NAME = "inibase";
|
|
27
|
+
/** This build's engine version, compared against a marker's `minReader`. */
|
|
28
|
+
export const ENGINE_VERSION = "3.8.0";
|
|
29
|
+
/** Oldest reader that understands format 2. */
|
|
30
|
+
export const MIN_READER = "3.8.0";
|
|
31
|
+
/** Release expected to detect (and refuse) format 2 rather than mis-read it. */
|
|
32
|
+
export const DETECTOR = "3.7.1";
|
|
33
|
+
/** Compare dotted numeric versions: >0 when `a > b`, <0 when `a < b`, else 0. */
|
|
34
|
+
export const compareVersions = (a, b) => {
|
|
35
|
+
const pa = a.split(".");
|
|
36
|
+
const pb = b.split(".");
|
|
37
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
38
|
+
const diff = (Number(pa[i]) || 0) - (Number(pb[i]) || 0);
|
|
39
|
+
if (diff !== 0)
|
|
40
|
+
return diff > 0 ? 1 : -1;
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Why this engine cannot open a marker, or null when it can.
|
|
46
|
+
*
|
|
47
|
+
* Refusing *before* any column is read keeps a newer-format table from being
|
|
48
|
+
* silently mis-read as format 2 (or legacy). Two independent conditions:
|
|
49
|
+
* a `format` above what this build writes, or a `minReader` above the running
|
|
50
|
+
* engine version.
|
|
51
|
+
*/
|
|
52
|
+
export const unsupportedFormatReason = (format) => {
|
|
53
|
+
if (format.format > CURRENT_FORMAT_VERSION)
|
|
54
|
+
return `format ${format.format} (this engine reads up to ${CURRENT_FORMAT_VERSION})`;
|
|
55
|
+
if (format.minReader && compareVersions(format.minReader, ENGINE_VERSION) > 0)
|
|
56
|
+
return `minReader ${format.minReader} (this engine is ${ENGINE_VERSION})`;
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
const formatPath = (tablePath) => join(tablePath, FORMAT_FILE);
|
|
60
|
+
/**
|
|
61
|
+
* Read a table's format marker. A missing or unreadable marker is legacy
|
|
62
|
+
* (format 1), which keeps every pre-3.8 table byte-for-byte compatible.
|
|
63
|
+
*/
|
|
64
|
+
export const readFormat = async (tablePath) => {
|
|
65
|
+
if (!(await File.isExists(formatPath(tablePath))))
|
|
66
|
+
return { ...LEGACY_FORMAT };
|
|
67
|
+
try {
|
|
68
|
+
const raw = JSON.parse(await readFile(formatPath(tablePath), "utf8"));
|
|
69
|
+
return {
|
|
70
|
+
// Accept both the current `format` key and the earlier `version`
|
|
71
|
+
// spelling so a marker written by any 3.8 prerelease still opens.
|
|
72
|
+
format: Number(raw?.format ?? raw?.version) || LEGACY_FORMAT.format,
|
|
73
|
+
log: raw?.log === true,
|
|
74
|
+
baseGen: Number(raw?.baseGen) || 0,
|
|
75
|
+
minReader: typeof raw?.minReader === "string" ? raw.minReader : undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// A corrupt marker means we cannot trust "no log": refusing to write
|
|
80
|
+
// over it is safer, but reading must still work, so treat as legacy and
|
|
81
|
+
// let the write path surface UNSUPPORTED_FORMAT.
|
|
82
|
+
return { ...LEGACY_FORMAT };
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/** The exact bytes of a format marker (shared by write and compaction commit). */
|
|
86
|
+
export const serializeFormat = (format) => `${JSON.stringify({
|
|
87
|
+
engine: ENGINE_NAME,
|
|
88
|
+
format: format.format,
|
|
89
|
+
minReader: MIN_READER,
|
|
90
|
+
detector: DETECTOR,
|
|
91
|
+
layout: "append",
|
|
92
|
+
log: format.log,
|
|
93
|
+
baseGen: format.baseGen,
|
|
94
|
+
})}\n`;
|
|
95
|
+
/** Write (or replace) the format marker, fsync-backed. */
|
|
96
|
+
export const writeFormat = async (tablePath, format) => {
|
|
97
|
+
await File.write(formatPath(tablePath), serializeFormat(format));
|
|
98
|
+
};
|
|
99
|
+
/** Remove the marker (used when downgrading a table back to legacy). */
|
|
100
|
+
export const removeFormat = async (tablePath) => {
|
|
101
|
+
const path = formatPath(tablePath);
|
|
102
|
+
try {
|
|
103
|
+
await unlink(path);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Already absent.
|
|
107
|
+
}
|
|
108
|
+
};
|