inibase 3.7.0 → 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 +67 -4
- package/dist/file.d.ts +12 -5
- package/dist/file.js +27 -10
- package/dist/format.d.ts +60 -0
- package/dist/format.js +126 -0
- package/dist/index.d.ts +114 -1
- package/dist/index.js +668 -22
- 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/dist/index.js
CHANGED
|
@@ -7,9 +7,21 @@ import { inspect } from "node:util";
|
|
|
7
7
|
import Inison from "inison";
|
|
8
8
|
import { buildFieldIndex, collectFieldDeps, parseExpression, resolveExpression, resolveFramePath, topoSortComputedFields, } from "./expression.js";
|
|
9
9
|
import * as File from "./file.js";
|
|
10
|
+
import { CURRENT_FORMAT_VERSION, FORMAT_FILE, readFormat, serializeFormat, unsupportedFormatReason, writeFormat, } from "./format.js";
|
|
10
11
|
import { DatabaseJournal, Journal } from "./journal.js";
|
|
12
|
+
import { TableOverlay } from "./overlay.js";
|
|
13
|
+
import { buildFrames, DEFAULT_MAX_FRAME_BYTES, UPDATES_LOG_NAME, } from "./updatelog.js";
|
|
11
14
|
import * as Utils from "./utils.js";
|
|
12
15
|
import * as UtilsServer from "./utils.server.js";
|
|
16
|
+
/**
|
|
17
|
+
* Default accumulation bounds for a format-2 table. Before each append the
|
|
18
|
+
* writer compacts when the log would exceed `maxLogBytes()` bytes or the
|
|
19
|
+
* overlay would exceed `maxOverlayRecords()` distinct rows; the log resets to
|
|
20
|
+
* empty on compaction, so these bound *between* compactions, not total writes.
|
|
21
|
+
* Both are overridable per deployment via environment for large tables.
|
|
22
|
+
*/
|
|
23
|
+
const maxLogBytes = () => Number(process.env.INIBASE_MAX_LOG_BYTES) || 64 * 1024 * 1024;
|
|
24
|
+
const maxOverlayRecords = () => Number(process.env.INIBASE_MAX_OVERLAY_RECORDS) || 100_000;
|
|
13
25
|
export const ERROR_CODES = [
|
|
14
26
|
"GROUP_UNIQUE",
|
|
15
27
|
"FIELD_UNIQUE",
|
|
@@ -33,6 +45,10 @@ export const ERROR_CODES = [
|
|
|
33
45
|
"COMPUTED_FIELD_SETTABLE",
|
|
34
46
|
"COMPUTED_FIELD_DANGLING_LINK",
|
|
35
47
|
"COMPUTED_FIELD_ARITHMETIC",
|
|
48
|
+
"UPDATE_LOG_CORRUPT",
|
|
49
|
+
"UPDATE_LOG_FRAME_TOO_LARGE",
|
|
50
|
+
"MAINTENANCE_REQUIRED",
|
|
51
|
+
"UNSUPPORTED_FORMAT",
|
|
36
52
|
];
|
|
37
53
|
// hide ExperimentalWarning glob()
|
|
38
54
|
// Guard against non-Node environments (e.g. accidental import in a browser bundle)
|
|
@@ -143,6 +159,15 @@ export default class Inibase {
|
|
|
143
159
|
* changes: see the invalidation in `getTable`'s reload branch,
|
|
144
160
|
* `createTable` and `updateTableLocked`. Bounded by the number of tables. */
|
|
145
161
|
computedPlanCache = new Map();
|
|
162
|
+
/** Per-table `.format` marker + live update-log overlays (format 2 tables).
|
|
163
|
+
* Keyed by `databasePath\0tableName` so two databases in one process never
|
|
164
|
+
* share state. Populated lazily by `getOverlay`. */
|
|
165
|
+
tableFormats = new Map();
|
|
166
|
+
overlays = new Map();
|
|
167
|
+
/** Last observed identity of each table's `.format` file (`mtime:size:ino`),
|
|
168
|
+
* used to detect a compaction by another process without re-reading the
|
|
169
|
+
* marker on every access. */
|
|
170
|
+
formatStamps = new Map();
|
|
146
171
|
databasePath;
|
|
147
172
|
uniqueMap;
|
|
148
173
|
schemaFileExtension = process.env.INIBASE_SCHEMA_EXTENSION ?? "json";
|
|
@@ -265,6 +290,25 @@ export default class Inibase {
|
|
|
265
290
|
await File.write(join(tablePath, ".prepend.config"), "");
|
|
266
291
|
if (config.decodeID)
|
|
267
292
|
await File.write(join(tablePath, ".decodeID.config"), "");
|
|
293
|
+
if (config.log) {
|
|
294
|
+
if (config.prepend)
|
|
295
|
+
throw this.createError("UNSUPPORTED_FORMAT", "append layout (no prepend)");
|
|
296
|
+
if (schema)
|
|
297
|
+
this.assertLogCompatibleSchema(schema);
|
|
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.
|
|
300
|
+
await writeFormat(tablePath, {
|
|
301
|
+
format: CURRENT_FORMAT_VERSION,
|
|
302
|
+
log: true,
|
|
303
|
+
baseGen: 0,
|
|
304
|
+
});
|
|
305
|
+
await File.write(join(tablePath, UPDATES_LOG_NAME), "");
|
|
306
|
+
this.tableFormats.set(tableName, {
|
|
307
|
+
format: CURRENT_FORMAT_VERSION,
|
|
308
|
+
log: true,
|
|
309
|
+
baseGen: 0,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
268
312
|
}
|
|
269
313
|
if (schema) {
|
|
270
314
|
const lastSchemaID = { value: 0 };
|
|
@@ -339,6 +383,53 @@ export default class Inibase {
|
|
|
339
383
|
}
|
|
340
384
|
}
|
|
341
385
|
async updateTableLocked(tableName, table, tablePath, schema, config) {
|
|
386
|
+
// A log table cannot be reshaped in place yet: its log frames reference
|
|
387
|
+
// column names and its overlay would have to be materialised into the
|
|
388
|
+
// base columns first (compaction, 3.9). Rename/other config are fine.
|
|
389
|
+
if (this.isLogTable(tableName)) {
|
|
390
|
+
if (schema)
|
|
391
|
+
throw this.createError("MAINTENANCE_REQUIRED", `${tableName} (schema change on update-log table)`);
|
|
392
|
+
if (config?.prepend)
|
|
393
|
+
throw this.createError("MAINTENANCE_REQUIRED", `${tableName} (prepend layout on update-log table)`);
|
|
394
|
+
if (config?.log === false)
|
|
395
|
+
throw this.createError("MAINTENANCE_REQUIRED", `${tableName} (disabling the update log needs compaction)`);
|
|
396
|
+
}
|
|
397
|
+
else if (config?.log) {
|
|
398
|
+
if (config.prepend || table.config.prepend)
|
|
399
|
+
throw this.createError("UNSUPPORTED_FORMAT", "append layout (no prepend)");
|
|
400
|
+
if (table.schema?.length)
|
|
401
|
+
this.assertLogCompatibleSchema(table.schema);
|
|
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
|
+
}
|
|
427
|
+
this.tableFormats.set(tableName, {
|
|
428
|
+
format: CURRENT_FORMAT_VERSION,
|
|
429
|
+
log: true,
|
|
430
|
+
baseGen: 0,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
342
433
|
if (schema) {
|
|
343
434
|
this.validateSchema(schema);
|
|
344
435
|
// remove id from schema
|
|
@@ -543,6 +634,13 @@ export default class Inibase {
|
|
|
543
634
|
this.computedPlanCache.delete(tableName);
|
|
544
635
|
if (config?.name && config.name !== tableName)
|
|
545
636
|
this.computedPlanCache.delete(config.name);
|
|
637
|
+
// Drop log/format caches for the touched table(s) so next access re-reads
|
|
638
|
+
// the (possibly moved) marker and overlay from disk.
|
|
639
|
+
for (const name of config?.name ? [tableName, config.name] : [tableName]) {
|
|
640
|
+
this.tableFormats.delete(name);
|
|
641
|
+
this.formatStamps.delete(name);
|
|
642
|
+
this.overlays.delete(this.overlayKey(name));
|
|
643
|
+
}
|
|
546
644
|
}
|
|
547
645
|
/**
|
|
548
646
|
* Get table schema and config
|
|
@@ -557,22 +655,313 @@ export default class Inibase {
|
|
|
557
655
|
throw this.createError("TABLE_NOT_EXISTS", tableName);
|
|
558
656
|
if (!globalConfig[this.databasePath].tables?.has(tableName) ||
|
|
559
657
|
globalConfig[this.databasePath].tables?.get(tableName)?.timestamp !==
|
|
560
|
-
(await File.getFileDate(join(tablePath, `schema.${this.schemaFileExtension}`))))
|
|
658
|
+
(await File.getFileDate(join(tablePath, `schema.${this.schemaFileExtension}`)))) {
|
|
659
|
+
const format = await this.readValidatedFormat(tableName, tablePath);
|
|
660
|
+
this.tableFormats.set(tableName, format);
|
|
661
|
+
await this.stampFormat(tableName);
|
|
662
|
+
const config = {
|
|
663
|
+
compression: await File.isExists(join(tablePath, ".compression.config")),
|
|
664
|
+
cache: await File.isExists(join(tablePath, ".cache.config")),
|
|
665
|
+
prepend: await File.isExists(join(tablePath, ".prepend.config")),
|
|
666
|
+
decodeID: await File.isExists(join(tablePath, ".decodeID.config")),
|
|
667
|
+
};
|
|
668
|
+
// Only surface `log` when enabled, so the public config shape of a
|
|
669
|
+
// legacy table is byte-for-byte what it was before 3.8.
|
|
670
|
+
if (format.log)
|
|
671
|
+
config.log = true;
|
|
561
672
|
globalConfig[this.databasePath].tables?.set(tableName, {
|
|
562
673
|
schema: await this.getTableSchema(tableName),
|
|
563
|
-
config
|
|
564
|
-
compression: await File.isExists(join(tablePath, ".compression.config")),
|
|
565
|
-
cache: await File.isExists(join(tablePath, ".cache.config")),
|
|
566
|
-
prepend: await File.isExists(join(tablePath, ".prepend.config")),
|
|
567
|
-
decodeID: await File.isExists(join(tablePath, ".decodeID.config")),
|
|
568
|
-
},
|
|
674
|
+
config,
|
|
569
675
|
timestamp: await File.getFileDate(join(tablePath, `schema.${this.schemaFileExtension}`)),
|
|
570
676
|
});
|
|
677
|
+
}
|
|
571
678
|
// A reload means the on-disk schema changed (create/update/external
|
|
572
679
|
// edit): the cached computed plan is derived from it.
|
|
573
680
|
this.computedPlanCache.delete(tableName);
|
|
574
681
|
return globalConfig[this.databasePath].tables?.get(tableName);
|
|
575
682
|
}
|
|
683
|
+
/* -----------------------------------------------------------------------
|
|
684
|
+
* Update-log overlay (format 2 tables)
|
|
685
|
+
* --------------------------------------------------------------------- */
|
|
686
|
+
overlayKey(tableName) {
|
|
687
|
+
return `${this.databasePath}\u0000${tableName}`;
|
|
688
|
+
}
|
|
689
|
+
/** True when the table opted into the incremental update log. */
|
|
690
|
+
isLogTable(tableName) {
|
|
691
|
+
const cached = this.tableFormats.get(tableName);
|
|
692
|
+
if (cached)
|
|
693
|
+
return cached.log;
|
|
694
|
+
return (globalConfig[this.databasePath].tables?.get(tableName)?.config.log ===
|
|
695
|
+
true);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Resolve a table's format marker, revalidating a cached *log* table when
|
|
699
|
+
* its `.format` file changed underneath us. A compaction by another process
|
|
700
|
+
* bumps `baseGen` and empties the log; without re-reading the marker this
|
|
701
|
+
* process would scan the new (empty) log with the old `baseGen`, and once a
|
|
702
|
+
* post-compaction frame appears the generation gap would look like
|
|
703
|
+
* corruption. Legacy tables never change format implicitly, so they skip
|
|
704
|
+
* the stat entirely and keep the pre-3.8 read path cost.
|
|
705
|
+
*/
|
|
706
|
+
/**
|
|
707
|
+
* Read a table's `.format` marker and refuse a table written by a newer
|
|
708
|
+
* engine (§4.1) *before* any column is read: a higher `format`, or a
|
|
709
|
+
* `minReader` above this build, throws UNSUPPORTED_FORMAT rather than being
|
|
710
|
+
* silently mis-read as format 2/legacy.
|
|
711
|
+
*/
|
|
712
|
+
async readValidatedFormat(tableName, tablePath) {
|
|
713
|
+
const format = await readFormat(tablePath);
|
|
714
|
+
const reason = unsupportedFormatReason(format);
|
|
715
|
+
if (reason)
|
|
716
|
+
throw this.createError("UNSUPPORTED_FORMAT", `${tableName} (${reason})`);
|
|
717
|
+
return format;
|
|
718
|
+
}
|
|
719
|
+
async currentFormat(tableName) {
|
|
720
|
+
const cached = this.tableFormats.get(tableName);
|
|
721
|
+
if (cached && !cached.log)
|
|
722
|
+
return cached;
|
|
723
|
+
const tablePath = join(this.databasePath, tableName);
|
|
724
|
+
let stamp = "none";
|
|
725
|
+
try {
|
|
726
|
+
const stat_ = await stat(join(tablePath, FORMAT_FILE));
|
|
727
|
+
stamp = `${stat_.mtimeMs}:${stat_.size}:${stat_.ino}`;
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
stamp = "none";
|
|
731
|
+
}
|
|
732
|
+
if (cached && this.formatStamps.get(tableName) === stamp)
|
|
733
|
+
return cached;
|
|
734
|
+
const format = await this.readValidatedFormat(tableName, tablePath);
|
|
735
|
+
this.tableFormats.set(tableName, format);
|
|
736
|
+
this.formatStamps.set(tableName, stamp);
|
|
737
|
+
// A changed baseGen invalidates the cached overlay: rebuild from the
|
|
738
|
+
// (possibly now empty) log at the new generation.
|
|
739
|
+
const key = this.overlayKey(tableName);
|
|
740
|
+
const overlay = this.overlays.get(key);
|
|
741
|
+
if (overlay && overlay.baseGen !== format.baseGen)
|
|
742
|
+
this.overlays.delete(key);
|
|
743
|
+
return format;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Load (or refresh) a table's live overlay. Returns null for legacy tables
|
|
747
|
+
* (no `.format` marker) so every existing code path stays untouched. With
|
|
748
|
+
* `forWrite`, repairs a torn tail and rebuilds to the committed boundary;
|
|
749
|
+
* the caller must already hold the table writer lock.
|
|
750
|
+
*/
|
|
751
|
+
async getOverlay(tableName, forWrite = false) {
|
|
752
|
+
const format = await this.currentFormat(tableName);
|
|
753
|
+
if (!format?.log)
|
|
754
|
+
return null;
|
|
755
|
+
const key = this.overlayKey(tableName);
|
|
756
|
+
let overlay = this.overlays.get(key);
|
|
757
|
+
if (!overlay || overlay.baseGen !== format.baseGen) {
|
|
758
|
+
overlay = new TableOverlay(join(this.databasePath, tableName), format.baseGen);
|
|
759
|
+
this.overlays.set(key, overlay);
|
|
760
|
+
}
|
|
761
|
+
if (forWrite)
|
|
762
|
+
await overlay.refreshForWrite();
|
|
763
|
+
else
|
|
764
|
+
await overlay.refresh();
|
|
765
|
+
return overlay;
|
|
766
|
+
}
|
|
767
|
+
async stampFormat(tableName) {
|
|
768
|
+
let stamp = "none";
|
|
769
|
+
try {
|
|
770
|
+
const stat_ = await stat(join(this.databasePath, tableName, FORMAT_FILE));
|
|
771
|
+
stamp = `${stat_.mtimeMs}:${stat_.size}:${stat_.ino}`;
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
stamp = "none";
|
|
775
|
+
}
|
|
776
|
+
this.formatStamps.set(tableName, stamp);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* The format-2 update log stores one encoded cell per changed *top-level
|
|
780
|
+
* simple* column; nested (object/array/table) and computed fields are not
|
|
781
|
+
* representable without a compaction step (3.9). A log table whose schema
|
|
782
|
+
* violates this is rejected at DDL time so the write path can always take
|
|
783
|
+
* the log and never needs to interleave a whole-column rewrite.
|
|
784
|
+
*/
|
|
785
|
+
assertLogCompatibleSchema(schema) {
|
|
786
|
+
for (const field of schema) {
|
|
787
|
+
if (["id", "createdAt", "updatedAt"].includes(field.key))
|
|
788
|
+
continue;
|
|
789
|
+
if (typeof field.computed !== "undefined")
|
|
790
|
+
throw this.createError("UNSUPPORTED_FORMAT", `simple top-level fields (computed field "${field.key}" is not supported with log)`);
|
|
791
|
+
if (!this.isSimpleField(field.type))
|
|
792
|
+
throw this.createError("UNSUPPORTED_FORMAT", `simple top-level fields (field "${field.key}" is ${field.type})`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
idFieldOf(tableName) {
|
|
796
|
+
const schema = globalConfig[this.databasePath].tables?.get(tableName)?.schema ?? [];
|
|
797
|
+
return (schema.find((field) => field.key === "id") ?? { key: "id", type: "id" });
|
|
798
|
+
}
|
|
799
|
+
/** Read the internal numeric id for each requested line (stable overlay key). */
|
|
800
|
+
async overlayLineIds(tableName, lines) {
|
|
801
|
+
const map = new Map();
|
|
802
|
+
if (!lines.length)
|
|
803
|
+
return map;
|
|
804
|
+
const filePath = join(this.databasePath, tableName, `id${this.getFileExtension(tableName)}`);
|
|
805
|
+
// Streaming reader (no shell spawn): like the sparse column read path,
|
|
806
|
+
// stop at the last requested line instead of shelling out to `sed -n`,
|
|
807
|
+
// which costs ~ms per call on a log-table read.
|
|
808
|
+
const values = await File.getLines(filePath, lines, {
|
|
809
|
+
...this.idFieldOf(tableName),
|
|
810
|
+
key: "id",
|
|
811
|
+
type: "number",
|
|
812
|
+
});
|
|
813
|
+
for (const [line, value] of Object.entries(values))
|
|
814
|
+
map.set(Number(line), Number(value));
|
|
815
|
+
return map;
|
|
816
|
+
}
|
|
817
|
+
/** Internal id -> current line number(s), built from the id column. */
|
|
818
|
+
async overlayIdToLines(tableName) {
|
|
819
|
+
const filePath = join(this.databasePath, tableName, `id${this.getFileExtension(tableName)}`);
|
|
820
|
+
const values = await File.get(filePath, undefined, {
|
|
821
|
+
...this.idFieldOf(tableName),
|
|
822
|
+
key: "id",
|
|
823
|
+
type: "number",
|
|
824
|
+
});
|
|
825
|
+
const map = new Map();
|
|
826
|
+
for (const [line, value] of Object.entries(values)) {
|
|
827
|
+
if (typeof value === "undefined" || value === null)
|
|
828
|
+
continue;
|
|
829
|
+
const id = Number(value);
|
|
830
|
+
const bucket = map.get(id);
|
|
831
|
+
if (bucket)
|
|
832
|
+
bucket.push(Number(line));
|
|
833
|
+
else
|
|
834
|
+
map.set(id, [Number(line)]);
|
|
835
|
+
}
|
|
836
|
+
return map;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Per-line raw-cell overrides for one column, from the overlay. Used by
|
|
840
|
+
* `File.search` so criteria, uniqueness and aggregate scans see the merged
|
|
841
|
+
* (un-compacted) values. Returns undefined when the table has no overlay or
|
|
842
|
+
* the column is never overlaid.
|
|
843
|
+
*/
|
|
844
|
+
async overlayColumnOverrides(tableName, columnKey) {
|
|
845
|
+
const overlay = await this.getOverlay(tableName);
|
|
846
|
+
if (!overlay || overlay.size === 0)
|
|
847
|
+
return undefined;
|
|
848
|
+
const entries = [];
|
|
849
|
+
for (const [id, entry] of overlay.byId)
|
|
850
|
+
if (Object.hasOwn(entry.cols, columnKey))
|
|
851
|
+
entries.push([Number(id), entry.cols[columnKey]]);
|
|
852
|
+
if (!entries.length)
|
|
853
|
+
return undefined;
|
|
854
|
+
const idToLines = await this.overlayIdToLines(tableName);
|
|
855
|
+
const overrides = new Map();
|
|
856
|
+
for (const [id, encoded] of entries) {
|
|
857
|
+
const lines = idToLines.get(id);
|
|
858
|
+
if (!lines)
|
|
859
|
+
continue;
|
|
860
|
+
for (const line of lines)
|
|
861
|
+
overrides.set(line, encoded);
|
|
862
|
+
}
|
|
863
|
+
return overrides.size ? overrides : undefined;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Fold every logged update into the base columns and reset the log.
|
|
867
|
+
*
|
|
868
|
+
* Safe to call on any table: legacy tables (no `.format` marker) are a
|
|
869
|
+
* no-op. Semantically neutral — it only materializes already-acknowledged
|
|
870
|
+
* updates, so a failure never makes an acknowledged `put` disappear.
|
|
871
|
+
* Returns the new `baseGen` for a compacted table, or `null` when there was
|
|
872
|
+
* nothing to fold.
|
|
873
|
+
*/
|
|
874
|
+
async compact(tableName) {
|
|
875
|
+
this.validateName(tableName);
|
|
876
|
+
if (this.activeTransaction)
|
|
877
|
+
throw this.createError("MAINTENANCE_REQUIRED", `${tableName} (compaction inside a transaction)`);
|
|
878
|
+
await this.ensureDatabaseRecovered();
|
|
879
|
+
const tablePath = join(this.databasePath, tableName);
|
|
880
|
+
if (!(await File.isExists(tablePath)))
|
|
881
|
+
throw this.createError("TABLE_NOT_EXISTS", tableName);
|
|
882
|
+
const format = await this.currentFormat(tableName);
|
|
883
|
+
if (!format?.log)
|
|
884
|
+
return null; // legacy table: nothing to fold
|
|
885
|
+
await File.lock(join(tablePath, ".tmp"));
|
|
886
|
+
try {
|
|
887
|
+
return await this.compactLogTable(tableName);
|
|
888
|
+
}
|
|
889
|
+
finally {
|
|
890
|
+
await File.unlock(join(tablePath, ".tmp"));
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Compaction core. The caller must hold the table writer lock. Streams each
|
|
895
|
+
* overlaid base column through `File.replace` (which handles compression),
|
|
896
|
+
* then publishes the new columns, an empty `updates.log`, and a `.format`
|
|
897
|
+
* marker with the bumped generation in one journaled commit, so a crash
|
|
898
|
+
* either rolls every file back (old base + old log + old `baseGen`) or
|
|
899
|
+
* forward (new base + empty log + new `baseGen`). No acknowledged update is
|
|
900
|
+
* ever lost.
|
|
901
|
+
*/
|
|
902
|
+
async compactLogTable(tableName) {
|
|
903
|
+
const tablePath = join(this.databasePath, tableName);
|
|
904
|
+
const overlay = await this.getOverlay(tableName, true);
|
|
905
|
+
if (!overlay || overlay.size === 0)
|
|
906
|
+
return null; // already compact
|
|
907
|
+
const schema = (await this.getTable(tableName))?.schema ?? [];
|
|
908
|
+
const schemaKeys = new Set(schema.map((field) => field.key));
|
|
909
|
+
// Only columns that some frame actually overlaid need rewriting; the
|
|
910
|
+
// untouched base columns are left exactly as they are.
|
|
911
|
+
const overlaidKeys = new Set();
|
|
912
|
+
for (const entry of overlay.byId.values())
|
|
913
|
+
for (const key of Object.keys(entry.cols))
|
|
914
|
+
if (schemaKeys.has(key))
|
|
915
|
+
overlaidKeys.add(key);
|
|
916
|
+
if (!overlaidKeys.size)
|
|
917
|
+
return null;
|
|
918
|
+
const idToLines = await this.overlayIdToLines(tableName);
|
|
919
|
+
const extension = this.getFileExtension(tableName);
|
|
920
|
+
const renameList = [];
|
|
921
|
+
for (const key of overlaidKeys) {
|
|
922
|
+
const record = {};
|
|
923
|
+
for (const [id, entry] of overlay.byId) {
|
|
924
|
+
if (!Object.hasOwn(entry.cols, key))
|
|
925
|
+
continue;
|
|
926
|
+
const lines = idToLines.get(Number(id));
|
|
927
|
+
if (!lines)
|
|
928
|
+
continue;
|
|
929
|
+
const encoded = entry.cols[key];
|
|
930
|
+
for (const line of lines)
|
|
931
|
+
record[line] = encoded;
|
|
932
|
+
}
|
|
933
|
+
if (!Object.keys(record).length)
|
|
934
|
+
continue;
|
|
935
|
+
renameList.push(await File.replace(join(tablePath, `${key}${extension}`), record));
|
|
936
|
+
}
|
|
937
|
+
if (!renameList.length)
|
|
938
|
+
return null;
|
|
939
|
+
const newGen = overlay.committedGen;
|
|
940
|
+
const staging = (name) => join(tablePath, ".tmp", `${name}.compact-${process.pid}-${randomUUID().slice(0, 8)}`);
|
|
941
|
+
// Empty log (frames are now folded into base) and the bumped marker,
|
|
942
|
+
// committed atomically with the column swaps.
|
|
943
|
+
const logTmp = staging(UPDATES_LOG_NAME);
|
|
944
|
+
await File.write(logTmp, "");
|
|
945
|
+
renameList.push([logTmp, join(tablePath, UPDATES_LOG_NAME)]);
|
|
946
|
+
const fmtTmp = staging(FORMAT_FILE);
|
|
947
|
+
await File.write(fmtTmp, serializeFormat({
|
|
948
|
+
format: CURRENT_FORMAT_VERSION,
|
|
949
|
+
log: true,
|
|
950
|
+
baseGen: newGen,
|
|
951
|
+
}));
|
|
952
|
+
renameList.push([fmtTmp, join(tablePath, FORMAT_FILE)]);
|
|
953
|
+
await this.commitFiles(tablePath, renameList, null);
|
|
954
|
+
// In-memory: the base now carries every update; drop the overlay so the
|
|
955
|
+
// next access rebuilds an empty one at the new generation.
|
|
956
|
+
this.tableFormats.set(tableName, {
|
|
957
|
+
format: CURRENT_FORMAT_VERSION,
|
|
958
|
+
log: true,
|
|
959
|
+
baseGen: newGen,
|
|
960
|
+
});
|
|
961
|
+
await this.stampFormat(tableName);
|
|
962
|
+
this.overlays.delete(this.overlayKey(tableName));
|
|
963
|
+
return newGen;
|
|
964
|
+
}
|
|
576
965
|
async getTableSchema(tableName) {
|
|
577
966
|
this.validateName(tableName);
|
|
578
967
|
const tablePath = join(this.databasePath, tableName);
|
|
@@ -823,7 +1212,7 @@ export default class Inibase {
|
|
|
823
1212
|
if (!field)
|
|
824
1213
|
continue;
|
|
825
1214
|
fieldsKeys.push(field.key);
|
|
826
|
-
const [_, totalLines, lineNumbers] = await File.search(join(tablePath, `${field.key}${this.getFileExtension(tableName)}`), "[]", Array.from(values), undefined, valueObject.exclude, { ...field, databasePath: this.databasePath }, 1, undefined, false);
|
|
1215
|
+
const [_, totalLines, lineNumbers] = await File.search(join(tablePath, `${field.key}${this.getFileExtension(tableName)}`), "[]", Array.from(values), undefined, valueObject.exclude, { ...field, databasePath: this.databasePath }, 1, undefined, false, undefined, await this.overlayColumnOverrides(tableName, field.key));
|
|
827
1216
|
if (totalLines > 0) {
|
|
828
1217
|
if (valueObject.columnsValues.size === 1 ||
|
|
829
1218
|
(valueObject.columnsValues.size === index &&
|
|
@@ -1051,11 +1440,58 @@ export default class Inibase {
|
|
|
1051
1440
|
await this.processTableField(tableName, field, RETURN, linesNumber, options, prefix);
|
|
1052
1441
|
}
|
|
1053
1442
|
}
|
|
1443
|
+
await this.mergeOverlayIntoRows(tableName, schema, RETURN);
|
|
1054
1444
|
return RETURN;
|
|
1055
1445
|
}
|
|
1056
|
-
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
1446
|
+
/**
|
|
1447
|
+
* Fold the table's un-compacted update-log overlay onto decoded rows.
|
|
1448
|
+
*
|
|
1449
|
+
* `RETURN` is keyed by line number and its `id` is the user-facing id, so
|
|
1450
|
+
* the overlay (keyed by the internal numeric id) is matched through a fresh
|
|
1451
|
+
* id-column read of just the requested lines. Only columns present in
|
|
1452
|
+
* `schema` (which is already narrowed to the requested `columns`) are
|
|
1453
|
+
* written, so a partial projection never grows extra keys. Legacy tables
|
|
1454
|
+
* (no `.format` marker) return immediately.
|
|
1455
|
+
*/
|
|
1456
|
+
async mergeOverlayIntoRows(tableName, schema, RETURN) {
|
|
1457
|
+
if (!this.isLogTable(tableName))
|
|
1458
|
+
return;
|
|
1459
|
+
const lines = Object.keys(RETURN).map(Number);
|
|
1460
|
+
if (!lines.length)
|
|
1461
|
+
return;
|
|
1462
|
+
const overlay = await this.getOverlay(tableName);
|
|
1463
|
+
if (!overlay || overlay.size === 0)
|
|
1464
|
+
return;
|
|
1465
|
+
const lineIds = await this.overlayLineIds(tableName, lines);
|
|
1466
|
+
if (!lineIds.size)
|
|
1467
|
+
return;
|
|
1468
|
+
const allowed = new Map();
|
|
1469
|
+
for (const field of schema)
|
|
1470
|
+
allowed.set(field.key, field);
|
|
1471
|
+
const hasUpdatedAt = allowed.has("updatedAt");
|
|
1472
|
+
for (const line of lines) {
|
|
1473
|
+
const id = lineIds.get(line);
|
|
1474
|
+
if (id === undefined)
|
|
1475
|
+
continue;
|
|
1476
|
+
const entry = overlay.get(id);
|
|
1477
|
+
if (!entry)
|
|
1478
|
+
continue;
|
|
1479
|
+
const row = RETURN[line];
|
|
1480
|
+
if (!row)
|
|
1481
|
+
continue;
|
|
1482
|
+
for (const [key, encoded] of Object.entries(entry.cols)) {
|
|
1483
|
+
const field = allowed.get(key);
|
|
1484
|
+
if (!field)
|
|
1485
|
+
continue;
|
|
1486
|
+
row[key] = File.decode(encoded, {
|
|
1487
|
+
...field,
|
|
1488
|
+
databasePath: this.databasePath,
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
if (hasUpdatedAt && entry.updatedAt !== undefined)
|
|
1492
|
+
row.updatedAt = entry.updatedAt;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1059
1495
|
/**
|
|
1060
1496
|
* Extract the raw expression from a field's `computed` property (string or
|
|
1061
1497
|
* persisted `{ expr, ast }` spec).
|
|
@@ -1971,7 +2407,7 @@ export default class Inibase {
|
|
|
1971
2407
|
}, (options.perPage ?? 0) < 0 ? undefined : options.perPage, (options.perPage ?? 0) < 0
|
|
1972
2408
|
? undefined
|
|
1973
2409
|
: (options.page - 1) * options.perPage +
|
|
1974
|
-
((options.page ?? 1) > 1 ? 1 : 0), true);
|
|
2410
|
+
((options.page ?? 1) > 1 ? 1 : 0), true, undefined, await this.overlayColumnOverrides(tableName, key));
|
|
1975
2411
|
if (!searchResult) {
|
|
1976
2412
|
if (allTrue)
|
|
1977
2413
|
return null;
|
|
@@ -2541,6 +2977,14 @@ export default class Inibase {
|
|
|
2541
2977
|
(Utils.isObject(where) && !Object.keys(where).length)))
|
|
2542
2978
|
where = undefined;
|
|
2543
2979
|
if (options.sort) {
|
|
2980
|
+
// With an un-compacted overlay the shell `paste | sort | awk` path
|
|
2981
|
+
// would read stale base cells. Fall back to an in-process sort of
|
|
2982
|
+
// the merged rows, which is correct for every sort shape.
|
|
2983
|
+
if (this.isLogTable(tableName)) {
|
|
2984
|
+
const overlay = await this.getOverlay(tableName);
|
|
2985
|
+
if (overlay?.size)
|
|
2986
|
+
return await this.getOnceSortedWithOverlay(tableName, schema, where, options, onlyOne);
|
|
2987
|
+
}
|
|
2544
2988
|
let sortArray;
|
|
2545
2989
|
let awkCommand = "";
|
|
2546
2990
|
if (Utils.isObject(options.sort) && !Array.isArray(options.sort)) {
|
|
@@ -2752,8 +3196,15 @@ export default class Inibase {
|
|
|
2752
3196
|
}
|
|
2753
3197
|
else if (Utils.isObject(where)) {
|
|
2754
3198
|
let cachedFilePath = "";
|
|
3199
|
+
// The criteria cache is versioned by the row count only; a put
|
|
3200
|
+
// changes matching rows without changing the count, so a cached
|
|
3201
|
+
// line set would go stale. Bypass it entirely while an overlay
|
|
3202
|
+
// exists.
|
|
3203
|
+
const overlayActive = this.isLogTable(tableName) &&
|
|
3204
|
+
((await this.getOverlay(tableName))?.size ?? 0) > 0;
|
|
2755
3205
|
// Criteria
|
|
2756
|
-
if (
|
|
3206
|
+
if (!overlayActive &&
|
|
3207
|
+
globalConfig[this.databasePath].tables?.get(tableName)?.config.cache) {
|
|
2757
3208
|
// Cache entries are versioned by the pagination row count so a
|
|
2758
3209
|
// stale cache written before a post/delete (in this process or
|
|
2759
3210
|
// another) is detectable: the candidate filename simply stops
|
|
@@ -2788,7 +3239,8 @@ export default class Inibase {
|
|
|
2788
3239
|
for (const [key] of this.totalItems)
|
|
2789
3240
|
if (key.startsWith(`${tableName}-`) && key !== `${tableName}-id`)
|
|
2790
3241
|
this.totalItems.delete(key);
|
|
2791
|
-
if (
|
|
3242
|
+
if (!overlayActive &&
|
|
3243
|
+
globalConfig[this.databasePath].tables?.get(tableName)?.config.cache)
|
|
2792
3244
|
await writeFile(cachedFilePath, Object.keys(LineNumberDataObj).join(","));
|
|
2793
3245
|
}
|
|
2794
3246
|
}
|
|
@@ -2810,6 +3262,70 @@ export default class Inibase {
|
|
|
2810
3262
|
};
|
|
2811
3263
|
return onlyOne && Array.isArray(RETURN) ? RETURN[0] : RETURN;
|
|
2812
3264
|
}
|
|
3265
|
+
/**
|
|
3266
|
+
* In-process sort fallback for log tables with a non-empty overlay: the
|
|
3267
|
+
* shell `paste | sort | awk` path reads base cells and would miss puts.
|
|
3268
|
+
* Reads the merged rows for the (optionally filtered) lines, sorts them by
|
|
3269
|
+
* the requested keys with type-aware comparison, then paginates.
|
|
3270
|
+
*/
|
|
3271
|
+
async getOnceSortedWithOverlay(tableName, schema, where, options, onlyOne) {
|
|
3272
|
+
let sortArray;
|
|
3273
|
+
if (Utils.isObject(options.sort) && !Array.isArray(options.sort))
|
|
3274
|
+
sortArray = Object.entries(options.sort).map(([key, value]) => [
|
|
3275
|
+
key,
|
|
3276
|
+
typeof value === "string" ? value.toLowerCase() === "asc" : value > 0,
|
|
3277
|
+
]);
|
|
3278
|
+
else
|
|
3279
|
+
sortArray = []
|
|
3280
|
+
.concat(options.sort)
|
|
3281
|
+
.map((column) => [column, true]);
|
|
3282
|
+
let lines;
|
|
3283
|
+
if (where) {
|
|
3284
|
+
const found = await this.get(tableName, where, undefined, undefined, true);
|
|
3285
|
+
if (!found)
|
|
3286
|
+
return null;
|
|
3287
|
+
lines = Array.isArray(found) ? found : [found];
|
|
3288
|
+
}
|
|
3289
|
+
else {
|
|
3290
|
+
const { total } = await this.resolvePagination(tableName);
|
|
3291
|
+
lines = Array.from({ length: total }, (_, index) => index + 1);
|
|
3292
|
+
}
|
|
3293
|
+
if (!lines.length)
|
|
3294
|
+
return null;
|
|
3295
|
+
const rows = Object.values(await this.processSchemaData(tableName, schema, lines, options));
|
|
3296
|
+
const comparator = (a, b) => {
|
|
3297
|
+
for (const [key, ascending] of sortArray) {
|
|
3298
|
+
const field = Utils.getField(key, Utils.flattenSchema(schema));
|
|
3299
|
+
const aValue = a?.[key];
|
|
3300
|
+
const bValue = b?.[key];
|
|
3301
|
+
let comparison;
|
|
3302
|
+
if (field && Utils.isFieldType(field, ["id", "number", "date"]))
|
|
3303
|
+
comparison = (Number(aValue) || 0) - (Number(bValue) || 0);
|
|
3304
|
+
else
|
|
3305
|
+
comparison = String(aValue ?? "").localeCompare(String(bValue ?? ""));
|
|
3306
|
+
if (comparison)
|
|
3307
|
+
return ascending ? comparison : -comparison;
|
|
3308
|
+
}
|
|
3309
|
+
// Deterministic tiebreak (legacy `sort` leaves ties unspecified).
|
|
3310
|
+
return (Number(a?.id) || 0) - (Number(b?.id) || 0);
|
|
3311
|
+
};
|
|
3312
|
+
rows.sort(comparator);
|
|
3313
|
+
const total = rows.length;
|
|
3314
|
+
const perPage = options.perPage;
|
|
3315
|
+
const pageRows = perPage < 0
|
|
3316
|
+
? rows
|
|
3317
|
+
: rows.slice((options.page - 1) * perPage, options.page * perPage);
|
|
3318
|
+
if (!pageRows.length)
|
|
3319
|
+
return null;
|
|
3320
|
+
this.totalItems.set(`${tableName}-*`, total);
|
|
3321
|
+
this.pageInfo[tableName] = {
|
|
3322
|
+
...(({ columns, ...restOfOptions }) => restOfOptions)(options),
|
|
3323
|
+
perPage: pageRows.length,
|
|
3324
|
+
totalPages: perPage < 0 ? 1 : Math.ceil(total / perPage),
|
|
3325
|
+
total,
|
|
3326
|
+
};
|
|
3327
|
+
return onlyOne ? pageRows[0] : pageRows;
|
|
3328
|
+
}
|
|
2813
3329
|
async post(tableName, data, options, returnPostedData) {
|
|
2814
3330
|
return File.runWithLockStore(() => this._post(tableName, data, options, returnPostedData));
|
|
2815
3331
|
}
|
|
@@ -2958,6 +3474,119 @@ export default class Inibase {
|
|
|
2958
3474
|
}, returnUpdatedData, _whereIsLinesNumbers) {
|
|
2959
3475
|
return File.runWithLockStore(() => this._put(tableName, data, where, options, returnUpdatedData, _whereIsLinesNumbers));
|
|
2960
3476
|
}
|
|
3477
|
+
/**
|
|
3478
|
+
* Incremental `put` for format-2 (log) tables.
|
|
3479
|
+
*
|
|
3480
|
+
* Resolves the target rows to their stable internal ids, validates the
|
|
3481
|
+
* payload against the merged view (uniqueness included), and appends one
|
|
3482
|
+
* bounded frame instead of rewriting whole columns. The base files are left
|
|
3483
|
+
* untouched; readers merge the overlay. Returns `undefined` when nothing
|
|
3484
|
+
* matches, matching the legacy `put` contract.
|
|
3485
|
+
*/
|
|
3486
|
+
async putViaLog(tableName, data, where, options = { page: 1, perPage: 15 }, returnUpdatedData, _whereIsLinesNumbers) {
|
|
3487
|
+
const tablePath = join(this.databasePath, tableName);
|
|
3488
|
+
const schema = globalConfig[this.databasePath].tables?.get(tableName)?.schema ?? [];
|
|
3489
|
+
const payloads = Array.isArray(data) ? data : [data];
|
|
3490
|
+
const isWhereLess = !where;
|
|
3491
|
+
const isLines = _whereIsLinesNumbers === true;
|
|
3492
|
+
const scalarWhere = !isWhereLess && !isLines && !Array.isArray(where);
|
|
3493
|
+
await this.throwErrorIfTableEmpty(tableName);
|
|
3494
|
+
let overlay = null;
|
|
3495
|
+
let resolvedLines = [];
|
|
3496
|
+
let inlineLock = false;
|
|
3497
|
+
try {
|
|
3498
|
+
await File.lock(join(tablePath, ".tmp"));
|
|
3499
|
+
inlineLock = true;
|
|
3500
|
+
// Writer-side refresh: repairs a torn tail so the next frame is
|
|
3501
|
+
// never appended behind unreadable bytes, and gives us the current
|
|
3502
|
+
// committed generation.
|
|
3503
|
+
overlay = await this.getOverlay(tableName, true);
|
|
3504
|
+
if (!overlay)
|
|
3505
|
+
return undefined;
|
|
3506
|
+
// Backpressure before the write: if the log or the overlay has grown
|
|
3507
|
+
// past its bound, fold it into the base first. Compaction is
|
|
3508
|
+
// maintenance (it only materializes acknowledged frames), so a
|
|
3509
|
+
// failure surfaces as MAINTENANCE_REQUIRED without losing any
|
|
3510
|
+
// committed update.
|
|
3511
|
+
if (overlay.size >= maxOverlayRecords() ||
|
|
3512
|
+
(await overlay.log.bytes()) >= maxLogBytes()) {
|
|
3513
|
+
try {
|
|
3514
|
+
await this.compactLogTable(tableName);
|
|
3515
|
+
}
|
|
3516
|
+
catch (error) {
|
|
3517
|
+
throw this.createError("MAINTENANCE_REQUIRED", `${tableName} (compaction failed: ${error?.message ?? error})`);
|
|
3518
|
+
}
|
|
3519
|
+
overlay = await this.getOverlay(tableName, true);
|
|
3520
|
+
if (overlay.size >= maxOverlayRecords())
|
|
3521
|
+
throw this.createError("MAINTENANCE_REQUIRED", `${tableName} (overlay still exceeds ${maxOverlayRecords()} rows after compaction)`);
|
|
3522
|
+
}
|
|
3523
|
+
if (isWhereLess) {
|
|
3524
|
+
const { total } = await this.resolvePagination(tableName);
|
|
3525
|
+
resolvedLines = Array.from({ length: total }, (_, index) => index + 1);
|
|
3526
|
+
}
|
|
3527
|
+
else if (isLines) {
|
|
3528
|
+
resolvedLines = Array.isArray(where)
|
|
3529
|
+
? where
|
|
3530
|
+
: [where];
|
|
3531
|
+
}
|
|
3532
|
+
else {
|
|
3533
|
+
const lines = await this.get(tableName, where, undefined, undefined, true);
|
|
3534
|
+
if (!lines)
|
|
3535
|
+
return undefined;
|
|
3536
|
+
resolvedLines = Array.isArray(lines)
|
|
3537
|
+
? lines
|
|
3538
|
+
: [lines];
|
|
3539
|
+
}
|
|
3540
|
+
if (!resolvedLines.length)
|
|
3541
|
+
return undefined;
|
|
3542
|
+
const lineIds = await this.overlayLineIds(tableName, resolvedLines);
|
|
3543
|
+
// Validate against the merged view: `checkUnique` searches the base
|
|
3544
|
+
// columns through `File.search` overrides, so this sees un-compacted
|
|
3545
|
+
// puts. Required fields are skipped (partial update).
|
|
3546
|
+
await this.validateTableData(tableName, structuredClone(payloads), true);
|
|
3547
|
+
const now = Date.now();
|
|
3548
|
+
const records = [];
|
|
3549
|
+
for (let index = 0; index < resolvedLines.length; index++) {
|
|
3550
|
+
const id = lineIds.get(resolvedLines[index]);
|
|
3551
|
+
if (id === undefined)
|
|
3552
|
+
continue;
|
|
3553
|
+
const payload = payloads.length === 1
|
|
3554
|
+
? payloads[0]
|
|
3555
|
+
: (payloads[index % payloads.length] ?? payloads[0]);
|
|
3556
|
+
const formatted = this.formatData(payload, schema, true);
|
|
3557
|
+
const cols = {};
|
|
3558
|
+
for (const [key, value] of Object.entries(formatted)) {
|
|
3559
|
+
if (key === "id" || key === "createdAt" || key === "updatedAt")
|
|
3560
|
+
continue;
|
|
3561
|
+
// Only columns the caller actually supplied are overlaid, so a
|
|
3562
|
+
// partial `put` never blanks untouched fields.
|
|
3563
|
+
if (!Object.hasOwn(payload, key))
|
|
3564
|
+
continue;
|
|
3565
|
+
cols[key] = File.encode(value);
|
|
3566
|
+
}
|
|
3567
|
+
cols.updatedAt = now;
|
|
3568
|
+
records.push({ id, cols, updatedAt: now });
|
|
3569
|
+
}
|
|
3570
|
+
if (records.length) {
|
|
3571
|
+
const frames = buildFrames(records, overlay.committedGen + 1, DEFAULT_MAX_FRAME_BYTES, overlay.log.path);
|
|
3572
|
+
for (const frame of frames)
|
|
3573
|
+
await overlay.log.append(frame);
|
|
3574
|
+
await overlay.applyLocal(frames);
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
finally {
|
|
3578
|
+
if (inlineLock)
|
|
3579
|
+
await File.unlock(join(tablePath, ".tmp"));
|
|
3580
|
+
}
|
|
3581
|
+
if (returnUpdatedData) {
|
|
3582
|
+
if (isWhereLess)
|
|
3583
|
+
return await this.get(tableName, undefined, options);
|
|
3584
|
+
if (scalarWhere)
|
|
3585
|
+
return await this.get(tableName, resolvedLines[0], options, true, undefined, true);
|
|
3586
|
+
return await this.get(tableName, resolvedLines, options, undefined, undefined, true);
|
|
3587
|
+
}
|
|
3588
|
+
return undefined;
|
|
3589
|
+
}
|
|
2961
3590
|
async _put(tableName, data, where, options = {
|
|
2962
3591
|
page: 1,
|
|
2963
3592
|
perPage: 15,
|
|
@@ -2973,6 +3602,16 @@ export default class Inibase {
|
|
|
2973
3602
|
if (!this.activeTransaction)
|
|
2974
3603
|
await this.ensureDatabaseRecovered();
|
|
2975
3604
|
await this.throwErrorIfTableEmpty(tableName);
|
|
3605
|
+
// Format-2 tables route every put through the append-only log, which
|
|
3606
|
+
// is proportional to the changed data. Transactions and DDL need a
|
|
3607
|
+
// compaction that 3.9 ships, so they are refused loudly rather than
|
|
3608
|
+
// silently falling back to a whole-column rewrite that would leave the
|
|
3609
|
+
// overlay shadowing newer base values.
|
|
3610
|
+
if (this.isLogTable(tableName)) {
|
|
3611
|
+
if (this.activeTransaction)
|
|
3612
|
+
throw this.createError("MAINTENANCE_REQUIRED", `${tableName} (put inside a transaction on update-log table)`);
|
|
3613
|
+
return this.putViaLog(tableName, data, where, options, returnUpdatedData, _whereIsLinesNumbers);
|
|
3614
|
+
}
|
|
2976
3615
|
let clonedData = structuredClone(data);
|
|
2977
3616
|
if (!where) {
|
|
2978
3617
|
if (Utils.isArrayOfObjects(clonedData)) {
|
|
@@ -3216,6 +3855,13 @@ export default class Inibase {
|
|
|
3216
3855
|
await this.ensureDatabaseRecovered();
|
|
3217
3856
|
const tablePath = join(this.databasePath, tableName);
|
|
3218
3857
|
await this.throwErrorIfTableEmpty(tableName);
|
|
3858
|
+
if (this.isLogTable(tableName) && this.activeTransaction)
|
|
3859
|
+
throw this.createError("MAINTENANCE_REQUIRED", `${tableName} (delete inside a transaction on update-log table)`);
|
|
3860
|
+
// Fold any pending puts into the base before a tombstone rewrite, so the
|
|
3861
|
+
// log is empty and the rewritten base carries every acknowledged value.
|
|
3862
|
+
// `compact` is a no-op when there is nothing to fold.
|
|
3863
|
+
if (this.isLogTable(tableName))
|
|
3864
|
+
await this.compact(tableName);
|
|
3219
3865
|
if (!where) {
|
|
3220
3866
|
let txnStaged = false;
|
|
3221
3867
|
// Crash-atomic truncate: park every column file (pure removal
|
|
@@ -3493,11 +4139,11 @@ export default class Inibase {
|
|
|
3493
4139
|
// the default page size (15): every matching row counts.
|
|
3494
4140
|
const lineNumbers = await this.get(tableName, where, { perPage: -1 }, undefined, true);
|
|
3495
4141
|
RETURN[column] = lineNumbers
|
|
3496
|
-
? await File.sum(columnPath, lineNumbers)
|
|
4142
|
+
? await File.sum(columnPath, lineNumbers, await this.overlayColumnOverrides(tableName, column))
|
|
3497
4143
|
: 0;
|
|
3498
4144
|
}
|
|
3499
4145
|
else
|
|
3500
|
-
RETURN[column] = await File.sum(columnPath);
|
|
4146
|
+
RETURN[column] = await File.sum(columnPath, undefined, await this.overlayColumnOverrides(tableName, column));
|
|
3501
4147
|
}
|
|
3502
4148
|
}
|
|
3503
4149
|
return columns.length > 1 ? RETURN : Object.values(RETURN)[0];
|
|
@@ -3682,11 +4328,11 @@ export default class Inibase {
|
|
|
3682
4328
|
if (where) {
|
|
3683
4329
|
const lineNumbers = await this.get(tableName, where, { perPage: -1 }, undefined, true);
|
|
3684
4330
|
RETURN[column] = lineNumbers
|
|
3685
|
-
? await File.avg(columnPath, lineNumbers)
|
|
4331
|
+
? await File.avg(columnPath, lineNumbers, await this.overlayColumnOverrides(tableName, column))
|
|
3686
4332
|
: 0;
|
|
3687
4333
|
}
|
|
3688
4334
|
else
|
|
3689
|
-
RETURN[column] = await File.avg(columnPath);
|
|
4335
|
+
RETURN[column] = await File.avg(columnPath, undefined, await this.overlayColumnOverrides(tableName, column));
|
|
3690
4336
|
}
|
|
3691
4337
|
}
|
|
3692
4338
|
return columns.length > 1 ? RETURN : Object.values(RETURN)[0];
|
|
@@ -3706,11 +4352,11 @@ export default class Inibase {
|
|
|
3706
4352
|
if (where) {
|
|
3707
4353
|
const lineNumbers = await this.get(tableName, where, { perPage: -1 }, undefined, true);
|
|
3708
4354
|
RETURN[column] = lineNumbers
|
|
3709
|
-
? await File.max(columnPath, lineNumbers)
|
|
4355
|
+
? await File.max(columnPath, lineNumbers, await this.overlayColumnOverrides(tableName, column))
|
|
3710
4356
|
: 0;
|
|
3711
4357
|
}
|
|
3712
4358
|
else
|
|
3713
|
-
RETURN[column] = await File.max(columnPath);
|
|
4359
|
+
RETURN[column] = await File.max(columnPath, undefined, await this.overlayColumnOverrides(tableName, column));
|
|
3714
4360
|
}
|
|
3715
4361
|
}
|
|
3716
4362
|
return RETURN;
|
|
@@ -3730,11 +4376,11 @@ export default class Inibase {
|
|
|
3730
4376
|
if (where) {
|
|
3731
4377
|
const lineNumbers = await this.get(tableName, where, { perPage: -1 }, undefined, true);
|
|
3732
4378
|
RETURN[column] = lineNumbers
|
|
3733
|
-
? await File.min(columnPath, lineNumbers)
|
|
4379
|
+
? await File.min(columnPath, lineNumbers, await this.overlayColumnOverrides(tableName, column))
|
|
3734
4380
|
: 0;
|
|
3735
4381
|
}
|
|
3736
4382
|
else
|
|
3737
|
-
RETURN[column] = await File.min(columnPath);
|
|
4383
|
+
RETURN[column] = await File.min(columnPath, undefined, await this.overlayColumnOverrides(tableName, column));
|
|
3738
4384
|
}
|
|
3739
4385
|
}
|
|
3740
4386
|
return RETURN;
|