jsql-neo 5.4.2 → 5.5.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/LICENSE +202 -202
- package/README.md +8097 -7971
- package/bin/jsql +320 -320
- package/bin/jsql-server +90 -90
- package/bin/jsql-server-control.js +177 -177
- package/index.d.ts +385 -357
- package/index.js +124 -118
- package/lib/ast.js +214 -0
- package/lib/btree.js +413 -413
- package/lib/cache.js +328 -328
- package/lib/client.js +125 -125
- package/lib/database.js +1744 -1744
- package/lib/date-types.js +182 -182
- package/lib/errors.js +89 -89
- package/lib/jsql_format.js +497 -497
- package/lib/migrate.js +254 -254
- package/lib/mod.js +292 -292
- package/lib/mongo_server.js +607 -607
- package/lib/multiserver.js +140 -140
- package/lib/mysql_compat.js +413 -413
- package/lib/mysql_server.js +1237 -1237
- package/lib/native_client.js +732 -732
- package/lib/nedb_compat.js +506 -506
- package/lib/pg_server.js +864 -864
- package/lib/plugin.js +34 -34
- package/lib/query.js +815 -815
- package/lib/redis_server.js +571 -571
- package/lib/sql.js +3460 -2925
- package/lib/sqlite_compat.js +323 -323
- package/lib/sqlite_worker.js +264 -264
- package/lib/table.js +1304 -1304
- package/lib/tui.js +502 -502
- package/lib/wasm.js +1 -1
- package/lib/wasm_client.js +718 -718
- package/lib/web_ui.js +257 -257
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
- package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
- package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
- package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
- package/nativesrc/jsql-neo-core/src/types.rs +93 -93
- package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
- package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
- package/nativesrc/jsql-neo-native/build.rs +3 -3
- package/nativesrc/jsql-neo-native/package.json +7 -7
- package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
- package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
- package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
- package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
- package/package.json +87 -86
- package/postinstall.js +158 -158
- package/test/btree.test.js +128 -128
- package/test/coverage.js +187 -187
- package/test/join.test.js +110 -110
- package/test/native.test.js +158 -158
- package/test/regress-5.1.0.js +271 -271
- package/test/smoke.js +58 -58
- package/test/sql-parser.test.js +273 -0
- package/test/wasm.test.js +110 -110
- package/wasm/browser.d.ts +89 -89
- package/wasm/browser.mjs +569 -569
- package/wasm/browser_bg.mjs +597 -597
- package/wasm/jsql_neo_wasm.d.ts +38 -38
- package/wasm/jsql_neo_wasm.js +520 -520
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
- package/wasm/package.json +11 -11
package/lib/jsql_format.js
CHANGED
|
@@ -1,497 +1,497 @@
|
|
|
1
|
-
const zlib = require('zlib');
|
|
2
|
-
const crypto = require('crypto');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const SZ = {
|
|
7
|
-
SUPER: 4096,
|
|
8
|
-
HEADER: 64,
|
|
9
|
-
BLOCK: 4096,
|
|
10
|
-
DATA: 4096 - 64
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const BT = { FREE: 0, DATA: 1, IDX: 2 };
|
|
14
|
-
|
|
15
|
-
const TYPE = { NUL:0, BOL:1, I32:2, F64:3, STR:4, BUF:5, ARR:6, OBJ:7 };
|
|
16
|
-
|
|
17
|
-
function tag(v) {
|
|
18
|
-
if (v===null||v===undefined) return TYPE.NUL;
|
|
19
|
-
if (typeof v==='boolean') return TYPE.BOL;
|
|
20
|
-
if (typeof v==='number') return Number.isInteger(v)?TYPE.I32:TYPE.F64;
|
|
21
|
-
if (typeof v==='string') return TYPE.STR;
|
|
22
|
-
if (v instanceof Buffer) return TYPE.BUF;
|
|
23
|
-
return Array.isArray(v)?TYPE.ARR:TYPE.OBJ;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function enc(v) {
|
|
27
|
-
const t = tag(v);
|
|
28
|
-
switch (t) {
|
|
29
|
-
case TYPE.NUL: return Buffer.from([t]);
|
|
30
|
-
case TYPE.BOL: return Buffer.from([t, v?1:0]);
|
|
31
|
-
case TYPE.I32: { const b=Buffer.alloc(5); b[0]=t; b.writeInt32LE(v,1); return b; }
|
|
32
|
-
case TYPE.F64: { const b=Buffer.alloc(9); b[0]=t; b.writeDoubleLE(v,1); return b; }
|
|
33
|
-
case TYPE.STR: { const s=Buffer.from(v,'utf8'); const b=Buffer.alloc(5+s.length); b[0]=t; b.writeUInt32LE(s.length,1); s.copy(b,5); return b; }
|
|
34
|
-
case TYPE.BUF: { const b=Buffer.alloc(5+v.length); b[0]=t; b.writeUInt32LE(v.length,1); v.copy(b,5); return b; }
|
|
35
|
-
case TYPE.ARR: { const p=[Buffer.from([t]),enc(v.length)]; for(const i of v) p.push(enc(i)); return Buffer.concat(p); }
|
|
36
|
-
case TYPE.OBJ: { const k=Object.keys(v); const p=[Buffer.from([t]),enc(k.length)]; for(const f of k){p.push(enc(f));p.push(enc(v[f]));} return Buffer.concat(p); }
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function dec(buf, off) {
|
|
41
|
-
const t = buf[off++];
|
|
42
|
-
switch (t) {
|
|
43
|
-
case TYPE.NUL: return {v:null,off};
|
|
44
|
-
case TYPE.BOL: return {v:buf[off]!==0,off:off+1};
|
|
45
|
-
case TYPE.I32: return {v:buf.readInt32LE(off),off:off+4};
|
|
46
|
-
case TYPE.F64: return {v:buf.readDoubleLE(off),off:off+8};
|
|
47
|
-
case TYPE.STR: { const l=buf.readUInt32LE(off);off+=4; return {v:buf.toString('utf8',off,off+l),off:off+l}; }
|
|
48
|
-
case TYPE.BUF: { const l=buf.readUInt32LE(off);off+=4; return {v:Buffer.from(buf.subarray(off,off+l)),off:off+l}; }
|
|
49
|
-
case TYPE.ARR: { let r=dec(buf,off);off=r.off; const l=r.v,a=new Array(l); for(let i=0;i<l;i++){r=dec(buf,off);a[i]=r.v;off=r.off;} return {v:a,off}; }
|
|
50
|
-
case TYPE.OBJ: { let r=dec(buf,off);off=r.off; const l=r.v,o={}; for(let i=0;i<l;i++){r=dec(buf,off);const k=r.v;off=r.off;r=dec(buf,off);o[k]=r.v;off=r.off;} return {v:o,off}; }
|
|
51
|
-
default: throw new Error('bad type '+t);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Pre-encoded field names for faster row encoding
|
|
56
|
-
function buildFieldEncoders(schema) {
|
|
57
|
-
const fields = Object.keys(schema).filter(f => f !== '_softDelete');
|
|
58
|
-
return fields.map(f => enc(f));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function encRowFast(row, fieldEncoders) {
|
|
62
|
-
// Estimate: 4(len) + sum of field values
|
|
63
|
-
// fieldEncoders includes pre-encoded field names
|
|
64
|
-
let size = 4;
|
|
65
|
-
for (let i = 0; i < fieldEncoders.length; i++) {
|
|
66
|
-
size += fieldEncoders[i].length;
|
|
67
|
-
const v = row[Object.keys(row)[i] || '']; // fallback
|
|
68
|
-
}
|
|
69
|
-
// Just use the buffer approach
|
|
70
|
-
const chunks = [Buffer.alloc(4)];
|
|
71
|
-
for (let i = 0; i < fieldEncoders.length; i++) {
|
|
72
|
-
chunks.push(fieldEncoders[i], enc(row[Object.keys(row)[i]]));
|
|
73
|
-
}
|
|
74
|
-
const data = Buffer.concat(chunks);
|
|
75
|
-
data.writeUInt32LE(data.length - 4, 0);
|
|
76
|
-
return data;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function writeEnc(buf, off, v) {
|
|
80
|
-
if (v === null || v === undefined) { buf[off] = 0; return off + 1; }
|
|
81
|
-
if (typeof v === 'boolean') { buf[off] = 1; buf[off+1] = v ? 1 : 0; return off + 2; }
|
|
82
|
-
if (typeof v === 'number') {
|
|
83
|
-
if (Number.isInteger(v)) { buf[off] = 2; buf.writeInt32LE(v, off+1); return off + 5; }
|
|
84
|
-
buf[off] = 3; buf.writeDoubleLE(v, off+1); return off + 9;
|
|
85
|
-
}
|
|
86
|
-
if (typeof v === 'string') {
|
|
87
|
-
buf[off] = 4;
|
|
88
|
-
const encoded = Buffer.from(v, 'utf8');
|
|
89
|
-
buf.writeUInt32LE(encoded.length, off+1);
|
|
90
|
-
encoded.copy(buf, off+5);
|
|
91
|
-
return off + 5 + encoded.length;
|
|
92
|
-
}
|
|
93
|
-
const e = enc(v);
|
|
94
|
-
e.copy(buf, off);
|
|
95
|
-
return off + e.length;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function encRowInPlace(row, encoders, scratch, offStart) {
|
|
99
|
-
let off = offStart + 4;
|
|
100
|
-
for (let i = 0; i < encoders.length; i++) {
|
|
101
|
-
encoders[i].nameEnc.copy(scratch, off); off += encoders[i].nameEnc.length;
|
|
102
|
-
off = writeEnc(scratch, off, row[encoders[i].name]);
|
|
103
|
-
}
|
|
104
|
-
scratch.writeUInt32LE(off - offStart - 4, offStart);
|
|
105
|
-
return off;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Build reusable field encoders from schema
|
|
109
|
-
function buildEncoders(schema) {
|
|
110
|
-
return Object.keys(schema).filter(f => f !== '_softDelete').map(name => ({ name, nameEnc: enc(name) }));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const encRow = encRowInPlace;
|
|
114
|
-
|
|
115
|
-
function decRow(buf, off) {
|
|
116
|
-
const l = buf.readUInt32LE(off); off += 4;
|
|
117
|
-
const end = off + l;
|
|
118
|
-
const row = {};
|
|
119
|
-
while (off < end) {
|
|
120
|
-
const r1 = dec(buf, off); const f = r1.v; off = r1.off;
|
|
121
|
-
const r2 = dec(buf, off); row[f] = r2.v; off = r2.off;
|
|
122
|
-
}
|
|
123
|
-
return { row, off };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function hashPK(val) {
|
|
127
|
-
let h = 0;
|
|
128
|
-
const s = String(val);
|
|
129
|
-
for (let i = 0; i < s.length; i++) h = (Math.imul(h,31) + s.charCodeAt(i)) | 0;
|
|
130
|
-
return h >>> 0;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function mkHeader(type, seq, tableId) {
|
|
134
|
-
const h = Buffer.alloc(SZ.HEADER);
|
|
135
|
-
h.writeUInt32LE(seq, 32);
|
|
136
|
-
h[36] = type;
|
|
137
|
-
h[37] = tableId || 0;
|
|
138
|
-
return h;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function finalize(buf) {
|
|
142
|
-
const h = crypto.createHash('sha256').update(buf.subarray(32)).digest();
|
|
143
|
-
h.copy(buf, 0);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function verifyHash(buf) {
|
|
147
|
-
const s = buf.slice(0, 32);
|
|
148
|
-
const c = crypto.createHash('sha256').update(buf.subarray(32)).digest();
|
|
149
|
-
return s.equals(c);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
class JSQLFormat {
|
|
153
|
-
constructor(path) {
|
|
154
|
-
this.path = path;
|
|
155
|
-
this.fd = null;
|
|
156
|
-
this.tables = new Map();
|
|
157
|
-
this.pkIdx = new Map();
|
|
158
|
-
this.nBlocks = 0;
|
|
159
|
-
this.dirty = false;
|
|
160
|
-
this.open = false;
|
|
161
|
-
this._nextTableId = 1;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
_open() {
|
|
165
|
-
if (this.fd) return;
|
|
166
|
-
this.open = true;
|
|
167
|
-
if (fs.existsSync(this.path)) {
|
|
168
|
-
this.fd = fs.openSync(this.path, 'r+');
|
|
169
|
-
this._load();
|
|
170
|
-
} else {
|
|
171
|
-
this.fd = fs.openSync(this.path, 'w+');
|
|
172
|
-
this._init();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
_close() {
|
|
177
|
-
if (!this.fd) return;
|
|
178
|
-
if (this.dirty) this._flush();
|
|
179
|
-
fs.closeSync(this.fd);
|
|
180
|
-
this.fd = null;
|
|
181
|
-
this.open = false;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
_init() {
|
|
185
|
-
const sb = Buffer.alloc(SZ.SUPER);
|
|
186
|
-
sb.write('JSQL', 0, 'utf8');
|
|
187
|
-
// block count = superblock only
|
|
188
|
-
this.nBlocks = 1;
|
|
189
|
-
fs.writeSync(this.fd, sb, 0, SZ.SUPER, 0);
|
|
190
|
-
fs.ftruncateSync(this.fd, SZ.SUPER);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
_load() {
|
|
194
|
-
const sb = Buffer.alloc(SZ.SUPER);
|
|
195
|
-
fs.readSync(this.fd, sb, 0, SZ.SUPER, 0);
|
|
196
|
-
const magic = sb.toString('utf8', 0, 4);
|
|
197
|
-
if (magic !== 'JSQL') throw new Error('not a .jsql file');
|
|
198
|
-
|
|
199
|
-
const tCount = sb.readUInt16LE(4);
|
|
200
|
-
let off = 6;
|
|
201
|
-
for (let i = 0; i < tCount; i++) {
|
|
202
|
-
const nLen = sb[off]; off += 1;
|
|
203
|
-
const name = sb.toString('utf8', off, off + nLen); off += nLen;
|
|
204
|
-
const sLen = sb.readUInt32LE(off); off += 4;
|
|
205
|
-
const rCnt = sb.readUInt32LE(off); off += 4;
|
|
206
|
-
const pkB = sb.readUInt32LE(off); off += 4;
|
|
207
|
-
const dB = sb.readUInt32LE(off); off += 4;
|
|
208
|
-
const tableId = sb.readUInt16LE(off); off += 2;
|
|
209
|
-
const schema = JSON.parse(sb.toString('utf8', off, off + sLen)); off += sLen;
|
|
210
|
-
this.tables.set(name, { schema, rowCount: rCnt, pkBlock: pkB, dataBlock: dB, tableId });
|
|
211
|
-
this.pkIdx.set(name, this._loadIdx(pkB));
|
|
212
|
-
if ((this.tables.get(name).tableId || 0) >= this._nextTableId)
|
|
213
|
-
this._nextTableId = (this.tables.get(name).tableId || 0) + 1;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
this.nBlocks = Math.max(1, Math.ceil(fs.fstatSync(this.fd).size / SZ.BLOCK));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
_saveIdx(name) {
|
|
220
|
-
const entries = this.pkIdx.get(name) || [];
|
|
221
|
-
const maxPerBlock = Math.floor((SZ.DATA - 4) / 12);
|
|
222
|
-
let firstBlock = 0, prevBlock = 0;
|
|
223
|
-
|
|
224
|
-
for (let chunk = 0; chunk < entries.length; chunk += maxPerBlock) {
|
|
225
|
-
const chunkEnd = Math.min(chunk + maxPerBlock, entries.length);
|
|
226
|
-
const buf = Buffer.alloc(SZ.BLOCK);
|
|
227
|
-
mkHeader(BT.IDX, chunk).copy(buf);
|
|
228
|
-
let off = SZ.HEADER;
|
|
229
|
-
|
|
230
|
-
const cnt = chunkEnd - chunk;
|
|
231
|
-
buf.writeUInt32LE(cnt, off); off += 4;
|
|
232
|
-
for (let i = chunk; i < chunkEnd; i++) {
|
|
233
|
-
const e = entries[i];
|
|
234
|
-
buf.writeUInt32LE(e.hash, off); off += 4;
|
|
235
|
-
buf.writeUInt32LE(e.block, off); off += 4;
|
|
236
|
-
buf.writeUInt32LE(e.off, off); off += 4;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const bid = this._alloc();
|
|
240
|
-
if (chunk === 0) firstBlock = bid;
|
|
241
|
-
if (prevBlock) buf.writeUInt32LE(bid, 52); // next block pointer
|
|
242
|
-
buf.writeUInt32LE(prevBlock, 56); // prev block pointer
|
|
243
|
-
|
|
244
|
-
// total entry count in first block
|
|
245
|
-
if (chunk === 0) buf.writeUInt32LE(entries.length, 48);
|
|
246
|
-
|
|
247
|
-
finalize(buf);
|
|
248
|
-
fs.writeSync(this.fd, buf, 0, SZ.BLOCK, bid * SZ.BLOCK);
|
|
249
|
-
prevBlock = bid;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const t = this.tables.get(name);
|
|
253
|
-
if (t) t.pkBlock = firstBlock;
|
|
254
|
-
return firstBlock;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
_loadIdx(blockId) {
|
|
258
|
-
if (blockId < 1) return [];
|
|
259
|
-
const all = [];
|
|
260
|
-
let bid = blockId;
|
|
261
|
-
while (bid) {
|
|
262
|
-
const buf = Buffer.alloc(SZ.BLOCK);
|
|
263
|
-
fs.readSync(this.fd, buf, 0, SZ.BLOCK, bid * SZ.BLOCK);
|
|
264
|
-
if (!verifyHash(buf)) throw new Error('corrupt idx block ' + bid);
|
|
265
|
-
let off = SZ.HEADER;
|
|
266
|
-
const cnt = buf.readUInt32LE(off); off += 4;
|
|
267
|
-
for (let i = 0; i < cnt; i++) {
|
|
268
|
-
const hash = buf.readUInt32LE(off); off += 4;
|
|
269
|
-
const block = buf.readUInt32LE(off); off += 4;
|
|
270
|
-
const dataOff = buf.readUInt32LE(off); off += 4;
|
|
271
|
-
all.push({ hash, block, off: dataOff });
|
|
272
|
-
}
|
|
273
|
-
bid = buf.readUInt32LE(52);
|
|
274
|
-
}
|
|
275
|
-
return all;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
_alloc() { return this.nBlocks++; }
|
|
279
|
-
|
|
280
|
-
_flush() {
|
|
281
|
-
for (const name of this.tables.keys()) this._saveIdx(name);
|
|
282
|
-
this._saveSuper();
|
|
283
|
-
this.dirty = false;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
_saveSuper() {
|
|
287
|
-
const sb = Buffer.alloc(SZ.SUPER);
|
|
288
|
-
sb.write('JSQL', 0, 'utf8');
|
|
289
|
-
sb.writeUInt16LE(this.tables.size, 4);
|
|
290
|
-
let off = 6;
|
|
291
|
-
for (const [name, t] of this.tables) {
|
|
292
|
-
const nB = Buffer.from(name, 'utf8');
|
|
293
|
-
sb[off] = nB.length; off += 1;
|
|
294
|
-
nB.copy(sb, off); off += nB.length;
|
|
295
|
-
const sStr = JSON.stringify(t.schema);
|
|
296
|
-
sb.writeUInt32LE(sStr.length, off); off += 4;
|
|
297
|
-
sb.writeUInt32LE(t.rowCount, off); off += 4;
|
|
298
|
-
sb.writeUInt32LE(t.pkBlock||0, off); off += 4;
|
|
299
|
-
sb.writeUInt32LE(t.dataBlock||0, off); off += 4;
|
|
300
|
-
sb.writeUInt16LE(t.tableId || 0, off); off += 2;
|
|
301
|
-
Buffer.from(sStr, 'utf8').copy(sb, off); off += sStr.length;
|
|
302
|
-
}
|
|
303
|
-
fs.writeSync(this.fd, sb, 0, SZ.SUPER, 0);
|
|
304
|
-
fs.fsyncSync(this.fd);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
_reset() {
|
|
308
|
-
this.nBlocks = 1;
|
|
309
|
-
this._nextTableId = 1;
|
|
310
|
-
this.tables.clear();
|
|
311
|
-
this.pkIdx.clear();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
_truncateToSuper() {
|
|
315
|
-
fs.ftruncateSync(this.fd, SZ.SUPER);
|
|
316
|
-
const sb = Buffer.alloc(SZ.SUPER); sb.write('JSQL', 0, 'utf8');
|
|
317
|
-
fs.writeSync(this.fd, sb, 0, SZ.SUPER, 0);
|
|
318
|
-
this.nBlocks = 1;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
saveAll(allTables) {
|
|
322
|
-
this._open();
|
|
323
|
-
this._reset();
|
|
324
|
-
|
|
325
|
-
const pending = [];
|
|
326
|
-
|
|
327
|
-
for (const [name, info] of Object.entries(allTables)) {
|
|
328
|
-
const { schema, rows, pkField } = info;
|
|
329
|
-
if (!rows || rows.length === 0) continue;
|
|
330
|
-
|
|
331
|
-
const tableId = this._nextTableId++;
|
|
332
|
-
this.tables.set(name, { schema, rowCount:0, pkBlock:0, dataBlock:0, tableId });
|
|
333
|
-
|
|
334
|
-
const pkEntries = [];
|
|
335
|
-
const rawBlocks = [];
|
|
336
|
-
const blockIds = [];
|
|
337
|
-
let curBlock = this._alloc();
|
|
338
|
-
let buf = Buffer.alloc(SZ.BLOCK);
|
|
339
|
-
let bp = SZ.HEADER;
|
|
340
|
-
mkHeader(BT.DATA, curBlock, tableId).copy(buf);
|
|
341
|
-
|
|
342
|
-
// Pre-build field encoders once per table
|
|
343
|
-
const encoders = buildEncoders(schema);
|
|
344
|
-
// Scratch buffer for row encoding (max row size fits in one block)
|
|
345
|
-
const scratch = Buffer.allocUnsafe(SZ.DATA);
|
|
346
|
-
|
|
347
|
-
// Phase 1: encode every row into memory first. Any error thrown here
|
|
348
|
-
// (oversized row, bad value) aborts before the file is touched.
|
|
349
|
-
for (const row of rows) {
|
|
350
|
-
const end = encRowInPlace(row, encoders, scratch, 0);
|
|
351
|
-
if (end > SZ.DATA) {
|
|
352
|
-
throw new Error(
|
|
353
|
-
`row in table '${name}' is too large (${end} bytes > max row size ${SZ.DATA} bytes); ` +
|
|
354
|
-
'split the value or reduce field sizes before saving'
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
if (bp + end > SZ.BLOCK) {
|
|
358
|
-
const raw = Buffer.from(buf.subarray(SZ.HEADER, bp));
|
|
359
|
-
rawBlocks.push(raw);
|
|
360
|
-
blockIds.push(curBlock);
|
|
361
|
-
buf.writeUInt32LE(bp - SZ.HEADER, 40);
|
|
362
|
-
|
|
363
|
-
curBlock = this._alloc();
|
|
364
|
-
buf = Buffer.alloc(SZ.BLOCK);
|
|
365
|
-
bp = SZ.HEADER;
|
|
366
|
-
mkHeader(BT.DATA, curBlock, tableId).copy(buf);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const startOff = bp;
|
|
370
|
-
scratch.copy(buf, bp, 0, end);
|
|
371
|
-
bp += end;
|
|
372
|
-
|
|
373
|
-
if (pkField && row[pkField] !== undefined) {
|
|
374
|
-
pkEntries.push({ hash: hashPK(row[pkField]), block: curBlock, off: startOff - SZ.HEADER });
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (bp > SZ.HEADER) {
|
|
379
|
-
const raw = Buffer.from(buf.subarray(SZ.HEADER, bp));
|
|
380
|
-
rawBlocks.push(raw);
|
|
381
|
-
blockIds.push(curBlock);
|
|
382
|
-
buf.writeUInt32LE(bp - SZ.HEADER, 40);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
pkEntries.sort((a, b) => a.hash - b.hash);
|
|
386
|
-
pending.push({ name, tableId, rawBlocks, blockIds, pkEntries });
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Phase 2: all rows encoded successfully — now write to disk.
|
|
390
|
-
for (const p of pending) {
|
|
391
|
-
// Compress and write blocks
|
|
392
|
-
for (let i = 0; i < p.rawBlocks.length; i++) {
|
|
393
|
-
const bid = p.blockIds[i];
|
|
394
|
-
const compBuf = zlib.gzipSync(p.rawBlocks[i], { level: 1 });
|
|
395
|
-
const block = Buffer.alloc(SZ.BLOCK);
|
|
396
|
-
mkHeader(BT.DATA, bid, p.tableId).copy(block);
|
|
397
|
-
block.writeUInt32LE(p.rawBlocks[i].length, 40);
|
|
398
|
-
block[48] = 1;
|
|
399
|
-
block.writeUInt32LE(compBuf.length, 44);
|
|
400
|
-
compBuf.copy(block, SZ.HEADER);
|
|
401
|
-
finalize(block);
|
|
402
|
-
fs.writeSync(this.fd, block, 0, SZ.BLOCK, bid * SZ.BLOCK);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const t = this.tables.get(p.name);
|
|
406
|
-
t.rowCount = p.pkEntries.length;
|
|
407
|
-
t.dataBlock = p.blockIds.length > 0 ? p.blockIds[p.blockIds.length - 1] : 0;
|
|
408
|
-
|
|
409
|
-
this.pkIdx.set(p.name, p.pkEntries);
|
|
410
|
-
this._saveIdx(p.name);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
this._saveSuper();
|
|
414
|
-
this.dirty = false;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
writeTable(name, schema, rows, pkField) {
|
|
418
|
-
this.saveAll({ [name]: { schema, rows, pkField } });
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
readTableSync(name) {
|
|
422
|
-
this._open();
|
|
423
|
-
const t = this.tables.get(name);
|
|
424
|
-
if (!t) return { schema: null, rows: [] };
|
|
425
|
-
|
|
426
|
-
const rows = [];
|
|
427
|
-
for (let bid = 1; bid < this.nBlocks; bid++) {
|
|
428
|
-
const buf = Buffer.alloc(SZ.BLOCK);
|
|
429
|
-
try { fs.readSync(this.fd, buf, 0, SZ.BLOCK, bid * SZ.BLOCK); } catch(e) { continue; }
|
|
430
|
-
if (buf[36] !== BT.DATA || buf[37] !== t.tableId) continue;
|
|
431
|
-
if (!verifyHash(buf)) continue;
|
|
432
|
-
|
|
433
|
-
const compFlag = buf[48];
|
|
434
|
-
const raw = compFlag
|
|
435
|
-
? zlib.gunzipSync(buf.slice(SZ.HEADER, SZ.HEADER + buf.readUInt32LE(44)))
|
|
436
|
-
: buf.slice(SZ.HEADER, SZ.HEADER + buf.readUInt32LE(40));
|
|
437
|
-
|
|
438
|
-
let off = 0;
|
|
439
|
-
while (off + 4 <= raw.length) {
|
|
440
|
-
const l = raw.readUInt32LE(off);
|
|
441
|
-
if (l === 0 || off + 4 + l > raw.length) break;
|
|
442
|
-
const r = decRow(raw, off);
|
|
443
|
-
rows.push(r.row);
|
|
444
|
-
off = r.off;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return { schema: t.schema, rows };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
readTable(name) {
|
|
451
|
-
return this.readTableSync(name);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
findRow(name, pkValue) {
|
|
455
|
-
const entries = this.pkIdx.get(name);
|
|
456
|
-
if (!entries || entries.length === 0) return null;
|
|
457
|
-
const h = hashPK(pkValue);
|
|
458
|
-
let lo = 0, hi = entries.length - 1;
|
|
459
|
-
while (lo <= hi) {
|
|
460
|
-
const m = (lo + hi) >>> 1;
|
|
461
|
-
const e = entries[m];
|
|
462
|
-
if (e.hash === h) return this._readInBlock(e.block, e.off);
|
|
463
|
-
if (e.hash < h) lo = m + 1;
|
|
464
|
-
else hi = m - 1;
|
|
465
|
-
}
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
_readInBlock(blockId, off) {
|
|
470
|
-
const buf = Buffer.alloc(SZ.BLOCK);
|
|
471
|
-
fs.readSync(this.fd, buf, 0, SZ.BLOCK, blockId * SZ.BLOCK);
|
|
472
|
-
const compFlag = buf[48];
|
|
473
|
-
const compSz = buf.readUInt32LE(44);
|
|
474
|
-
const dataSz = buf.readUInt32LE(40);
|
|
475
|
-
let raw;
|
|
476
|
-
if (compFlag) {
|
|
477
|
-
raw = zlib.gunzipSync(buf.slice(SZ.HEADER, SZ.HEADER + compSz));
|
|
478
|
-
} else {
|
|
479
|
-
raw = buf.slice(SZ.HEADER, SZ.HEADER + dataSz);
|
|
480
|
-
}
|
|
481
|
-
const r = decRow(raw, off);
|
|
482
|
-
return r.row;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
appendRows(name, schema, rows, pkField) {
|
|
486
|
-
this._open();
|
|
487
|
-
const t = this.tables.get(name);
|
|
488
|
-
if (!t) { this.writeTable(name, schema, rows, pkField); return; }
|
|
489
|
-
t.schema = schema;
|
|
490
|
-
|
|
491
|
-
const { rows: oldRows } = this.readTable(name);
|
|
492
|
-
oldRows.push(...rows);
|
|
493
|
-
this.writeTable(name, schema, oldRows, pkField);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
module.exports = JSQLFormat;
|
|
1
|
+
const zlib = require('zlib');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const SZ = {
|
|
7
|
+
SUPER: 4096,
|
|
8
|
+
HEADER: 64,
|
|
9
|
+
BLOCK: 4096,
|
|
10
|
+
DATA: 4096 - 64
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const BT = { FREE: 0, DATA: 1, IDX: 2 };
|
|
14
|
+
|
|
15
|
+
const TYPE = { NUL:0, BOL:1, I32:2, F64:3, STR:4, BUF:5, ARR:6, OBJ:7 };
|
|
16
|
+
|
|
17
|
+
function tag(v) {
|
|
18
|
+
if (v===null||v===undefined) return TYPE.NUL;
|
|
19
|
+
if (typeof v==='boolean') return TYPE.BOL;
|
|
20
|
+
if (typeof v==='number') return Number.isInteger(v)?TYPE.I32:TYPE.F64;
|
|
21
|
+
if (typeof v==='string') return TYPE.STR;
|
|
22
|
+
if (v instanceof Buffer) return TYPE.BUF;
|
|
23
|
+
return Array.isArray(v)?TYPE.ARR:TYPE.OBJ;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function enc(v) {
|
|
27
|
+
const t = tag(v);
|
|
28
|
+
switch (t) {
|
|
29
|
+
case TYPE.NUL: return Buffer.from([t]);
|
|
30
|
+
case TYPE.BOL: return Buffer.from([t, v?1:0]);
|
|
31
|
+
case TYPE.I32: { const b=Buffer.alloc(5); b[0]=t; b.writeInt32LE(v,1); return b; }
|
|
32
|
+
case TYPE.F64: { const b=Buffer.alloc(9); b[0]=t; b.writeDoubleLE(v,1); return b; }
|
|
33
|
+
case TYPE.STR: { const s=Buffer.from(v,'utf8'); const b=Buffer.alloc(5+s.length); b[0]=t; b.writeUInt32LE(s.length,1); s.copy(b,5); return b; }
|
|
34
|
+
case TYPE.BUF: { const b=Buffer.alloc(5+v.length); b[0]=t; b.writeUInt32LE(v.length,1); v.copy(b,5); return b; }
|
|
35
|
+
case TYPE.ARR: { const p=[Buffer.from([t]),enc(v.length)]; for(const i of v) p.push(enc(i)); return Buffer.concat(p); }
|
|
36
|
+
case TYPE.OBJ: { const k=Object.keys(v); const p=[Buffer.from([t]),enc(k.length)]; for(const f of k){p.push(enc(f));p.push(enc(v[f]));} return Buffer.concat(p); }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function dec(buf, off) {
|
|
41
|
+
const t = buf[off++];
|
|
42
|
+
switch (t) {
|
|
43
|
+
case TYPE.NUL: return {v:null,off};
|
|
44
|
+
case TYPE.BOL: return {v:buf[off]!==0,off:off+1};
|
|
45
|
+
case TYPE.I32: return {v:buf.readInt32LE(off),off:off+4};
|
|
46
|
+
case TYPE.F64: return {v:buf.readDoubleLE(off),off:off+8};
|
|
47
|
+
case TYPE.STR: { const l=buf.readUInt32LE(off);off+=4; return {v:buf.toString('utf8',off,off+l),off:off+l}; }
|
|
48
|
+
case TYPE.BUF: { const l=buf.readUInt32LE(off);off+=4; return {v:Buffer.from(buf.subarray(off,off+l)),off:off+l}; }
|
|
49
|
+
case TYPE.ARR: { let r=dec(buf,off);off=r.off; const l=r.v,a=new Array(l); for(let i=0;i<l;i++){r=dec(buf,off);a[i]=r.v;off=r.off;} return {v:a,off}; }
|
|
50
|
+
case TYPE.OBJ: { let r=dec(buf,off);off=r.off; const l=r.v,o={}; for(let i=0;i<l;i++){r=dec(buf,off);const k=r.v;off=r.off;r=dec(buf,off);o[k]=r.v;off=r.off;} return {v:o,off}; }
|
|
51
|
+
default: throw new Error('bad type '+t);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Pre-encoded field names for faster row encoding
|
|
56
|
+
function buildFieldEncoders(schema) {
|
|
57
|
+
const fields = Object.keys(schema).filter(f => f !== '_softDelete');
|
|
58
|
+
return fields.map(f => enc(f));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function encRowFast(row, fieldEncoders) {
|
|
62
|
+
// Estimate: 4(len) + sum of field values
|
|
63
|
+
// fieldEncoders includes pre-encoded field names
|
|
64
|
+
let size = 4;
|
|
65
|
+
for (let i = 0; i < fieldEncoders.length; i++) {
|
|
66
|
+
size += fieldEncoders[i].length;
|
|
67
|
+
const v = row[Object.keys(row)[i] || '']; // fallback
|
|
68
|
+
}
|
|
69
|
+
// Just use the buffer approach
|
|
70
|
+
const chunks = [Buffer.alloc(4)];
|
|
71
|
+
for (let i = 0; i < fieldEncoders.length; i++) {
|
|
72
|
+
chunks.push(fieldEncoders[i], enc(row[Object.keys(row)[i]]));
|
|
73
|
+
}
|
|
74
|
+
const data = Buffer.concat(chunks);
|
|
75
|
+
data.writeUInt32LE(data.length - 4, 0);
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeEnc(buf, off, v) {
|
|
80
|
+
if (v === null || v === undefined) { buf[off] = 0; return off + 1; }
|
|
81
|
+
if (typeof v === 'boolean') { buf[off] = 1; buf[off+1] = v ? 1 : 0; return off + 2; }
|
|
82
|
+
if (typeof v === 'number') {
|
|
83
|
+
if (Number.isInteger(v)) { buf[off] = 2; buf.writeInt32LE(v, off+1); return off + 5; }
|
|
84
|
+
buf[off] = 3; buf.writeDoubleLE(v, off+1); return off + 9;
|
|
85
|
+
}
|
|
86
|
+
if (typeof v === 'string') {
|
|
87
|
+
buf[off] = 4;
|
|
88
|
+
const encoded = Buffer.from(v, 'utf8');
|
|
89
|
+
buf.writeUInt32LE(encoded.length, off+1);
|
|
90
|
+
encoded.copy(buf, off+5);
|
|
91
|
+
return off + 5 + encoded.length;
|
|
92
|
+
}
|
|
93
|
+
const e = enc(v);
|
|
94
|
+
e.copy(buf, off);
|
|
95
|
+
return off + e.length;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function encRowInPlace(row, encoders, scratch, offStart) {
|
|
99
|
+
let off = offStart + 4;
|
|
100
|
+
for (let i = 0; i < encoders.length; i++) {
|
|
101
|
+
encoders[i].nameEnc.copy(scratch, off); off += encoders[i].nameEnc.length;
|
|
102
|
+
off = writeEnc(scratch, off, row[encoders[i].name]);
|
|
103
|
+
}
|
|
104
|
+
scratch.writeUInt32LE(off - offStart - 4, offStart);
|
|
105
|
+
return off;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Build reusable field encoders from schema
|
|
109
|
+
function buildEncoders(schema) {
|
|
110
|
+
return Object.keys(schema).filter(f => f !== '_softDelete').map(name => ({ name, nameEnc: enc(name) }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const encRow = encRowInPlace;
|
|
114
|
+
|
|
115
|
+
function decRow(buf, off) {
|
|
116
|
+
const l = buf.readUInt32LE(off); off += 4;
|
|
117
|
+
const end = off + l;
|
|
118
|
+
const row = {};
|
|
119
|
+
while (off < end) {
|
|
120
|
+
const r1 = dec(buf, off); const f = r1.v; off = r1.off;
|
|
121
|
+
const r2 = dec(buf, off); row[f] = r2.v; off = r2.off;
|
|
122
|
+
}
|
|
123
|
+
return { row, off };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hashPK(val) {
|
|
127
|
+
let h = 0;
|
|
128
|
+
const s = String(val);
|
|
129
|
+
for (let i = 0; i < s.length; i++) h = (Math.imul(h,31) + s.charCodeAt(i)) | 0;
|
|
130
|
+
return h >>> 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function mkHeader(type, seq, tableId) {
|
|
134
|
+
const h = Buffer.alloc(SZ.HEADER);
|
|
135
|
+
h.writeUInt32LE(seq, 32);
|
|
136
|
+
h[36] = type;
|
|
137
|
+
h[37] = tableId || 0;
|
|
138
|
+
return h;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function finalize(buf) {
|
|
142
|
+
const h = crypto.createHash('sha256').update(buf.subarray(32)).digest();
|
|
143
|
+
h.copy(buf, 0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function verifyHash(buf) {
|
|
147
|
+
const s = buf.slice(0, 32);
|
|
148
|
+
const c = crypto.createHash('sha256').update(buf.subarray(32)).digest();
|
|
149
|
+
return s.equals(c);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
class JSQLFormat {
|
|
153
|
+
constructor(path) {
|
|
154
|
+
this.path = path;
|
|
155
|
+
this.fd = null;
|
|
156
|
+
this.tables = new Map();
|
|
157
|
+
this.pkIdx = new Map();
|
|
158
|
+
this.nBlocks = 0;
|
|
159
|
+
this.dirty = false;
|
|
160
|
+
this.open = false;
|
|
161
|
+
this._nextTableId = 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_open() {
|
|
165
|
+
if (this.fd) return;
|
|
166
|
+
this.open = true;
|
|
167
|
+
if (fs.existsSync(this.path)) {
|
|
168
|
+
this.fd = fs.openSync(this.path, 'r+');
|
|
169
|
+
this._load();
|
|
170
|
+
} else {
|
|
171
|
+
this.fd = fs.openSync(this.path, 'w+');
|
|
172
|
+
this._init();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_close() {
|
|
177
|
+
if (!this.fd) return;
|
|
178
|
+
if (this.dirty) this._flush();
|
|
179
|
+
fs.closeSync(this.fd);
|
|
180
|
+
this.fd = null;
|
|
181
|
+
this.open = false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_init() {
|
|
185
|
+
const sb = Buffer.alloc(SZ.SUPER);
|
|
186
|
+
sb.write('JSQL', 0, 'utf8');
|
|
187
|
+
// block count = superblock only
|
|
188
|
+
this.nBlocks = 1;
|
|
189
|
+
fs.writeSync(this.fd, sb, 0, SZ.SUPER, 0);
|
|
190
|
+
fs.ftruncateSync(this.fd, SZ.SUPER);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
_load() {
|
|
194
|
+
const sb = Buffer.alloc(SZ.SUPER);
|
|
195
|
+
fs.readSync(this.fd, sb, 0, SZ.SUPER, 0);
|
|
196
|
+
const magic = sb.toString('utf8', 0, 4);
|
|
197
|
+
if (magic !== 'JSQL') throw new Error('not a .jsql file');
|
|
198
|
+
|
|
199
|
+
const tCount = sb.readUInt16LE(4);
|
|
200
|
+
let off = 6;
|
|
201
|
+
for (let i = 0; i < tCount; i++) {
|
|
202
|
+
const nLen = sb[off]; off += 1;
|
|
203
|
+
const name = sb.toString('utf8', off, off + nLen); off += nLen;
|
|
204
|
+
const sLen = sb.readUInt32LE(off); off += 4;
|
|
205
|
+
const rCnt = sb.readUInt32LE(off); off += 4;
|
|
206
|
+
const pkB = sb.readUInt32LE(off); off += 4;
|
|
207
|
+
const dB = sb.readUInt32LE(off); off += 4;
|
|
208
|
+
const tableId = sb.readUInt16LE(off); off += 2;
|
|
209
|
+
const schema = JSON.parse(sb.toString('utf8', off, off + sLen)); off += sLen;
|
|
210
|
+
this.tables.set(name, { schema, rowCount: rCnt, pkBlock: pkB, dataBlock: dB, tableId });
|
|
211
|
+
this.pkIdx.set(name, this._loadIdx(pkB));
|
|
212
|
+
if ((this.tables.get(name).tableId || 0) >= this._nextTableId)
|
|
213
|
+
this._nextTableId = (this.tables.get(name).tableId || 0) + 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.nBlocks = Math.max(1, Math.ceil(fs.fstatSync(this.fd).size / SZ.BLOCK));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_saveIdx(name) {
|
|
220
|
+
const entries = this.pkIdx.get(name) || [];
|
|
221
|
+
const maxPerBlock = Math.floor((SZ.DATA - 4) / 12);
|
|
222
|
+
let firstBlock = 0, prevBlock = 0;
|
|
223
|
+
|
|
224
|
+
for (let chunk = 0; chunk < entries.length; chunk += maxPerBlock) {
|
|
225
|
+
const chunkEnd = Math.min(chunk + maxPerBlock, entries.length);
|
|
226
|
+
const buf = Buffer.alloc(SZ.BLOCK);
|
|
227
|
+
mkHeader(BT.IDX, chunk).copy(buf);
|
|
228
|
+
let off = SZ.HEADER;
|
|
229
|
+
|
|
230
|
+
const cnt = chunkEnd - chunk;
|
|
231
|
+
buf.writeUInt32LE(cnt, off); off += 4;
|
|
232
|
+
for (let i = chunk; i < chunkEnd; i++) {
|
|
233
|
+
const e = entries[i];
|
|
234
|
+
buf.writeUInt32LE(e.hash, off); off += 4;
|
|
235
|
+
buf.writeUInt32LE(e.block, off); off += 4;
|
|
236
|
+
buf.writeUInt32LE(e.off, off); off += 4;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const bid = this._alloc();
|
|
240
|
+
if (chunk === 0) firstBlock = bid;
|
|
241
|
+
if (prevBlock) buf.writeUInt32LE(bid, 52); // next block pointer
|
|
242
|
+
buf.writeUInt32LE(prevBlock, 56); // prev block pointer
|
|
243
|
+
|
|
244
|
+
// total entry count in first block
|
|
245
|
+
if (chunk === 0) buf.writeUInt32LE(entries.length, 48);
|
|
246
|
+
|
|
247
|
+
finalize(buf);
|
|
248
|
+
fs.writeSync(this.fd, buf, 0, SZ.BLOCK, bid * SZ.BLOCK);
|
|
249
|
+
prevBlock = bid;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const t = this.tables.get(name);
|
|
253
|
+
if (t) t.pkBlock = firstBlock;
|
|
254
|
+
return firstBlock;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_loadIdx(blockId) {
|
|
258
|
+
if (blockId < 1) return [];
|
|
259
|
+
const all = [];
|
|
260
|
+
let bid = blockId;
|
|
261
|
+
while (bid) {
|
|
262
|
+
const buf = Buffer.alloc(SZ.BLOCK);
|
|
263
|
+
fs.readSync(this.fd, buf, 0, SZ.BLOCK, bid * SZ.BLOCK);
|
|
264
|
+
if (!verifyHash(buf)) throw new Error('corrupt idx block ' + bid);
|
|
265
|
+
let off = SZ.HEADER;
|
|
266
|
+
const cnt = buf.readUInt32LE(off); off += 4;
|
|
267
|
+
for (let i = 0; i < cnt; i++) {
|
|
268
|
+
const hash = buf.readUInt32LE(off); off += 4;
|
|
269
|
+
const block = buf.readUInt32LE(off); off += 4;
|
|
270
|
+
const dataOff = buf.readUInt32LE(off); off += 4;
|
|
271
|
+
all.push({ hash, block, off: dataOff });
|
|
272
|
+
}
|
|
273
|
+
bid = buf.readUInt32LE(52);
|
|
274
|
+
}
|
|
275
|
+
return all;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
_alloc() { return this.nBlocks++; }
|
|
279
|
+
|
|
280
|
+
_flush() {
|
|
281
|
+
for (const name of this.tables.keys()) this._saveIdx(name);
|
|
282
|
+
this._saveSuper();
|
|
283
|
+
this.dirty = false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_saveSuper() {
|
|
287
|
+
const sb = Buffer.alloc(SZ.SUPER);
|
|
288
|
+
sb.write('JSQL', 0, 'utf8');
|
|
289
|
+
sb.writeUInt16LE(this.tables.size, 4);
|
|
290
|
+
let off = 6;
|
|
291
|
+
for (const [name, t] of this.tables) {
|
|
292
|
+
const nB = Buffer.from(name, 'utf8');
|
|
293
|
+
sb[off] = nB.length; off += 1;
|
|
294
|
+
nB.copy(sb, off); off += nB.length;
|
|
295
|
+
const sStr = JSON.stringify(t.schema);
|
|
296
|
+
sb.writeUInt32LE(sStr.length, off); off += 4;
|
|
297
|
+
sb.writeUInt32LE(t.rowCount, off); off += 4;
|
|
298
|
+
sb.writeUInt32LE(t.pkBlock||0, off); off += 4;
|
|
299
|
+
sb.writeUInt32LE(t.dataBlock||0, off); off += 4;
|
|
300
|
+
sb.writeUInt16LE(t.tableId || 0, off); off += 2;
|
|
301
|
+
Buffer.from(sStr, 'utf8').copy(sb, off); off += sStr.length;
|
|
302
|
+
}
|
|
303
|
+
fs.writeSync(this.fd, sb, 0, SZ.SUPER, 0);
|
|
304
|
+
fs.fsyncSync(this.fd);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_reset() {
|
|
308
|
+
this.nBlocks = 1;
|
|
309
|
+
this._nextTableId = 1;
|
|
310
|
+
this.tables.clear();
|
|
311
|
+
this.pkIdx.clear();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
_truncateToSuper() {
|
|
315
|
+
fs.ftruncateSync(this.fd, SZ.SUPER);
|
|
316
|
+
const sb = Buffer.alloc(SZ.SUPER); sb.write('JSQL', 0, 'utf8');
|
|
317
|
+
fs.writeSync(this.fd, sb, 0, SZ.SUPER, 0);
|
|
318
|
+
this.nBlocks = 1;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
saveAll(allTables) {
|
|
322
|
+
this._open();
|
|
323
|
+
this._reset();
|
|
324
|
+
|
|
325
|
+
const pending = [];
|
|
326
|
+
|
|
327
|
+
for (const [name, info] of Object.entries(allTables)) {
|
|
328
|
+
const { schema, rows, pkField } = info;
|
|
329
|
+
if (!rows || rows.length === 0) continue;
|
|
330
|
+
|
|
331
|
+
const tableId = this._nextTableId++;
|
|
332
|
+
this.tables.set(name, { schema, rowCount:0, pkBlock:0, dataBlock:0, tableId });
|
|
333
|
+
|
|
334
|
+
const pkEntries = [];
|
|
335
|
+
const rawBlocks = [];
|
|
336
|
+
const blockIds = [];
|
|
337
|
+
let curBlock = this._alloc();
|
|
338
|
+
let buf = Buffer.alloc(SZ.BLOCK);
|
|
339
|
+
let bp = SZ.HEADER;
|
|
340
|
+
mkHeader(BT.DATA, curBlock, tableId).copy(buf);
|
|
341
|
+
|
|
342
|
+
// Pre-build field encoders once per table
|
|
343
|
+
const encoders = buildEncoders(schema);
|
|
344
|
+
// Scratch buffer for row encoding (max row size fits in one block)
|
|
345
|
+
const scratch = Buffer.allocUnsafe(SZ.DATA);
|
|
346
|
+
|
|
347
|
+
// Phase 1: encode every row into memory first. Any error thrown here
|
|
348
|
+
// (oversized row, bad value) aborts before the file is touched.
|
|
349
|
+
for (const row of rows) {
|
|
350
|
+
const end = encRowInPlace(row, encoders, scratch, 0);
|
|
351
|
+
if (end > SZ.DATA) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`row in table '${name}' is too large (${end} bytes > max row size ${SZ.DATA} bytes); ` +
|
|
354
|
+
'split the value or reduce field sizes before saving'
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (bp + end > SZ.BLOCK) {
|
|
358
|
+
const raw = Buffer.from(buf.subarray(SZ.HEADER, bp));
|
|
359
|
+
rawBlocks.push(raw);
|
|
360
|
+
blockIds.push(curBlock);
|
|
361
|
+
buf.writeUInt32LE(bp - SZ.HEADER, 40);
|
|
362
|
+
|
|
363
|
+
curBlock = this._alloc();
|
|
364
|
+
buf = Buffer.alloc(SZ.BLOCK);
|
|
365
|
+
bp = SZ.HEADER;
|
|
366
|
+
mkHeader(BT.DATA, curBlock, tableId).copy(buf);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const startOff = bp;
|
|
370
|
+
scratch.copy(buf, bp, 0, end);
|
|
371
|
+
bp += end;
|
|
372
|
+
|
|
373
|
+
if (pkField && row[pkField] !== undefined) {
|
|
374
|
+
pkEntries.push({ hash: hashPK(row[pkField]), block: curBlock, off: startOff - SZ.HEADER });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (bp > SZ.HEADER) {
|
|
379
|
+
const raw = Buffer.from(buf.subarray(SZ.HEADER, bp));
|
|
380
|
+
rawBlocks.push(raw);
|
|
381
|
+
blockIds.push(curBlock);
|
|
382
|
+
buf.writeUInt32LE(bp - SZ.HEADER, 40);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
pkEntries.sort((a, b) => a.hash - b.hash);
|
|
386
|
+
pending.push({ name, tableId, rawBlocks, blockIds, pkEntries });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Phase 2: all rows encoded successfully — now write to disk.
|
|
390
|
+
for (const p of pending) {
|
|
391
|
+
// Compress and write blocks
|
|
392
|
+
for (let i = 0; i < p.rawBlocks.length; i++) {
|
|
393
|
+
const bid = p.blockIds[i];
|
|
394
|
+
const compBuf = zlib.gzipSync(p.rawBlocks[i], { level: 1 });
|
|
395
|
+
const block = Buffer.alloc(SZ.BLOCK);
|
|
396
|
+
mkHeader(BT.DATA, bid, p.tableId).copy(block);
|
|
397
|
+
block.writeUInt32LE(p.rawBlocks[i].length, 40);
|
|
398
|
+
block[48] = 1;
|
|
399
|
+
block.writeUInt32LE(compBuf.length, 44);
|
|
400
|
+
compBuf.copy(block, SZ.HEADER);
|
|
401
|
+
finalize(block);
|
|
402
|
+
fs.writeSync(this.fd, block, 0, SZ.BLOCK, bid * SZ.BLOCK);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const t = this.tables.get(p.name);
|
|
406
|
+
t.rowCount = p.pkEntries.length;
|
|
407
|
+
t.dataBlock = p.blockIds.length > 0 ? p.blockIds[p.blockIds.length - 1] : 0;
|
|
408
|
+
|
|
409
|
+
this.pkIdx.set(p.name, p.pkEntries);
|
|
410
|
+
this._saveIdx(p.name);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this._saveSuper();
|
|
414
|
+
this.dirty = false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
writeTable(name, schema, rows, pkField) {
|
|
418
|
+
this.saveAll({ [name]: { schema, rows, pkField } });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
readTableSync(name) {
|
|
422
|
+
this._open();
|
|
423
|
+
const t = this.tables.get(name);
|
|
424
|
+
if (!t) return { schema: null, rows: [] };
|
|
425
|
+
|
|
426
|
+
const rows = [];
|
|
427
|
+
for (let bid = 1; bid < this.nBlocks; bid++) {
|
|
428
|
+
const buf = Buffer.alloc(SZ.BLOCK);
|
|
429
|
+
try { fs.readSync(this.fd, buf, 0, SZ.BLOCK, bid * SZ.BLOCK); } catch(e) { continue; }
|
|
430
|
+
if (buf[36] !== BT.DATA || buf[37] !== t.tableId) continue;
|
|
431
|
+
if (!verifyHash(buf)) continue;
|
|
432
|
+
|
|
433
|
+
const compFlag = buf[48];
|
|
434
|
+
const raw = compFlag
|
|
435
|
+
? zlib.gunzipSync(buf.slice(SZ.HEADER, SZ.HEADER + buf.readUInt32LE(44)))
|
|
436
|
+
: buf.slice(SZ.HEADER, SZ.HEADER + buf.readUInt32LE(40));
|
|
437
|
+
|
|
438
|
+
let off = 0;
|
|
439
|
+
while (off + 4 <= raw.length) {
|
|
440
|
+
const l = raw.readUInt32LE(off);
|
|
441
|
+
if (l === 0 || off + 4 + l > raw.length) break;
|
|
442
|
+
const r = decRow(raw, off);
|
|
443
|
+
rows.push(r.row);
|
|
444
|
+
off = r.off;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { schema: t.schema, rows };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
readTable(name) {
|
|
451
|
+
return this.readTableSync(name);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
findRow(name, pkValue) {
|
|
455
|
+
const entries = this.pkIdx.get(name);
|
|
456
|
+
if (!entries || entries.length === 0) return null;
|
|
457
|
+
const h = hashPK(pkValue);
|
|
458
|
+
let lo = 0, hi = entries.length - 1;
|
|
459
|
+
while (lo <= hi) {
|
|
460
|
+
const m = (lo + hi) >>> 1;
|
|
461
|
+
const e = entries[m];
|
|
462
|
+
if (e.hash === h) return this._readInBlock(e.block, e.off);
|
|
463
|
+
if (e.hash < h) lo = m + 1;
|
|
464
|
+
else hi = m - 1;
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
_readInBlock(blockId, off) {
|
|
470
|
+
const buf = Buffer.alloc(SZ.BLOCK);
|
|
471
|
+
fs.readSync(this.fd, buf, 0, SZ.BLOCK, blockId * SZ.BLOCK);
|
|
472
|
+
const compFlag = buf[48];
|
|
473
|
+
const compSz = buf.readUInt32LE(44);
|
|
474
|
+
const dataSz = buf.readUInt32LE(40);
|
|
475
|
+
let raw;
|
|
476
|
+
if (compFlag) {
|
|
477
|
+
raw = zlib.gunzipSync(buf.slice(SZ.HEADER, SZ.HEADER + compSz));
|
|
478
|
+
} else {
|
|
479
|
+
raw = buf.slice(SZ.HEADER, SZ.HEADER + dataSz);
|
|
480
|
+
}
|
|
481
|
+
const r = decRow(raw, off);
|
|
482
|
+
return r.row;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
appendRows(name, schema, rows, pkField) {
|
|
486
|
+
this._open();
|
|
487
|
+
const t = this.tables.get(name);
|
|
488
|
+
if (!t) { this.writeTable(name, schema, rows, pkField); return; }
|
|
489
|
+
t.schema = schema;
|
|
490
|
+
|
|
491
|
+
const { rows: oldRows } = this.readTable(name);
|
|
492
|
+
oldRows.push(...rows);
|
|
493
|
+
this.writeTable(name, schema, oldRows, pkField);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = JSQLFormat;
|