-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathrecordEncoder.test.js
More file actions
179 lines (161 loc) · 7.54 KB
/
Copy pathrecordEncoder.test.js
File metadata and controls
179 lines (161 loc) · 7.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
require('../testUtils');
const assert = require('assert');
const { RecordEncoder, IndexRecordEncoder } = require('#src/resources/RecordEncoder');
const env = require('#js/utility/environment/environmentManager');
const terms = require('#src/utility/hdbTerms');
// In-memory shared structures (mirrors how a DBI shares the structures array under
// Symbol.for('structures')) so struct/record structures cross between encoder instances. We keep the
// live array reference rather than round-tripping through encode/decode, since msgpackr attaches
// bookkeeping to the structures array that a re-decode would strip.
function sharedStore() {
let structures = [];
return {
save(s) {
structures = s;
return true;
},
get() {
return structures;
},
};
}
function makeEncoder(store, extra) {
return new RecordEncoder({
// randomAccessStructure stays on regardless of the opt-out — reads must keep decoding typed
// structs so existing data is still readable; only writes change.
randomAccessStructure: true,
getStructures: store.get,
saveStructures: store.save,
...extra,
});
}
const record = { name: 'price', type: 'Float', indexed: true };
describe('RecordEncoder random-access fields opt-out (readOnlyStructures)', () => {
it('writes typed random-access structs by default (random-access fields on)', () => {
const store = sharedStore();
const enc = makeEncoder(store);
const bytes = enc.encode(record);
assert.ok(bytes[0] >= 0x20 && bytes[0] < 0x40, `expected typed-struct header byte, got 0x${bytes[0].toString(16)}`);
});
it('writes classic shared structures when readOnlyStructures is set (opt-out)', () => {
const store = sharedStore();
const enc = makeEncoder(store, { readOnlyStructures: true });
const bytes = enc.encode(record);
assert.ok(
bytes[0] >= 0x40 && bytes[0] < 0x80,
`expected classic shared-structure byte, got 0x${bytes[0].toString(16)}`
);
});
it('opt-out encoder still reads typed-struct data written before opting out', () => {
// Existing data was written as typed structs; after the user opts out the same store must keep
// decoding it (randomAccessStructure stays on), only new writes switch to classic.
const store = sharedStore();
const typedWriter = makeEncoder(store);
const typedBytes = typedWriter.encode(record);
assert.ok(typedBytes[0] >= 0x20 && typedBytes[0] < 0x40, 'precondition: typed-struct bytes');
const optedOutReader = makeEncoder(store, { readOnlyStructures: true });
const decoded = optedOutReader.decode(Buffer.from(typedBytes));
assert.ok(decoded, 'typed-struct data should still decode (not swallowed to null)');
assert.strictEqual(decoded.name, record.name);
assert.strictEqual(decoded.type, record.type);
assert.strictEqual(decoded.indexed, record.indexed);
});
it('classic records and typed structs round-trip together on the same store', () => {
const store = sharedStore();
const typedWriter = makeEncoder(store);
const classicWriter = makeEncoder(store, { readOnlyStructures: true });
const reader = makeEncoder(store);
const a = { id: 1, kind: 'typed' };
const b = { id: 2, kind: 'classic', extra: true };
const typedBytes = typedWriter.encode(a);
const classicBytes = classicWriter.encode(b);
assert.ok(typedBytes[0] >= 0x20 && typedBytes[0] < 0x40, 'precondition: typed bytes');
assert.ok(classicBytes[0] >= 0x40 && classicBytes[0] < 0x80, 'precondition: classic bytes');
// Typed structs decode with the RecordObject prototype, classic records as plain objects; spread
// to compare field values regardless of prototype.
assert.deepStrictEqual({ ...reader.decode(Buffer.from(typedBytes)) }, a);
assert.deepStrictEqual({ ...reader.decode(Buffer.from(classicBytes)) }, b);
});
it('IndexRecordEncoder keeps writing typed structs even with readOnlyStructures requested', () => {
// Object-store indexes (HNSW) must stay in struct mode regardless of the table opt-out.
const store = sharedStore();
const enc = new IndexRecordEncoder({
randomAccessStructure: true,
readOnlyStructures: true,
getStructures: store.get,
saveStructures: store.save,
});
const bytes = enc.encode(record);
assert.ok(
bytes[0] >= 0x20 && bytes[0] < 0x40,
`index encoder should ignore readOnlyStructures and write typed structs, got 0x${bytes[0].toString(16)}`
);
});
it('decodes a classic record whose structure-id byte is 66 (0x42) when noMetadata is set', () => {
// Regression: 66 (0x42) is classic shared-structure record-id #2 and also a rocksdb local-timestamp
// marker. On a rocksdb store the prefix heuristic strips 8 bytes and corrupts the record, decoding
// to null (the MQTT "publish non-JSON" failure). The audit store passes { noMetadata: true } to skip
// the heuristic for values that have no on-disk timestamp prefix.
const store = sharedStore();
const writer = makeEncoder(store, { readOnlyStructures: true });
// The 3rd distinct classic structure starts with byte 0x42; encode three shapes to reach it.
writer.encode({ a: 1 });
writer.encode({ b: 2, c: 3 });
const target = { d: 4, e: 5, f: 6 };
const bytes = Buffer.from(writer.encode(target));
assert.strictEqual(bytes[0], 66, 'precondition: target record begins with structure-id byte 66 (0x42)');
const reader = makeEncoder(store);
// Warm up the structure cache with a normal (non-rocksdb) decode so getStructures isn't needed
// once we flip isRocksDB below (the rocksdb getStructures path needs a rootStore we don't mock).
assert.deepStrictEqual(reader.decode(bytes), target, 'baseline classic decode works');
// Simulate the rocksdb decode path, where byte 66 collides with the local-timestamp marker.
reader.isRocksDB = true;
assert.deepStrictEqual(
reader.decode(bytes, { noMetadata: true }),
target,
'with noMetadata the classic record still decodes correctly'
);
// Without noMetadata the rocksdb timestamp heuristic misreads byte 66 and corrupts the decode.
assert.notDeepStrictEqual(
reader.decode(bytes),
target,
'without noMetadata the 0x42 collision corrupts the decode (demonstrating why the flag is needed)'
);
});
describe('storage.randomAccessFields config drives the default', () => {
let previous;
beforeEach(() => {
previous = env.get(terms.CONFIG_PARAMS.STORAGE_RANDOMACCESSFIELDS);
});
afterEach(() => {
env.setProperty(terms.CONFIG_PARAMS.STORAGE_RANDOMACCESSFIELDS, previous);
});
it('writes classic structures when storage.randomAccessFields is false (no explicit option)', () => {
env.setProperty(terms.CONFIG_PARAMS.STORAGE_RANDOMACCESSFIELDS, false);
const enc = makeEncoder(sharedStore()); // no explicit readOnlyStructures
const bytes = enc.encode(record);
assert.ok(
bytes[0] >= 0x40 && bytes[0] < 0x80,
`config off should write classic structures, got 0x${bytes[0].toString(16)}`
);
});
it('writes typed structs when storage.randomAccessFields is true', () => {
env.setProperty(terms.CONFIG_PARAMS.STORAGE_RANDOMACCESSFIELDS, true);
const enc = makeEncoder(sharedStore());
const bytes = enc.encode(record);
assert.ok(
bytes[0] >= 0x20 && bytes[0] < 0x40,
`config on should write typed structs, got 0x${bytes[0].toString(16)}`
);
});
it('an explicit readOnlyStructures option overrides the config', () => {
env.setProperty(terms.CONFIG_PARAMS.STORAGE_RANDOMACCESSFIELDS, false);
const enc = makeEncoder(sharedStore(), { readOnlyStructures: false });
const bytes = enc.encode(record);
assert.ok(
bytes[0] >= 0x20 && bytes[0] < 0x40,
`explicit readOnlyStructures:false should keep typed structs despite config, got 0x${bytes[0].toString(16)}`
);
});
});
});