inibase 3.7.1 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -1
- package/dist/format.d.ts +7 -1
- package/dist/format.js +21 -3
- package/dist/index.js +28 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ In the **100,000-record CMS benchmark**, Inibase **3.7.0 with gzip** used **1.64
|
|
|
29
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)
|
|
30
30
|
- **Built-in** form validation (+unique values :new: ) :sunglasses:
|
|
31
31
|
- **Suitable for large data** :page_with_curl: (tested with 4M records)
|
|
32
|
+
- **Incremental updates** :recycle: Opt-in update log (`log: true`) appends small frames instead of rewriting whole columns
|
|
32
33
|
- **Support Compression** :eight_spoked_asterisk: (using built-in nodejs zlib)
|
|
33
34
|
- **Support Table Joins** :link:
|
|
34
35
|
- **Memory-conscious design** :chart_with_downwards_trend: File-based storage and streaming reads
|
|
@@ -84,7 +85,7 @@ const users = await db.get("user", { favoriteFoods: "![]Pizza,Burger" });
|
|
|
84
85
|
|
|
85
86
|
- **POST**: New data is appended to column files efficiently.
|
|
86
87
|
- **GET**: Data retrieval is optimized by reading files line-by-line.
|
|
87
|
-
- **PUT**: Updates are streamlined
|
|
88
|
+
- **PUT**: Updates are streamlined: on a plain table only the relevant column file is rewritten, while a table with the [update log](#incremental-update-log) enabled appends a small frame instead.
|
|
88
89
|
- **DELETE**: Removes lines from column files for swift deletion.
|
|
89
90
|
|
|
90
91
|
This structure ensures efficient storage, retrieval, and updates, making our system scalable and high-performing for diverse datasets and applications.
|
|
@@ -181,6 +182,50 @@ live-reader and stale-lock test suite, `pnpm test:transaction` for the
|
|
|
181
182
|
transaction/cascade/crash-recovery suite, and `pnpm benchmark:durability` for
|
|
182
183
|
the `full` vs `none` throughput comparison.
|
|
183
184
|
|
|
185
|
+
## Incremental update log
|
|
186
|
+
|
|
187
|
+
Opt a table into the append-only **update log** (on-disk format 2) to make `put`
|
|
188
|
+
cheap when a table is large but each update touches few rows:
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
await db.createTable("user", userTableSchema, { log: true });
|
|
192
|
+
// or migrate an existing table in place:
|
|
193
|
+
await db.updateTable("user", undefined, { log: true });
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- **Writes.** `put` appends one length-prefixed, `crc32`-checked frame
|
|
197
|
+
(`{ id, cols, updatedAt }`) to `updates.log` instead of rewriting column
|
|
198
|
+
files, so the fast path is proportional to the number of updated rows rather
|
|
199
|
+
than the table size. `post` and `delete` still rewrite the base columns.
|
|
200
|
+
- **Reads.** Every read — `get`, criteria, `columns`, sort, pagination,
|
|
201
|
+
aggregates, and uniqueness checks — merges the log over the base columns, so
|
|
202
|
+
results match an un-logged table. With an empty log the merge is a no-op.
|
|
203
|
+
- **Compaction.** `await db.compact(tableName)` folds the log into the base
|
|
204
|
+
columns and resets it, returning the new `baseGen` — or `null` when there was
|
|
205
|
+
nothing to fold. Compaction is crash-safe and semantically neutral: it only
|
|
206
|
+
materializes already-acknowledged updates. It also runs automatically under
|
|
207
|
+
backpressure so the overlay and log stay bounded
|
|
208
|
+
(`INIBASE_MAX_OVERLAY_RECORDS`, default `100000`; `INIBASE_MAX_LOG_BYTES`,
|
|
209
|
+
default 64 MiB); if the bound is hit and compaction cannot run, the write
|
|
210
|
+
fails with `MAINTENANCE_REQUIRED` rather than growing without limit.
|
|
211
|
+
- **Requirements.** The table must use the append layout (no `prepend`) and a
|
|
212
|
+
schema of simple top-level fields. Transactions, schema changes, and
|
|
213
|
+
disabling the log on a log table throw `MAINTENANCE_REQUIRED` for now (they
|
|
214
|
+
need compaction rewiring, planned for 3.9).
|
|
215
|
+
- **On disk.** A `.format` marker records `format`, `baseGen`, and the
|
|
216
|
+
compatibility contract (`minReader`); `updates.log` holds the frames. A table
|
|
217
|
+
with no `.format` is legacy format 1 and behaves exactly as before.
|
|
218
|
+
- **Downgrade safety.** A reader older than the log format ignores the marker
|
|
219
|
+
and would silently serve **stale** base columns. The marker's `minReader`
|
|
220
|
+
makes a conforming engine refuse such a table with `UNSUPPORTED_FORMAT`
|
|
221
|
+
instead of mis-reading it.
|
|
222
|
+
|
|
223
|
+
> [!WARNING]
|
|
224
|
+
> **Backups.** A raw directory copy is only a valid backup when writes are
|
|
225
|
+
> quiesced or the copy is a filesystem snapshot. Copying a logged table *during*
|
|
226
|
+
> writes can capture base columns paired with a mismatched `updates.log` /
|
|
227
|
+
> `baseGen`. Running `compact` first does not remove the need to quiesce writes.
|
|
228
|
+
|
|
184
229
|
## Inibase CLI
|
|
185
230
|
|
|
186
231
|
```shell
|
package/dist/format.d.ts
CHANGED
|
@@ -48,7 +48,13 @@ export declare const unsupportedFormatReason: (format: TableFormat) => string |
|
|
|
48
48
|
export declare const readFormat: (tablePath: string) => Promise<TableFormat>;
|
|
49
49
|
/** The exact bytes of a format marker (shared by write and compaction commit). */
|
|
50
50
|
export declare const serializeFormat: (format: TableFormat) => string;
|
|
51
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Write (or replace) the format marker durably and atomically (§9): stage the
|
|
53
|
+
* bytes in the table's `.tmp` dir, fsync the temp, rename it into place, then
|
|
54
|
+
* fsync the table directory. A crash can therefore leave either the old marker
|
|
55
|
+
* or the new one, never a partial file — a torn marker would read back as
|
|
56
|
+
* legacy and silently drop the log.
|
|
57
|
+
*/
|
|
52
58
|
export declare const writeFormat: (tablePath: string, format: TableFormat) => Promise<void>;
|
|
53
59
|
/** Remove the marker (used when downgrading a table back to legacy). */
|
|
54
60
|
export declare const removeFormat: (tablePath: string) => Promise<void>;
|
package/dist/format.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, rename, unlink } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import * as File from "./file.js";
|
|
4
5
|
/**
|
|
@@ -92,9 +93,26 @@ export const serializeFormat = (format) => `${JSON.stringify({
|
|
|
92
93
|
log: format.log,
|
|
93
94
|
baseGen: format.baseGen,
|
|
94
95
|
})}\n`;
|
|
95
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Write (or replace) the format marker durably and atomically (§9): stage the
|
|
98
|
+
* bytes in the table's `.tmp` dir, fsync the temp, rename it into place, then
|
|
99
|
+
* fsync the table directory. A crash can therefore leave either the old marker
|
|
100
|
+
* or the new one, never a partial file — a torn marker would read back as
|
|
101
|
+
* legacy and silently drop the log.
|
|
102
|
+
*/
|
|
96
103
|
export const writeFormat = async (tablePath, format) => {
|
|
97
|
-
|
|
104
|
+
const path = formatPath(tablePath);
|
|
105
|
+
const tmp = join(tablePath, ".tmp", `${FORMAT_FILE}.${randomUUID()}`);
|
|
106
|
+
try {
|
|
107
|
+
await File.write(tmp, serializeFormat(format));
|
|
108
|
+
await File.syncFile(tmp);
|
|
109
|
+
await rename(tmp, path);
|
|
110
|
+
await File.syncDir(tablePath);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
await unlink(tmp).catch(() => { });
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
98
116
|
};
|
|
99
117
|
/** Remove the marker (used when downgrading a table back to legacy). */
|
|
100
118
|
export const removeFormat = async (tablePath) => {
|
package/dist/index.js
CHANGED
|
@@ -295,12 +295,14 @@ export default class Inibase {
|
|
|
295
295
|
throw this.createError("UNSUPPORTED_FORMAT", "append layout (no prepend)");
|
|
296
296
|
if (schema)
|
|
297
297
|
this.assertLogCompatibleSchema(schema);
|
|
298
|
-
|
|
298
|
+
// Publish the marker before the log: a missing log reads as an
|
|
299
|
+
// empty log, whereas a log without a marker would be legacy.
|
|
299
300
|
await writeFormat(tablePath, {
|
|
300
301
|
format: CURRENT_FORMAT_VERSION,
|
|
301
302
|
log: true,
|
|
302
303
|
baseGen: 0,
|
|
303
304
|
});
|
|
305
|
+
await File.write(join(tablePath, UPDATES_LOG_NAME), "");
|
|
304
306
|
this.tableFormats.set(tableName, {
|
|
305
307
|
format: CURRENT_FORMAT_VERSION,
|
|
306
308
|
log: true,
|
|
@@ -397,12 +399,31 @@ export default class Inibase {
|
|
|
397
399
|
throw this.createError("UNSUPPORTED_FORMAT", "append layout (no prepend)");
|
|
398
400
|
if (table.schema?.length)
|
|
399
401
|
this.assertLogCompatibleSchema(table.schema);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
402
|
+
// Durable migration (§9): publish the marker durably and atomically
|
|
403
|
+
// *before* the table is treated as log-format. Format 2 shares the
|
|
404
|
+
// format-1 base layout, so no column rewrite is needed. A missing
|
|
405
|
+
// `updates.log` reads as an empty log, so creating it after the
|
|
406
|
+
// marker is safe.
|
|
407
|
+
try {
|
|
408
|
+
await writeFormat(tablePath, {
|
|
409
|
+
format: CURRENT_FORMAT_VERSION,
|
|
410
|
+
log: true,
|
|
411
|
+
baseGen: 0,
|
|
412
|
+
});
|
|
413
|
+
await File.write(join(tablePath, UPDATES_LOG_NAME), "");
|
|
414
|
+
await File.syncDir(tablePath);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
// Re-read the marker to decide: if it never became visible the
|
|
418
|
+
// table is unchanged and must stay legacy; if it did become
|
|
419
|
+
// visible, keep treating it as log-format rather than writing
|
|
420
|
+
// through the legacy path over a log-format table.
|
|
421
|
+
const landed = await readFormat(tablePath);
|
|
422
|
+
if (!landed.log) {
|
|
423
|
+
this.tableFormats.delete(tableName);
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
406
427
|
this.tableFormats.set(tableName, {
|
|
407
428
|
format: CURRENT_FORMAT_VERSION,
|
|
408
429
|
log: true,
|