@zakkster/lite-bake-stream 1.0.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/CHANGELOG.md +457 -0
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/SPEC.md +364 -0
- package/llms.txt +81 -0
- package/package.json +117 -0
- package/src/FileIngest.js +104 -0
- package/src/MultiReader.js +160 -0
- package/src/PreserveReader.js +180 -0
- package/src/PreserveTokenizer.js +172 -0
- package/src/PreserveWriter.js +218 -0
- package/src/RangeReader.js +470 -0
- package/src/Reader.js +349 -0
- package/src/Split.js +359 -0
- package/src/StringTable.js +225 -0
- package/src/Tokenizer.js +691 -0
- package/src/Writer.js +713 -0
- package/src/index.js +193 -0
- package/types/FileIngest.d.ts +44 -0
- package/types/MultiReader.d.ts +36 -0
- package/types/PreserveReader.d.ts +44 -0
- package/types/PreserveTokenizer.d.ts +27 -0
- package/types/PreserveWriter.d.ts +35 -0
- package/types/RangeReader.d.ts +93 -0
- package/types/Reader.d.ts +85 -0
- package/types/Split.d.ts +66 -0
- package/types/StringTable.d.ts +23 -0
- package/types/Tokenizer.d.ts +42 -0
- package/types/Writer.d.ts +92 -0
- package/types/index.d.ts +58 -0
package/src/Writer.js
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
// @zakkster/lite-bake-stream / Writer
|
|
2
|
+
// LBK1 shard emitter. Plugs into Tokenizer as a sink.
|
|
3
|
+
// Copyright (c) 2026 Zahary Shinikchiev. MIT.
|
|
4
|
+
//
|
|
5
|
+
// Two modes:
|
|
6
|
+
// - Explicit schema: consumer passes { fields: [names...] } or
|
|
7
|
+
// { fields: [{name, laneKind}...] }; no sampling.
|
|
8
|
+
// - Sample-and-infer (default): first `sampleBytes` of input buffered in a
|
|
9
|
+
// columnar staging area, per-field lane kind inferred from observed
|
|
10
|
+
// value types (number->F64, string->U32-into-string-table). Fields that
|
|
11
|
+
// saw both raise W_MIXED_LANE_TYPES at freeze.
|
|
12
|
+
//
|
|
13
|
+
// v1 lane kinds: F64 (numeric) and U32 (string-table index). Per-shard string
|
|
14
|
+
// tables ride immediately after each shard's payload; the shard directory
|
|
15
|
+
// carries local_string_off/local_string_len so each shard is self-contained.
|
|
16
|
+
//
|
|
17
|
+
// Post-freeze allocation profile: ZERO per record on the numeric hot path.
|
|
18
|
+
// String fields intern via the per-shard StringTable which allocates only on
|
|
19
|
+
// unique inserts (bounded by cardinality, not row count).
|
|
20
|
+
//
|
|
21
|
+
// Error codes (stable):
|
|
22
|
+
// W_TOP_LEVEL_NOT_OBJECT - top-level value is not an object
|
|
23
|
+
// W_NESTED_UNSUPPORTED - nested object/array inside a record
|
|
24
|
+
// W_EMPTY_INPUT - finalize() with zero records seen
|
|
25
|
+
// W_ROW_STRIDE_ZERO - schema has zero fields (nothing to store)
|
|
26
|
+
// W_MIXED_LANE_TYPES - a field saw both number and string values
|
|
27
|
+
// W_LANE_MISMATCH - post-freeze: value type doesn't match schema lane
|
|
28
|
+
// W_UNKNOWN_LANE_KIND - explicit schema declares an unknown laneKind
|
|
29
|
+
|
|
30
|
+
import { StringTable } from './StringTable.js';
|
|
31
|
+
|
|
32
|
+
export const VERSION = '1.0.0';
|
|
33
|
+
|
|
34
|
+
const CONTAINER_HEADER_BYTES = 48;
|
|
35
|
+
const SHARD_ENTRY_BYTES = 40;
|
|
36
|
+
const FIELD_DESCRIPTOR_BYTES = 24;
|
|
37
|
+
const FOOTER_BYTES = 16;
|
|
38
|
+
|
|
39
|
+
const LANE_F64 = 1;
|
|
40
|
+
const LANE_U32 = 3;
|
|
41
|
+
const FIELD_FLAGS_NONE = 0;
|
|
42
|
+
|
|
43
|
+
const SHARD_MIN_READER_VERSION = 1;
|
|
44
|
+
const SHARD_FLAGS_NONE = 0;
|
|
45
|
+
|
|
46
|
+
const FORMAT_VERSION = 1;
|
|
47
|
+
const ENDIAN_LE = 1;
|
|
48
|
+
|
|
49
|
+
const DEFAULT_SHARD_BYTES = 32 * 1024 * 1024;
|
|
50
|
+
const DEFAULT_SAMPLE_ROWS_CAPACITY = 4096;
|
|
51
|
+
|
|
52
|
+
// Kind tags for sample-time bookkeeping
|
|
53
|
+
const K_UNKNOWN = 0;
|
|
54
|
+
const K_NUMBER = 1;
|
|
55
|
+
const K_STRING = 2;
|
|
56
|
+
const K_MIXED = 3;
|
|
57
|
+
|
|
58
|
+
function laneKindBytes(k) { return k === LANE_F64 ? 8 : (k === LANE_U32 ? 4 : 0); }
|
|
59
|
+
function normalizeLaneKindStr(s) {
|
|
60
|
+
if (typeof s !== 'string') return s;
|
|
61
|
+
const t = s.toLowerCase();
|
|
62
|
+
if (t === 'f64') return LANE_F64;
|
|
63
|
+
if (t === 'u32') return LANE_U32;
|
|
64
|
+
return -1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class WriterError extends Error {
|
|
68
|
+
constructor(code, msg) {
|
|
69
|
+
super(msg);
|
|
70
|
+
this.code = code;
|
|
71
|
+
this.name = 'WriterError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hashBytes(bytes, from, to) {
|
|
76
|
+
let h = 0x811c9dc5 | 0;
|
|
77
|
+
for (let i = from; i < to; i++) {
|
|
78
|
+
h = (h ^ bytes[i]) | 0;
|
|
79
|
+
h = Math.imul(h, 0x01000193) | 0;
|
|
80
|
+
}
|
|
81
|
+
return h >>> 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function bytesEqual(a, aFrom, aTo, b) {
|
|
85
|
+
const n = aTo - aFrom;
|
|
86
|
+
if (n !== b.length) return false;
|
|
87
|
+
for (let i = 0; i < n; i++) if (a[aFrom + i] !== b[i]) return false;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// SampleBuffer: columnar SoA during sample window. Each field independently
|
|
92
|
+
// records observed kind (numeric or string). Numeric values -> F64 column;
|
|
93
|
+
// string values -> U32 column, interned into a shared string table that is
|
|
94
|
+
// handed to shard 0 at drain time.
|
|
95
|
+
class SampleBuffer {
|
|
96
|
+
constructor(initialCapacity, stringTable) {
|
|
97
|
+
this._rowCount = 0;
|
|
98
|
+
this._capacity = initialCapacity;
|
|
99
|
+
this._fieldOrder = []; // Array<string>
|
|
100
|
+
this._fieldMeta = new Map(); // fieldName -> { kind, columnF64?, columnU32? }
|
|
101
|
+
this._byteEstimate = 0;
|
|
102
|
+
this._stringTable = stringTable;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setNumber(name, value) {
|
|
106
|
+
let meta = this._ensureField(name);
|
|
107
|
+
if (meta.kind === K_UNKNOWN) { meta.kind = K_NUMBER; meta.columnF64 = new Float64Array(this._capacity); }
|
|
108
|
+
else if (meta.kind === K_STRING) { meta.kind = K_MIXED; return; }
|
|
109
|
+
else if (meta.kind === K_MIXED) { return; }
|
|
110
|
+
meta.columnF64[this._rowCount] = value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setString(name, bytes, from, to) {
|
|
114
|
+
let meta = this._ensureField(name);
|
|
115
|
+
if (meta.kind === K_UNKNOWN) { meta.kind = K_STRING; meta.columnU32 = new Uint32Array(this._capacity); }
|
|
116
|
+
else if (meta.kind === K_NUMBER) { meta.kind = K_MIXED; return; }
|
|
117
|
+
else if (meta.kind === K_MIXED) { return; }
|
|
118
|
+
const idx = this._stringTable.intern(bytes, from, to);
|
|
119
|
+
meta.columnU32[this._rowCount] = idx;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_ensureField(name) {
|
|
123
|
+
let meta = this._fieldMeta.get(name);
|
|
124
|
+
if (!meta) {
|
|
125
|
+
meta = { kind: K_UNKNOWN, columnF64: null, columnU32: null };
|
|
126
|
+
this._fieldMeta.set(name, meta);
|
|
127
|
+
this._fieldOrder.push(name);
|
|
128
|
+
}
|
|
129
|
+
return meta;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
commitRow(inputBytesConsumed) {
|
|
133
|
+
this._rowCount++;
|
|
134
|
+
this._byteEstimate += inputBytesConsumed;
|
|
135
|
+
if (this._rowCount >= this._capacity) this._growCapacity();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_growCapacity() {
|
|
139
|
+
const newCap = this._capacity * 2;
|
|
140
|
+
for (const name of this._fieldOrder) {
|
|
141
|
+
const meta = this._fieldMeta.get(name);
|
|
142
|
+
if (meta.columnF64) {
|
|
143
|
+
const grown = new Float64Array(newCap); grown.set(meta.columnF64); meta.columnF64 = grown;
|
|
144
|
+
}
|
|
145
|
+
if (meta.columnU32) {
|
|
146
|
+
const grown = new Uint32Array(newCap); grown.set(meta.columnU32); meta.columnU32 = grown;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
this._capacity = newCap;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get byteEstimate() { return this._byteEstimate; }
|
|
153
|
+
get rowCount() { return this._rowCount; }
|
|
154
|
+
get fieldOrder() { return this._fieldOrder; }
|
|
155
|
+
getFieldMeta(name) { return this._fieldMeta.get(name); }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class Writer {
|
|
159
|
+
constructor(opts) {
|
|
160
|
+
opts = opts || {};
|
|
161
|
+
this._targetShardBytes = opts.targetShardBytes || DEFAULT_SHARD_BYTES;
|
|
162
|
+
this._sampleBytes = opts.sampleBytes || this._targetShardBytes;
|
|
163
|
+
this._explicitSchema = opts.schema || null;
|
|
164
|
+
|
|
165
|
+
// Frozen schema state
|
|
166
|
+
this._schema = null; // { fields: [{name, laneKind, offsetInRow}], rowStride }
|
|
167
|
+
this._fieldNamesUtf8 = null;
|
|
168
|
+
this._fieldNameHashes = null;
|
|
169
|
+
this._fieldLaneKinds = null; // Uint8Array — per-field LANE_* (fast dispatch)
|
|
170
|
+
this._fieldOffsets = null; // Uint16Array — per-field byte offset in row
|
|
171
|
+
this._rowValueSlotsF64 = null; // Float64Array<fieldCount> scratch
|
|
172
|
+
this._rowValueSlotsU32 = null; // Uint32Array<fieldCount> scratch
|
|
173
|
+
|
|
174
|
+
// Sample-window state
|
|
175
|
+
this._stringTable = new StringTable();
|
|
176
|
+
this._sample = new SampleBuffer(DEFAULT_SAMPLE_ROWS_CAPACITY, this._stringTable);
|
|
177
|
+
|
|
178
|
+
// Shard state
|
|
179
|
+
this._currentShardBuffer = null;
|
|
180
|
+
this._currentShardBytes = null; // Uint8Array view over buffer
|
|
181
|
+
this._currentShardRowCount = 0;
|
|
182
|
+
this._currentShardMaxRows = 0;
|
|
183
|
+
this._shards = []; // { bytes, rowCount, stringTableBytes }
|
|
184
|
+
this._totalRows = 0;
|
|
185
|
+
|
|
186
|
+
// Per-record parse state
|
|
187
|
+
this._recordDepth = 0;
|
|
188
|
+
this._currentKeyBytes = null;
|
|
189
|
+
this._currentKeyFrom = 0;
|
|
190
|
+
this._currentKeyTo = 0;
|
|
191
|
+
this._currentFieldIdx = -1;
|
|
192
|
+
this._recordStartByteOffset = 0;
|
|
193
|
+
this._absOffset = 0;
|
|
194
|
+
|
|
195
|
+
this._finalized = false;
|
|
196
|
+
|
|
197
|
+
if (this._explicitSchema) this._freezeSchemaExplicit(this._explicitSchema.fields);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// -------- sink protocol --------
|
|
201
|
+
|
|
202
|
+
onStartObject() {
|
|
203
|
+
this._recordDepth++;
|
|
204
|
+
if (this._recordDepth > 1) throw new WriterError('W_NESTED_UNSUPPORTED', 'nested object inside record');
|
|
205
|
+
if (this._schema) {
|
|
206
|
+
// clear scratch slots
|
|
207
|
+
const f = this._rowValueSlotsF64, u = this._rowValueSlotsU32;
|
|
208
|
+
for (let i = 0; i < f.length; i++) { f[i] = 0; u[i] = 0; }
|
|
209
|
+
}
|
|
210
|
+
this._recordStartByteOffset = this._absOffset;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
onEndObject() {
|
|
214
|
+
this._recordDepth--;
|
|
215
|
+
if (this._recordDepth === 0) this._commitRecord();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
onStartArray() {
|
|
219
|
+
if (this._recordDepth === 0) throw new WriterError('W_TOP_LEVEL_NOT_OBJECT', 'top-level array (in-record) not supported');
|
|
220
|
+
throw new WriterError('W_NESTED_UNSUPPORTED', 'nested array inside record');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
onEndArray() {}
|
|
224
|
+
|
|
225
|
+
onKey(bytes, from, to) {
|
|
226
|
+
if (this._schema) {
|
|
227
|
+
// Post-freeze: resolve to field index NOW; the bytes are ephemeral (SPEC 5.3).
|
|
228
|
+
this._currentFieldIdx = this._lookupFieldIdx(bytes, from, to);
|
|
229
|
+
} else {
|
|
230
|
+
// Sample window: decode to a JS string NOW. The tokenizer will reuse this
|
|
231
|
+
// buffer for the value bytes before onNumber/onString fires, so we cannot
|
|
232
|
+
// defer decoding. String allocation here is expected — bounded to the
|
|
233
|
+
// sample window; steady-state post-freeze remains zero-alloc.
|
|
234
|
+
this._currentKeyName = new TextDecoder().decode(bytes.subarray(from, to));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
onNumber(v) {
|
|
239
|
+
if (this._recordDepth !== 1) return;
|
|
240
|
+
if (this._schema) {
|
|
241
|
+
const idx = this._currentFieldIdx;
|
|
242
|
+
if (idx < 0) return;
|
|
243
|
+
const lane = this._fieldLaneKinds[idx];
|
|
244
|
+
if (lane !== LANE_F64) throw new WriterError('W_LANE_MISMATCH',
|
|
245
|
+
'field ' + this._schema.fields[idx].name + ' is not an F64 lane but got a number');
|
|
246
|
+
this._rowValueSlotsF64[idx] = v;
|
|
247
|
+
} else {
|
|
248
|
+
if (!this._currentKeyName) return;
|
|
249
|
+
this._sample.setNumber(this._currentKeyName, v);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
onTrue() { this.onNumber(1); }
|
|
254
|
+
onFalse() { this.onNumber(0); }
|
|
255
|
+
onNull() { this.onNumber(0); }
|
|
256
|
+
|
|
257
|
+
onString(bytes, from, to) {
|
|
258
|
+
if (this._recordDepth === 0) return;
|
|
259
|
+
if (this._schema) {
|
|
260
|
+
const idx = this._currentFieldIdx;
|
|
261
|
+
if (idx < 0) return;
|
|
262
|
+
const lane = this._fieldLaneKinds[idx];
|
|
263
|
+
if (lane !== LANE_U32) throw new WriterError('W_LANE_MISMATCH',
|
|
264
|
+
'field ' + this._schema.fields[idx].name + ' is not a U32 lane but got a string');
|
|
265
|
+
const strIdx = this._currentShardStringTable().intern(bytes, from, to);
|
|
266
|
+
this._rowValueSlotsU32[idx] = strIdx;
|
|
267
|
+
} else {
|
|
268
|
+
if (!this._currentKeyName) return;
|
|
269
|
+
this._sample.setString(this._currentKeyName, bytes, from, to);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
onEnd() {}
|
|
274
|
+
|
|
275
|
+
// The current shard's string table. On the first record post-freeze we
|
|
276
|
+
// hand over the sample's string table (which already contains the drained
|
|
277
|
+
// records' strings). Subsequent shards get a fresh reset table.
|
|
278
|
+
_currentShardStringTable() {
|
|
279
|
+
if (!this._perShardStringTable) this._perShardStringTable = this._stringTable;
|
|
280
|
+
return this._perShardStringTable;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// -------- schema management --------
|
|
284
|
+
|
|
285
|
+
_freezeSchemaExplicit(rawFields) {
|
|
286
|
+
if (!rawFields || rawFields.length === 0) throw new WriterError('W_ROW_STRIDE_ZERO', 'schema has zero fields');
|
|
287
|
+
const fields = new Array(rawFields.length);
|
|
288
|
+
let offset = 0;
|
|
289
|
+
for (let i = 0; i < rawFields.length; i++) {
|
|
290
|
+
const rf = rawFields[i];
|
|
291
|
+
let name, laneKind;
|
|
292
|
+
if (typeof rf === 'string') { name = rf; laneKind = LANE_F64; }
|
|
293
|
+
else {
|
|
294
|
+
name = rf.name;
|
|
295
|
+
const asStr = normalizeLaneKindStr(rf.laneKind);
|
|
296
|
+
laneKind = typeof rf.laneKind === 'number' ? rf.laneKind : asStr;
|
|
297
|
+
if (laneKind !== LANE_F64 && laneKind !== LANE_U32)
|
|
298
|
+
throw new WriterError('W_UNKNOWN_LANE_KIND', 'field ' + name + ' laneKind=' + rf.laneKind);
|
|
299
|
+
}
|
|
300
|
+
fields[i] = { name, laneKind, offsetInRow: offset };
|
|
301
|
+
offset += laneKindBytes(laneKind);
|
|
302
|
+
}
|
|
303
|
+
// Pad row stride to 8 (per SPEC 3.5)
|
|
304
|
+
const rowStride = (offset + 7) & ~7;
|
|
305
|
+
this._finalizeSchema(fields, rowStride);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_freezeSchemaFromSample() {
|
|
309
|
+
const names = this._sample.fieldOrder;
|
|
310
|
+
if (names.length === 0) throw new WriterError('W_ROW_STRIDE_ZERO', 'sample discovered no fields');
|
|
311
|
+
const fields = new Array(names.length);
|
|
312
|
+
let offset = 0;
|
|
313
|
+
for (let i = 0; i < names.length; i++) {
|
|
314
|
+
const name = names[i];
|
|
315
|
+
const meta = this._sample.getFieldMeta(name);
|
|
316
|
+
let laneKind;
|
|
317
|
+
if (meta.kind === K_MIXED) throw new WriterError('W_MIXED_LANE_TYPES',
|
|
318
|
+
'field ' + name + ' had both numeric and string values in sample');
|
|
319
|
+
if (meta.kind === K_STRING) laneKind = LANE_U32;
|
|
320
|
+
else laneKind = LANE_F64; // K_NUMBER or K_UNKNOWN default
|
|
321
|
+
fields[i] = { name, laneKind, offsetInRow: offset };
|
|
322
|
+
offset += laneKindBytes(laneKind);
|
|
323
|
+
}
|
|
324
|
+
const rowStride = (offset + 7) & ~7;
|
|
325
|
+
this._finalizeSchema(fields, rowStride);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_finalizeSchema(fields, rowStride) {
|
|
329
|
+
this._schema = { fields, rowStride };
|
|
330
|
+
|
|
331
|
+
const enc = new TextEncoder();
|
|
332
|
+
this._fieldNamesUtf8 = new Array(fields.length);
|
|
333
|
+
this._fieldNameHashes = new Uint32Array(fields.length);
|
|
334
|
+
this._fieldLaneKinds = new Uint8Array(fields.length);
|
|
335
|
+
this._fieldOffsets = new Uint16Array(fields.length);
|
|
336
|
+
for (let i = 0; i < fields.length; i++) {
|
|
337
|
+
const b = enc.encode(fields[i].name);
|
|
338
|
+
this._fieldNamesUtf8[i] = b;
|
|
339
|
+
this._fieldNameHashes[i] = hashBytes(b, 0, b.length);
|
|
340
|
+
this._fieldLaneKinds[i] = fields[i].laneKind;
|
|
341
|
+
this._fieldOffsets[i] = fields[i].offsetInRow;
|
|
342
|
+
}
|
|
343
|
+
this._rowValueSlotsF64 = new Float64Array(fields.length);
|
|
344
|
+
this._rowValueSlotsU32 = new Uint32Array(fields.length);
|
|
345
|
+
this._currentShardMaxRows = Math.max(1, Math.floor(this._targetShardBytes / rowStride));
|
|
346
|
+
|
|
347
|
+
// Zone-map tracking (M7): every F64 field gets a min/max slot per shard.
|
|
348
|
+
// trackedFieldIndices[t] = schema field index; trackedFieldToPos[schemaFieldIdx]
|
|
349
|
+
// = t or -1. The row-commit hot path uses trackedFieldToPos for O(1) dispatch.
|
|
350
|
+
const tracked = [];
|
|
351
|
+
for (let i = 0; i < fields.length; i++) {
|
|
352
|
+
if (fields[i].laneKind === LANE_F64) tracked.push(i);
|
|
353
|
+
}
|
|
354
|
+
this._trackedFieldIndices = new Uint16Array(tracked);
|
|
355
|
+
this._trackedFieldToPos = new Int32Array(fields.length);
|
|
356
|
+
for (let i = 0; i < fields.length; i++) this._trackedFieldToPos[i] = -1;
|
|
357
|
+
for (let t = 0; t < tracked.length; t++) this._trackedFieldToPos[tracked[t]] = t;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
_lookupFieldIdx(bytes, from, to) {
|
|
361
|
+
const h = hashBytes(bytes, from, to);
|
|
362
|
+
const hashes = this._fieldNameHashes;
|
|
363
|
+
const names = this._fieldNamesUtf8;
|
|
364
|
+
for (let i = 0; i < hashes.length; i++) {
|
|
365
|
+
if (hashes[i] === h && bytesEqual(bytes, from, to, names[i])) return i;
|
|
366
|
+
}
|
|
367
|
+
return -1;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// -------- record write path --------
|
|
371
|
+
|
|
372
|
+
_commitRecord() {
|
|
373
|
+
if (this._schema) {
|
|
374
|
+
if (!this._currentShardBuffer) this._allocateCurrentShard();
|
|
375
|
+
const row = this._currentShardRowCount;
|
|
376
|
+
const stride = this._schema.rowStride;
|
|
377
|
+
const rowOff = row * stride;
|
|
378
|
+
const dv = this._currentShardDv;
|
|
379
|
+
const kinds = this._fieldLaneKinds;
|
|
380
|
+
const offs = this._fieldOffsets;
|
|
381
|
+
const f = this._rowValueSlotsF64;
|
|
382
|
+
const u = this._rowValueSlotsU32;
|
|
383
|
+
const trackedIdx = this._trackedFieldToPos; // Int32Array: schemaFieldIdx -> trackingPos or -1
|
|
384
|
+
const mins = this._currentShardMins;
|
|
385
|
+
const maxes = this._currentShardMaxes;
|
|
386
|
+
for (let i = 0; i < kinds.length; i++) {
|
|
387
|
+
const fo = rowOff + offs[i];
|
|
388
|
+
if (kinds[i] === LANE_F64) {
|
|
389
|
+
const v = f[i];
|
|
390
|
+
dv.setFloat64(fo, v, true);
|
|
391
|
+
const t = trackedIdx[i];
|
|
392
|
+
if (t >= 0) {
|
|
393
|
+
if (v < mins[t]) mins[t] = v;
|
|
394
|
+
if (v > maxes[t]) maxes[t] = v;
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
dv.setUint32(fo, u[i], true);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
this._currentShardRowCount++;
|
|
401
|
+
this._totalRows++;
|
|
402
|
+
if (this._currentShardRowCount >= this._currentShardMaxRows) this._finalizeCurrentShard();
|
|
403
|
+
} else {
|
|
404
|
+
const consumed = Math.max(1, this._absOffset - this._recordStartByteOffset);
|
|
405
|
+
this._sample.commitRow(consumed);
|
|
406
|
+
if (this._sample.byteEstimate >= this._sampleBytes) {
|
|
407
|
+
this._freezeSchemaFromSample();
|
|
408
|
+
this._drainSampleToShards();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
_allocateCurrentShard() {
|
|
414
|
+
const stride = this._schema.rowStride;
|
|
415
|
+
const bytes = this._currentShardMaxRows * stride;
|
|
416
|
+
this._currentShardBuffer = new ArrayBuffer(bytes);
|
|
417
|
+
this._currentShardBytes = new Uint8Array(this._currentShardBuffer);
|
|
418
|
+
this._currentShardDv = new DataView(this._currentShardBuffer);
|
|
419
|
+
this._currentShardRowCount = 0;
|
|
420
|
+
// Zone-map tracking: per-F64-field min/max for this shard. Initialized to
|
|
421
|
+
// +Inf / -Inf sentinels; every row's write path updates them. Only F64
|
|
422
|
+
// fields participate (M7 scope); the trackedFieldIndices map converts a
|
|
423
|
+
// schema field index -> position in the tracking arrays.
|
|
424
|
+
const T = this._trackedFieldIndices.length;
|
|
425
|
+
this._currentShardMins = new Float64Array(T);
|
|
426
|
+
this._currentShardMaxes = new Float64Array(T);
|
|
427
|
+
for (let i = 0; i < T; i++) {
|
|
428
|
+
this._currentShardMins[i] = Number.POSITIVE_INFINITY;
|
|
429
|
+
this._currentShardMaxes[i] = Number.NEGATIVE_INFINITY;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
_finalizeCurrentShard() {
|
|
434
|
+
if (this._currentShardRowCount === 0) return;
|
|
435
|
+
const usedBytes = this._currentShardRowCount * this._schema.rowStride;
|
|
436
|
+
const copy = new Uint8Array(usedBytes);
|
|
437
|
+
copy.set(this._currentShardBytes.subarray(0, usedBytes));
|
|
438
|
+
// Serialize the current shard's string table (empty for F64-only shards).
|
|
439
|
+
const st = this._perShardStringTable || this._stringTable;
|
|
440
|
+
const stSer = st.serialize();
|
|
441
|
+
// Snapshot zone-map bounds for this shard. Copy so subsequent shards can
|
|
442
|
+
// reuse _currentShardMins/Maxes without clobbering the pushed record.
|
|
443
|
+
const T = this._trackedFieldIndices.length;
|
|
444
|
+
const shardMins = new Float64Array(T);
|
|
445
|
+
const shardMaxes = new Float64Array(T);
|
|
446
|
+
shardMins.set(this._currentShardMins);
|
|
447
|
+
shardMaxes.set(this._currentShardMaxes);
|
|
448
|
+
this._shards.push({
|
|
449
|
+
bytes: copy,
|
|
450
|
+
rowCount: this._currentShardRowCount,
|
|
451
|
+
stringTableBytes: stSer.bytes,
|
|
452
|
+
mins: shardMins,
|
|
453
|
+
maxes: shardMaxes,
|
|
454
|
+
});
|
|
455
|
+
this._currentShardBuffer = null;
|
|
456
|
+
this._currentShardBytes = null;
|
|
457
|
+
this._currentShardDv = null;
|
|
458
|
+
this._currentShardRowCount = 0;
|
|
459
|
+
// Reset string table for next shard (per-shard independence)
|
|
460
|
+
st.reset();
|
|
461
|
+
this._perShardStringTable = st;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
_drainSampleToShards() {
|
|
465
|
+
const rowCount = this._sample.rowCount;
|
|
466
|
+
const fields = this._schema.fields;
|
|
467
|
+
const cols = new Array(fields.length);
|
|
468
|
+
for (let i = 0; i < fields.length; i++) {
|
|
469
|
+
const meta = this._sample.getFieldMeta(fields[i].name);
|
|
470
|
+
cols[i] = fields[i].laneKind === LANE_F64 ? (meta ? meta.columnF64 : null)
|
|
471
|
+
: (meta ? meta.columnU32 : null);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// The sample interned all strings into a SHARED table. When the sample
|
|
475
|
+
// overflows multiple output shards, each shard needs its own self-contained
|
|
476
|
+
// string table (SPEC 3.3 per-shard independence for HTTP Range fetches).
|
|
477
|
+
// Solution: re-intern per shard during drain. Look up the original bytes
|
|
478
|
+
// in the sample table, intern into a per-shard fresh table, remap the U32
|
|
479
|
+
// index. This preserves per-shard independence at the cost of a modest
|
|
480
|
+
// string-table copy for tags that appear across shards.
|
|
481
|
+
const sampleTable = this._stringTable;
|
|
482
|
+
this._perShardStringTable = new StringTable();
|
|
483
|
+
|
|
484
|
+
let rowsRemaining = rowCount;
|
|
485
|
+
let srcRow = 0;
|
|
486
|
+
while (rowsRemaining > 0) {
|
|
487
|
+
this._allocateCurrentShard();
|
|
488
|
+
const chunkRows = Math.min(rowsRemaining, this._currentShardMaxRows);
|
|
489
|
+
const dv = this._currentShardDv;
|
|
490
|
+
const stride = this._schema.rowStride;
|
|
491
|
+
const dstTable = this._perShardStringTable;
|
|
492
|
+
const trackedIdx = this._trackedFieldToPos;
|
|
493
|
+
const mins = this._currentShardMins;
|
|
494
|
+
const maxes = this._currentShardMaxes;
|
|
495
|
+
for (let r = 0; r < chunkRows; r++) {
|
|
496
|
+
const rowOff = r * stride;
|
|
497
|
+
for (let f = 0; f < fields.length; f++) {
|
|
498
|
+
const fo = rowOff + fields[f].offsetInRow;
|
|
499
|
+
const col = cols[f];
|
|
500
|
+
if (fields[f].laneKind === LANE_F64) {
|
|
501
|
+
const v = col ? col[srcRow + r] : 0;
|
|
502
|
+
dv.setFloat64(fo, v, true);
|
|
503
|
+
const t = trackedIdx[f];
|
|
504
|
+
if (t >= 0) {
|
|
505
|
+
if (v < mins[t]) mins[t] = v;
|
|
506
|
+
if (v > maxes[t]) maxes[t] = v;
|
|
507
|
+
}
|
|
508
|
+
} else {
|
|
509
|
+
const sampleIdx = col ? col[srcRow + r] : 0;
|
|
510
|
+
const strBytes = sampleTable.bytesAt(sampleIdx);
|
|
511
|
+
const newIdx = strBytes ? dstTable.intern(strBytes, 0, strBytes.length) : 0;
|
|
512
|
+
dv.setUint32(fo, newIdx, true);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
this._currentShardRowCount = chunkRows;
|
|
517
|
+
this._totalRows += chunkRows;
|
|
518
|
+
this._finalizeCurrentShard(); // serializes dstTable, resets in-place for the next iter
|
|
519
|
+
srcRow += chunkRows;
|
|
520
|
+
rowsRemaining -= chunkRows;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
this._sample = null;
|
|
524
|
+
// _perShardStringTable now holds a reset (empty) StringTable ready to
|
|
525
|
+
// receive strings from post-drain records that fill subsequent shards.
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
finalize() {
|
|
529
|
+
if (this._finalized) return this._container;
|
|
530
|
+
if (!this._schema) {
|
|
531
|
+
if (this._sample.rowCount === 0) throw new WriterError('W_EMPTY_INPUT', 'no records to write');
|
|
532
|
+
this._freezeSchemaFromSample();
|
|
533
|
+
this._drainSampleToShards();
|
|
534
|
+
}
|
|
535
|
+
if (this._currentShardRowCount > 0) this._finalizeCurrentShard();
|
|
536
|
+
if (this._shards.length === 0) throw new WriterError('W_EMPTY_INPUT', 'no records to write');
|
|
537
|
+
this._container = this._assembleContainer();
|
|
538
|
+
this._finalized = true;
|
|
539
|
+
return this._container;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
setInputByteOffset(n) { this._absOffset = n; }
|
|
543
|
+
|
|
544
|
+
// -------- container assembly --------
|
|
545
|
+
|
|
546
|
+
_assembleContainer() {
|
|
547
|
+
const enc = new TextEncoder();
|
|
548
|
+
const nameBytes = new Array(this._schema.fields.length);
|
|
549
|
+
let nameBlobLen = 0;
|
|
550
|
+
for (let i = 0; i < this._schema.fields.length; i++) {
|
|
551
|
+
nameBytes[i] = enc.encode(this._schema.fields[i].name);
|
|
552
|
+
nameBlobLen += nameBytes[i].length;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const fieldCount = this._schema.fields.length;
|
|
556
|
+
const descriptorBytes = fieldCount * FIELD_DESCRIPTOR_BYTES;
|
|
557
|
+
let schemaBlockBytes = 8 + descriptorBytes + 4 + nameBlobLen;
|
|
558
|
+
const schemaPad = (8 - (schemaBlockBytes & 7)) & 7;
|
|
559
|
+
schemaBlockBytes += schemaPad;
|
|
560
|
+
|
|
561
|
+
const shardCount = this._shards.length;
|
|
562
|
+
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
563
|
+
|
|
564
|
+
const schemaBlockOff = CONTAINER_HEADER_BYTES;
|
|
565
|
+
const shardDirOff = schemaBlockOff + schemaBlockBytes;
|
|
566
|
+
|
|
567
|
+
// Zone maps segment (M7). Placed between shard directory and first shard
|
|
568
|
+
// so a Reader loading header/schema/dir/zoneMaps up front does it in one
|
|
569
|
+
// contiguous byte range.
|
|
570
|
+
const T = this._trackedFieldIndices.length;
|
|
571
|
+
const zoneMapsEnabled = T > 0 && shardCount > 0;
|
|
572
|
+
let zoneMapsOff = 0;
|
|
573
|
+
let zoneMapsBytes = 0;
|
|
574
|
+
let zoneMapsFieldTableOff = 0;
|
|
575
|
+
let zoneMapsMinsOff = 0;
|
|
576
|
+
let zoneMapsMaxesOff = 0;
|
|
577
|
+
if (zoneMapsEnabled) {
|
|
578
|
+
zoneMapsOff = shardDirOff + shardDirBytes;
|
|
579
|
+
// Layout: 16-byte header + T*u16 field indices, padded to 8, then
|
|
580
|
+
// shardCount*T*8 mins, then shardCount*T*8 maxes.
|
|
581
|
+
const headerLen = 16;
|
|
582
|
+
const fieldTableLen = T * 2;
|
|
583
|
+
const fieldTablePad = (8 - (fieldTableLen & 7)) & 7;
|
|
584
|
+
zoneMapsFieldTableOff = zoneMapsOff + headerLen;
|
|
585
|
+
const minsRel = headerLen + fieldTableLen + fieldTablePad;
|
|
586
|
+
zoneMapsMinsOff = zoneMapsOff + minsRel;
|
|
587
|
+
const minsMaxesLen = shardCount * T * 8 * 2;
|
|
588
|
+
zoneMapsMaxesOff = zoneMapsMinsOff + shardCount * T * 8;
|
|
589
|
+
zoneMapsBytes = minsRel + minsMaxesLen;
|
|
590
|
+
}
|
|
591
|
+
const firstShardOff = shardDirOff + shardDirBytes + zoneMapsBytes;
|
|
592
|
+
|
|
593
|
+
// Compute per-shard payload and string-table offsets
|
|
594
|
+
let cursor = firstShardOff;
|
|
595
|
+
const shardPayloadOffsets = new Array(shardCount);
|
|
596
|
+
const shardPayloadPadded = new Array(shardCount);
|
|
597
|
+
const shardStringTableOffsets = new Array(shardCount);
|
|
598
|
+
const shardStringTableLens = new Array(shardCount);
|
|
599
|
+
for (let i = 0; i < shardCount; i++) {
|
|
600
|
+
const s = this._shards[i];
|
|
601
|
+
shardPayloadOffsets[i] = cursor;
|
|
602
|
+
const payloadLen = s.bytes.length;
|
|
603
|
+
const payloadPad = (8 - (payloadLen & 7)) & 7;
|
|
604
|
+
shardPayloadPadded[i] = payloadLen + payloadPad;
|
|
605
|
+
cursor += shardPayloadPadded[i];
|
|
606
|
+
// String table immediately follows the shard payload
|
|
607
|
+
shardStringTableOffsets[i] = cursor;
|
|
608
|
+
const stLen = s.stringTableBytes.length;
|
|
609
|
+
shardStringTableLens[i] = stLen;
|
|
610
|
+
cursor += stLen; // stringTable.serialize() already 8-byte padded
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const totalBytes = cursor + FOOTER_BYTES;
|
|
614
|
+
const container = new ArrayBuffer(totalBytes);
|
|
615
|
+
const dv = new DataView(container);
|
|
616
|
+
const bytes = new Uint8Array(container);
|
|
617
|
+
|
|
618
|
+
// Header
|
|
619
|
+
bytes[0] = 0x4C; bytes[1] = 0x42; bytes[2] = 0x4B; bytes[3] = 0x31;
|
|
620
|
+
dv.setUint16(4, FORMAT_VERSION, true);
|
|
621
|
+
bytes[6] = ENDIAN_LE;
|
|
622
|
+
bytes[7] = 0;
|
|
623
|
+
dv.setBigUint64(8, BigInt(schemaBlockOff), true);
|
|
624
|
+
dv.setBigUint64(16, BigInt(zoneMapsOff), true); // metadata_off — 0 iff no zone maps
|
|
625
|
+
dv.setBigUint64(24, BigInt(shardDirOff), true);
|
|
626
|
+
dv.setUint32(32, shardCount, true);
|
|
627
|
+
dv.setUint32(36, 0, true); // reserved1
|
|
628
|
+
dv.setBigUint64(40, BigInt(this._totalRows), true);
|
|
629
|
+
|
|
630
|
+
// Schema block
|
|
631
|
+
dv.setUint32(schemaBlockOff, fieldCount, true);
|
|
632
|
+
dv.setUint32(schemaBlockOff + 4, this._schema.rowStride, true);
|
|
633
|
+
let nameOff = 0;
|
|
634
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
635
|
+
const descOff = schemaBlockOff + 8 + i * FIELD_DESCRIPTOR_BYTES;
|
|
636
|
+
const f = this._schema.fields[i];
|
|
637
|
+
const nb = nameBytes[i];
|
|
638
|
+
dv.setUint16(descOff + 0, nb.length, true);
|
|
639
|
+
dv.setUint16(descOff + 2, f.offsetInRow, true);
|
|
640
|
+
bytes[descOff + 4] = f.laneKind;
|
|
641
|
+
bytes[descOff + 5] = FIELD_FLAGS_NONE;
|
|
642
|
+
dv.setUint16(descOff + 6, 0, true);
|
|
643
|
+
dv.setBigUint64(descOff + 8, BigInt(nameOff), true);
|
|
644
|
+
dv.setBigUint64(descOff + 16, 0n, true);
|
|
645
|
+
nameOff += nb.length;
|
|
646
|
+
}
|
|
647
|
+
const nameBlobLenOff = schemaBlockOff + 8 + descriptorBytes;
|
|
648
|
+
dv.setUint32(nameBlobLenOff, nameBlobLen, true);
|
|
649
|
+
let namePosOff = nameBlobLenOff + 4;
|
|
650
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
651
|
+
bytes.set(nameBytes[i], namePosOff);
|
|
652
|
+
namePosOff += nameBytes[i].length;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Shard directory
|
|
656
|
+
for (let i = 0; i < shardCount; i++) {
|
|
657
|
+
const entryOff = shardDirOff + i * SHARD_ENTRY_BYTES;
|
|
658
|
+
const s = this._shards[i];
|
|
659
|
+
dv.setBigUint64(entryOff + 0, BigInt(shardPayloadOffsets[i]), true);
|
|
660
|
+
dv.setUint32(entryOff + 8, s.bytes.length, true);
|
|
661
|
+
dv.setUint32(entryOff + 12, s.rowCount, true);
|
|
662
|
+
dv.setUint16(entryOff + 16, SHARD_MIN_READER_VERSION, true);
|
|
663
|
+
dv.setUint16(entryOff + 18, SHARD_FLAGS_NONE, true);
|
|
664
|
+
dv.setUint32(entryOff + 20, 0, true); // reserved
|
|
665
|
+
dv.setBigUint64(entryOff + 24, BigInt(shardStringTableOffsets[i]), true);
|
|
666
|
+
dv.setBigUint64(entryOff + 32, BigInt(shardStringTableLens[i]), true);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Zone maps segment (M7)
|
|
670
|
+
if (zoneMapsEnabled) {
|
|
671
|
+
// Header: 'ZM01' magic + shard_count + tracked_field_count + reserved0
|
|
672
|
+
bytes[zoneMapsOff + 0] = 0x30; bytes[zoneMapsOff + 1] = 0x5A;
|
|
673
|
+
bytes[zoneMapsOff + 2] = 0x4D; bytes[zoneMapsOff + 3] = 0x31;
|
|
674
|
+
dv.setUint32(zoneMapsOff + 4, shardCount, true);
|
|
675
|
+
dv.setUint32(zoneMapsOff + 8, T, true);
|
|
676
|
+
dv.setUint32(zoneMapsOff + 12, 0, true);
|
|
677
|
+
// Field index table
|
|
678
|
+
for (let t = 0; t < T; t++) {
|
|
679
|
+
dv.setUint16(zoneMapsFieldTableOff + t * 2, this._trackedFieldIndices[t], true);
|
|
680
|
+
}
|
|
681
|
+
// Mins / maxes: row-major, [shard * T + tracked]
|
|
682
|
+
for (let s = 0; s < shardCount; s++) {
|
|
683
|
+
const sh = this._shards[s];
|
|
684
|
+
for (let t = 0; t < T; t++) {
|
|
685
|
+
dv.setFloat64(zoneMapsMinsOff + (s * T + t) * 8, sh.mins[t], true);
|
|
686
|
+
dv.setFloat64(zoneMapsMaxesOff + (s * T + t) * 8, sh.maxes[t], true);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Payloads + local string tables
|
|
692
|
+
for (let i = 0; i < shardCount; i++) {
|
|
693
|
+
bytes.set(this._shards[i].bytes, shardPayloadOffsets[i]);
|
|
694
|
+
bytes.set(this._shards[i].stringTableBytes, shardStringTableOffsets[i]);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Footer
|
|
698
|
+
const footerOff = totalBytes - FOOTER_BYTES;
|
|
699
|
+
dv.setUint32(footerOff + 0, 0xFFFFFFFF, true);
|
|
700
|
+
dv.setUint32(footerOff + 4, 0, true);
|
|
701
|
+
bytes[footerOff + 8] = 0x31;
|
|
702
|
+
bytes[footerOff + 9] = 0x4B;
|
|
703
|
+
bytes[footerOff + 10] = 0x42;
|
|
704
|
+
bytes[footerOff + 11] = 0x4C;
|
|
705
|
+
dv.setUint32(footerOff + 12, FOOTER_BYTES, true);
|
|
706
|
+
|
|
707
|
+
return { buffer: container, totalRows: this._totalRows, shardCount, schema: this._schema };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
get schema() { return this._schema; }
|
|
711
|
+
get totalRows() { return this._totalRows; }
|
|
712
|
+
get shardCount() { return this._shards.length + (this._currentShardRowCount > 0 ? 1 : 0); }
|
|
713
|
+
}
|