jsql-neo 2.2.0 → 2.2.1
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/index.js +2 -1
- package/lib/compress_pool.js +122 -0
- package/lib/compress_worker.js +32 -0
- package/lib/database.js +32 -0
- package/lib/jsql_format.js +478 -0
- package/lib/jsql_format2.js +448 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -15,5 +15,6 @@ module.exports = {
|
|
|
15
15
|
BTree: require('./lib/btree'),
|
|
16
16
|
Cache: require('./lib/cache'),
|
|
17
17
|
JSQL_Error: require('./lib/errors').JSQL_Error,
|
|
18
|
-
ErrorCodes: require('./lib/errors').ErrorCodes
|
|
18
|
+
ErrorCodes: require('./lib/errors').ErrorCodes,
|
|
19
|
+
JSQLFormat: require('./lib/jsql_format')
|
|
19
20
|
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const { Worker, MessageChannel, receiveMessageOnPort } = require('worker_threads');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const zlib = require('zlib');
|
|
5
|
+
|
|
6
|
+
const POOL_SIZE = Math.max(2, Math.min(os.cpus().length - 1, 8));
|
|
7
|
+
|
|
8
|
+
let pool = null;
|
|
9
|
+
|
|
10
|
+
class SyncCompressionPool {
|
|
11
|
+
constructor(size) {
|
|
12
|
+
this.size = size;
|
|
13
|
+
this.workers = [];
|
|
14
|
+
this.ports = [];
|
|
15
|
+
this.nextId = 1;
|
|
16
|
+
const wp = path.join(__dirname, 'compress_worker.js');
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < size; i++) {
|
|
19
|
+
const { port1, port2 } = new MessageChannel();
|
|
20
|
+
const worker = new Worker(wp);
|
|
21
|
+
worker.postMessage({ type: 'init', port: port2 }, [port2]);
|
|
22
|
+
worker.unref();
|
|
23
|
+
this.workers.push({ worker, port: port1 });
|
|
24
|
+
this.ports.push(port1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_getIdle() {
|
|
29
|
+
for (const p of this.ports) {
|
|
30
|
+
if (!p._busy) return p;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_copyBuffer(data) {
|
|
36
|
+
const src = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
37
|
+
const ab = new ArrayBuffer(src.length);
|
|
38
|
+
const dst = Buffer.from(ab);
|
|
39
|
+
src.copy(dst);
|
|
40
|
+
return dst;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_sendMsg(port, msg, data) {
|
|
44
|
+
const copy = this._copyBuffer(data);
|
|
45
|
+
port._busy = true;
|
|
46
|
+
port.postMessage({ ...msg, data: copy.buffer, level: msg.level || 1 }, [copy.buffer]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_waitResult(port) {
|
|
50
|
+
let r = null;
|
|
51
|
+
while (!r) { r = receiveMessageOnPort(port); }
|
|
52
|
+
port._busy = false;
|
|
53
|
+
if (r.message.error) throw new Error(r.message.error);
|
|
54
|
+
return r.message;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
compressSync(data, level = 1) {
|
|
58
|
+
const port = this._getIdle();
|
|
59
|
+
if (!port) {
|
|
60
|
+
return { result: zlib.gzipSync(Buffer.isBuffer(data) ? data : Buffer.from(data), { level }).buffer };
|
|
61
|
+
}
|
|
62
|
+
const id = this.nextId++;
|
|
63
|
+
this._sendMsg(port, { type: 'compress', id, level }, data);
|
|
64
|
+
return this._waitResult(port);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
decompressSync(data) {
|
|
68
|
+
const port = this._getIdle();
|
|
69
|
+
if (!port) {
|
|
70
|
+
return { result: zlib.gunzipSync(Buffer.isBuffer(data) ? data : Buffer.from(data)).buffer };
|
|
71
|
+
}
|
|
72
|
+
const id = this.nextId++;
|
|
73
|
+
this._sendMsg(port, { type: 'decompress', id }, data);
|
|
74
|
+
return this._waitResult(port);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
compressBatchSync(blocks, level = 1) {
|
|
78
|
+
const n = blocks.length;
|
|
79
|
+
const results = new Array(n);
|
|
80
|
+
let submitted = 0;
|
|
81
|
+
let completed = 0;
|
|
82
|
+
|
|
83
|
+
const trySubmit = () => {
|
|
84
|
+
while (submitted < n) {
|
|
85
|
+
const port = this._getIdle();
|
|
86
|
+
if (!port) break;
|
|
87
|
+
const idx = submitted++;
|
|
88
|
+
port._idx = idx;
|
|
89
|
+
port._busy = true;
|
|
90
|
+
const copy = this._copyBuffer(blocks[idx]);
|
|
91
|
+
port.postMessage({ type: 'compress', id: this.nextId++, data: copy.buffer, level }, [copy.buffer]);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
trySubmit();
|
|
96
|
+
while (completed < n) {
|
|
97
|
+
for (const port of this.ports) {
|
|
98
|
+
if (!port._busy) continue;
|
|
99
|
+
const msg = receiveMessageOnPort(port);
|
|
100
|
+
if (msg) {
|
|
101
|
+
results[port._idx] = msg.message;
|
|
102
|
+
port._busy = false;
|
|
103
|
+
completed++;
|
|
104
|
+
trySubmit();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
terminate() {
|
|
112
|
+
for (const { worker } of this.workers) worker.terminate();
|
|
113
|
+
pool = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getPool() {
|
|
118
|
+
if (!pool) pool = new SyncCompressionPool(POOL_SIZE);
|
|
119
|
+
return pool;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { getPool, SyncCompressionPool };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const { parentPort } = require('worker_threads');
|
|
2
|
+
const zlib = require('zlib');
|
|
3
|
+
|
|
4
|
+
let commPort = null;
|
|
5
|
+
|
|
6
|
+
parentPort.on('message', (msg) => {
|
|
7
|
+
if (msg.type === 'init' && msg.port) {
|
|
8
|
+
commPort = msg.port;
|
|
9
|
+
commPort.on('message', (task) => {
|
|
10
|
+
try {
|
|
11
|
+
if (task.type === 'compress') {
|
|
12
|
+
const buf = Buffer.from(task.data);
|
|
13
|
+
const result = zlib.gzipSync(buf, { level: task.level || 1 });
|
|
14
|
+
const ab = new ArrayBuffer(result.length);
|
|
15
|
+
const view = new Uint8Array(ab);
|
|
16
|
+
for (let i = 0; i < result.length; i++) view[i] = result[i];
|
|
17
|
+
commPort.postMessage({ id: task.id, result: ab, len: result.length }, [ab]);
|
|
18
|
+
} else if (task.type === 'decompress') {
|
|
19
|
+
const buf = Buffer.from(task.data);
|
|
20
|
+
const result = zlib.gunzipSync(buf);
|
|
21
|
+
const ab = new ArrayBuffer(result.length);
|
|
22
|
+
const view = new Uint8Array(ab);
|
|
23
|
+
for (let i = 0; i < result.length; i++) view[i] = result[i];
|
|
24
|
+
commPort.postMessage({ id: task.id, result: ab, len: result.length }, [ab]);
|
|
25
|
+
}
|
|
26
|
+
} catch (e) {
|
|
27
|
+
commPort.postMessage({ id: task.id, error: e.message });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
commPort.unref();
|
|
31
|
+
}
|
|
32
|
+
});
|
package/lib/database.js
CHANGED
|
@@ -9,6 +9,7 @@ const path = require('path');
|
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
const Table = require('./table');
|
|
11
11
|
const { createError } = require('./errors');
|
|
12
|
+
const JSQLFormat = require('./jsql_format');
|
|
12
13
|
|
|
13
14
|
class Database {
|
|
14
15
|
/**
|
|
@@ -57,6 +58,10 @@ class Database {
|
|
|
57
58
|
// 事务隔离
|
|
58
59
|
this._isolationLevel = options.isolationLevel || 'READ_COMMITTED';
|
|
59
60
|
|
|
61
|
+
// JSql 格式 (binary block-based)
|
|
62
|
+
this._jsqlMode = this._filePath && this._filePath.endsWith('.jsql');
|
|
63
|
+
this._jsqlFormat = this._jsqlMode ? new JSQLFormat(this._filePath) : null;
|
|
64
|
+
|
|
60
65
|
// 文件锁
|
|
61
66
|
this._fileLockEnabled = options.fileLock === true && !this._memoryMode;
|
|
62
67
|
this._lockFd = null;
|
|
@@ -898,6 +903,20 @@ class Database {
|
|
|
898
903
|
|
|
899
904
|
if (this._memoryMode) return;
|
|
900
905
|
|
|
906
|
+
if (this._jsqlMode) {
|
|
907
|
+
const all = {};
|
|
908
|
+
for (const [name, table] of Object.entries(this._tables)) {
|
|
909
|
+
if (name === '__meta__') continue;
|
|
910
|
+
const pkField = Object.keys(table._schema).find(f => table._schema[f].primaryKey);
|
|
911
|
+
all[name] = { schema: table._schema, rows: table._rows, pkField };
|
|
912
|
+
}
|
|
913
|
+
this._jsqlFormat.saveAll(all);
|
|
914
|
+
this._jsqlFormat._close();
|
|
915
|
+
this._dirty = false;
|
|
916
|
+
this._checkpoint();
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
901
920
|
const data = { __schema__: {}, __meta__: { version: this._getCurrentVersion() } };
|
|
902
921
|
for (const [name, table] of Object.entries(this._tables)) {
|
|
903
922
|
if (name === '__meta__') continue;
|
|
@@ -917,6 +936,18 @@ class Database {
|
|
|
917
936
|
|
|
918
937
|
_load() {
|
|
919
938
|
try {
|
|
939
|
+
if (this._jsqlMode) {
|
|
940
|
+
this._jsqlFormat._open();
|
|
941
|
+
for (const [name, t] of this._jsqlFormat.tables) {
|
|
942
|
+
if (!this._tables[name]) this.createTable(name, t.schema);
|
|
943
|
+
if (t.rowCount > 0) {
|
|
944
|
+
const { rows } = this._jsqlFormat.readTableSync(name);
|
|
945
|
+
this._tables[name]._loadRows(rows);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
|
|
920
951
|
const json = this._readFile(this._filePath);
|
|
921
952
|
const data = JSON.parse(json);
|
|
922
953
|
|
|
@@ -964,6 +995,7 @@ class Database {
|
|
|
964
995
|
if (this._dirty && !this._memoryMode) {
|
|
965
996
|
this.save();
|
|
966
997
|
}
|
|
998
|
+
if (this._jsqlFormat) this._jsqlFormat._close();
|
|
967
999
|
this._releaseLock();
|
|
968
1000
|
this._tables = {};
|
|
969
1001
|
}
|
|
@@ -0,0 +1,478 @@
|
|
|
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
|
+
fs.ftruncateSync(this.fd, SZ.SUPER);
|
|
309
|
+
const sb = Buffer.alloc(SZ.SUPER); sb.write('JSQL', 0, 'utf8');
|
|
310
|
+
fs.writeSync(this.fd, sb, 0, SZ.SUPER, 0);
|
|
311
|
+
this.nBlocks = 1;
|
|
312
|
+
this._nextTableId = 1;
|
|
313
|
+
this.tables.clear();
|
|
314
|
+
this.pkIdx.clear();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
saveAll(allTables) {
|
|
318
|
+
this._open();
|
|
319
|
+
this._reset();
|
|
320
|
+
|
|
321
|
+
for (const [name, info] of Object.entries(allTables)) {
|
|
322
|
+
const { schema, rows, pkField } = info;
|
|
323
|
+
if (!rows || rows.length === 0) continue;
|
|
324
|
+
|
|
325
|
+
const tableId = this._nextTableId++;
|
|
326
|
+
this.tables.set(name, { schema, rowCount:0, pkBlock:0, dataBlock:0, tableId });
|
|
327
|
+
|
|
328
|
+
const pkEntries = [];
|
|
329
|
+
const rawBlocks = [];
|
|
330
|
+
const blockIds = [];
|
|
331
|
+
let curBlock = this._alloc();
|
|
332
|
+
let buf = Buffer.alloc(SZ.BLOCK);
|
|
333
|
+
let bp = SZ.HEADER;
|
|
334
|
+
mkHeader(BT.DATA, curBlock, tableId).copy(buf);
|
|
335
|
+
|
|
336
|
+
// Pre-build field encoders once per table
|
|
337
|
+
const encoders = buildEncoders(schema);
|
|
338
|
+
// Scratch buffer for row encoding (max row size fits in one block)
|
|
339
|
+
const scratch = Buffer.allocUnsafe(SZ.DATA);
|
|
340
|
+
|
|
341
|
+
for (const row of rows) {
|
|
342
|
+
const end = encRowInPlace(row, encoders, scratch, 0);
|
|
343
|
+
if (bp + end > SZ.BLOCK) {
|
|
344
|
+
const raw = Buffer.from(buf.subarray(SZ.HEADER, bp));
|
|
345
|
+
rawBlocks.push(raw);
|
|
346
|
+
blockIds.push(curBlock);
|
|
347
|
+
buf.writeUInt32LE(bp - SZ.HEADER, 40);
|
|
348
|
+
|
|
349
|
+
curBlock = this._alloc();
|
|
350
|
+
buf = Buffer.alloc(SZ.BLOCK);
|
|
351
|
+
bp = SZ.HEADER;
|
|
352
|
+
mkHeader(BT.DATA, curBlock, tableId).copy(buf);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const startOff = bp;
|
|
356
|
+
scratch.copy(buf, bp, 0, end);
|
|
357
|
+
bp += end;
|
|
358
|
+
|
|
359
|
+
if (pkField && row[pkField] !== undefined) {
|
|
360
|
+
pkEntries.push({ hash: hashPK(row[pkField]), block: curBlock, off: startOff - SZ.HEADER });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (bp > SZ.HEADER) {
|
|
365
|
+
const raw = Buffer.from(buf.subarray(SZ.HEADER, bp));
|
|
366
|
+
rawBlocks.push(raw);
|
|
367
|
+
blockIds.push(curBlock);
|
|
368
|
+
buf.writeUInt32LE(bp - SZ.HEADER, 40);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Compress and write blocks
|
|
372
|
+
for (let i = 0; i < rawBlocks.length; i++) {
|
|
373
|
+
const bid = blockIds[i];
|
|
374
|
+
const compBuf = zlib.gzipSync(rawBlocks[i], { level: 1 });
|
|
375
|
+
const block = Buffer.alloc(SZ.BLOCK);
|
|
376
|
+
mkHeader(BT.DATA, bid, tableId).copy(block);
|
|
377
|
+
block.writeUInt32LE(rawBlocks[i].length, 40);
|
|
378
|
+
block[48] = 1;
|
|
379
|
+
block.writeUInt32LE(compBuf.length, 44);
|
|
380
|
+
compBuf.copy(block, SZ.HEADER);
|
|
381
|
+
finalize(block);
|
|
382
|
+
fs.writeSync(this.fd, block, 0, SZ.BLOCK, bid * SZ.BLOCK);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const t = this.tables.get(name);
|
|
386
|
+
t.rowCount = rows.length;
|
|
387
|
+
t.dataBlock = blockIds.length > 0 ? blockIds[blockIds.length - 1] : 0;
|
|
388
|
+
|
|
389
|
+
pkEntries.sort((a, b) => a.hash - b.hash);
|
|
390
|
+
this.pkIdx.set(name, pkEntries);
|
|
391
|
+
this._saveIdx(name);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this._saveSuper();
|
|
395
|
+
this.dirty = false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
writeTable(name, schema, rows, pkField) {
|
|
399
|
+
this.saveAll({ [name]: { schema, rows, pkField } });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
readTableSync(name) {
|
|
403
|
+
this._open();
|
|
404
|
+
const t = this.tables.get(name);
|
|
405
|
+
if (!t) return { schema: null, rows: [] };
|
|
406
|
+
|
|
407
|
+
const rows = [];
|
|
408
|
+
for (let bid = 1; bid < this.nBlocks; bid++) {
|
|
409
|
+
const buf = Buffer.alloc(SZ.BLOCK);
|
|
410
|
+
try { fs.readSync(this.fd, buf, 0, SZ.BLOCK, bid * SZ.BLOCK); } catch(e) { continue; }
|
|
411
|
+
if (buf[36] !== BT.DATA || buf[37] !== t.tableId) continue;
|
|
412
|
+
if (!verifyHash(buf)) continue;
|
|
413
|
+
|
|
414
|
+
const compFlag = buf[48];
|
|
415
|
+
const raw = compFlag
|
|
416
|
+
? zlib.gunzipSync(buf.slice(SZ.HEADER, SZ.HEADER + buf.readUInt32LE(44)))
|
|
417
|
+
: buf.slice(SZ.HEADER, SZ.HEADER + buf.readUInt32LE(40));
|
|
418
|
+
|
|
419
|
+
let off = 0;
|
|
420
|
+
while (off + 4 <= raw.length) {
|
|
421
|
+
const l = raw.readUInt32LE(off);
|
|
422
|
+
if (l === 0 || off + 4 + l > raw.length) break;
|
|
423
|
+
const r = decRow(raw, off);
|
|
424
|
+
rows.push(r.row);
|
|
425
|
+
off = r.off;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { schema: t.schema, rows };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
readTable(name) {
|
|
432
|
+
return this.readTableSync(name);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
findRow(name, pkValue) {
|
|
436
|
+
const entries = this.pkIdx.get(name);
|
|
437
|
+
if (!entries || entries.length === 0) return null;
|
|
438
|
+
const h = hashPK(pkValue);
|
|
439
|
+
let lo = 0, hi = entries.length - 1;
|
|
440
|
+
while (lo <= hi) {
|
|
441
|
+
const m = (lo + hi) >>> 1;
|
|
442
|
+
const e = entries[m];
|
|
443
|
+
if (e.hash === h) return this._readInBlock(e.block, e.off);
|
|
444
|
+
if (e.hash < h) lo = m + 1;
|
|
445
|
+
else hi = m - 1;
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
_readInBlock(blockId, off) {
|
|
451
|
+
const buf = Buffer.alloc(SZ.BLOCK);
|
|
452
|
+
fs.readSync(this.fd, buf, 0, SZ.BLOCK, blockId * SZ.BLOCK);
|
|
453
|
+
const compFlag = buf[48];
|
|
454
|
+
const compSz = buf.readUInt32LE(44);
|
|
455
|
+
const dataSz = buf.readUInt32LE(40);
|
|
456
|
+
let raw;
|
|
457
|
+
if (compFlag) {
|
|
458
|
+
raw = zlib.gunzipSync(buf.slice(SZ.HEADER, SZ.HEADER + compSz));
|
|
459
|
+
} else {
|
|
460
|
+
raw = buf.slice(SZ.HEADER, SZ.HEADER + dataSz);
|
|
461
|
+
}
|
|
462
|
+
const r = decRow(raw, off);
|
|
463
|
+
return r.row;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
appendRows(name, schema, rows, pkField) {
|
|
467
|
+
this._open();
|
|
468
|
+
const t = this.tables.get(name);
|
|
469
|
+
if (!t) { this.writeTable(name, schema, rows, pkField); return; }
|
|
470
|
+
t.schema = schema;
|
|
471
|
+
|
|
472
|
+
const { rows: oldRows } = this.readTable(name);
|
|
473
|
+
oldRows.push(...rows);
|
|
474
|
+
this.writeTable(name, schema, oldRows, pkField);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
module.exports = JSQLFormat;
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
const zlib = require('zlib');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
const MAGIC = Buffer.from('4A534F4C', 'hex');
|
|
6
|
+
const VERSION = 1;
|
|
7
|
+
const BLOCK_SIZE = 65536;
|
|
8
|
+
const BLOCK_HEADER_SIZE = 64;
|
|
9
|
+
const BLOCK_DATA_SIZE = BLOCK_SIZE - BLOCK_HEADER_SIZE;
|
|
10
|
+
const SUPERBLOCK_SIZE = 4096;
|
|
11
|
+
|
|
12
|
+
const BT = { FREE: 0, DATA: 1, PK_INDEX: 2, SUPER: 3 };
|
|
13
|
+
|
|
14
|
+
const TYPE = {
|
|
15
|
+
NULL: 0, BOOL: 1, INT: 2, UINT: 3, FLOAT: 4,
|
|
16
|
+
STRING: 5, BUFFER: 6, ARRAY: 7, OBJECT: 8
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function typeTag(v) {
|
|
20
|
+
if (v === null || v === undefined) return TYPE.NULL;
|
|
21
|
+
if (typeof v === 'boolean') return TYPE.BOOL;
|
|
22
|
+
if (typeof v === 'number') return Number.isInteger(v) ? TYPE.INT : TYPE.FLOAT;
|
|
23
|
+
if (typeof v === 'string') return TYPE.STRING;
|
|
24
|
+
if (v instanceof Buffer) return TYPE.BUFFER;
|
|
25
|
+
return Array.isArray(v) ? TYPE.ARRAY : TYPE.OBJECT;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function encodeValue(v) {
|
|
29
|
+
const tag = typeTag(v);
|
|
30
|
+
switch (tag) {
|
|
31
|
+
case TYPE.NULL: return Buffer.from([tag]);
|
|
32
|
+
case TYPE.BOOL: return Buffer.from([tag, v ? 1 : 0]);
|
|
33
|
+
case TYPE.INT: { const b = Buffer.alloc(5); b[0] = tag; b.writeInt32LE(v, 1); return b; }
|
|
34
|
+
case TYPE.UINT: { const b = Buffer.alloc(5); b[0] = tag; b.writeUInt32LE(v, 1); return b; }
|
|
35
|
+
case TYPE.FLOAT: { const b = Buffer.alloc(9); b[0] = tag; b.writeDoubleLE(v, 1); return b; }
|
|
36
|
+
case TYPE.STRING: { const s = Buffer.from(v, 'utf8'); const b = Buffer.alloc(5 + s.length); b[0] = tag; b.writeUInt32LE(s.length, 1); s.copy(b, 5); return b; }
|
|
37
|
+
case TYPE.BUFFER: { const b = Buffer.alloc(5 + v.length); b[0] = tag; b.writeUInt32LE(v.length, 1); v.copy(b, 5); return b; }
|
|
38
|
+
case TYPE.ARRAY: {
|
|
39
|
+
const parts = [Buffer.from([tag]), encodeValue(v.length)];
|
|
40
|
+
for (const item of v) parts.push(encodeValue(item));
|
|
41
|
+
return Buffer.concat(parts);
|
|
42
|
+
}
|
|
43
|
+
case TYPE.OBJECT: {
|
|
44
|
+
const keys = Object.keys(v);
|
|
45
|
+
const parts = [Buffer.from([tag]), encodeValue(keys.length)];
|
|
46
|
+
for (const k of keys) { parts.push(encodeValue(k)); parts.push(encodeValue(v[k])); }
|
|
47
|
+
return Buffer.concat(parts);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function decodeValue(buf, off) {
|
|
53
|
+
const tag = buf[off++];
|
|
54
|
+
switch (tag) {
|
|
55
|
+
case TYPE.NULL: return { v: null, off };
|
|
56
|
+
case TYPE.BOOL: return { v: buf[off] !== 0, off: off + 1 };
|
|
57
|
+
case TYPE.INT: return { v: buf.readInt32LE(off), off: off + 4 };
|
|
58
|
+
case TYPE.UINT: return { v: buf.readUInt32LE(off), off: off + 4 };
|
|
59
|
+
case TYPE.FLOAT: return { v: buf.readDoubleLE(off), off: off + 8 };
|
|
60
|
+
case TYPE.STRING: { const len = buf.readUInt32LE(off); off += 4; return { v: buf.toString('utf8', off, off + len), off: off + len }; }
|
|
61
|
+
case TYPE.BUFFER: { const len = buf.readUInt32LE(off); off += 4; return { v: Buffer.from(buf.subarray(off, off + len)), off: off + len }; }
|
|
62
|
+
case TYPE.ARRAY: {
|
|
63
|
+
let r = decodeValue(buf, off); off = r.off;
|
|
64
|
+
const len = r.v, arr = new Array(len);
|
|
65
|
+
for (let i = 0; i < len; i++) { r = decodeValue(buf, off); arr[i] = r.v; off = r.off; }
|
|
66
|
+
return { v: arr, off };
|
|
67
|
+
}
|
|
68
|
+
case TYPE.OBJECT: {
|
|
69
|
+
let r = decodeValue(buf, off); off = r.off;
|
|
70
|
+
const len = r.v, obj = {};
|
|
71
|
+
for (let i = 0; i < len; i++) { r = decodeValue(buf, off); const k = r.v; off = r.off; r = decodeValue(buf, off); obj[k] = r.v; off = r.off; }
|
|
72
|
+
return { v: obj, off };
|
|
73
|
+
}
|
|
74
|
+
default: throw new Error('Unknown type: ' + tag);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function makeBlockHeader(type, seq) {
|
|
79
|
+
const h = Buffer.alloc(BLOCK_HEADER_SIZE);
|
|
80
|
+
h.writeUInt32LE(seq, 32);
|
|
81
|
+
h[36] = type;
|
|
82
|
+
return h;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function finalizeBlock(block) {
|
|
86
|
+
const hash = crypto.createHash('sha256').update(block.subarray(32)).digest();
|
|
87
|
+
hash.copy(block, 0);
|
|
88
|
+
return block;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function verifyBlockHash(block) {
|
|
92
|
+
const stored = block.slice(0, 32);
|
|
93
|
+
const computed = crypto.createHash('sha256').update(block.subarray(32)).digest();
|
|
94
|
+
return stored.equals(computed);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function encodeRow(row, schema) {
|
|
98
|
+
const fields = Object.keys(schema).filter(f => f !== '_softDelete');
|
|
99
|
+
const parts = [encodeValue(fields.length)];
|
|
100
|
+
for (const f of fields) {
|
|
101
|
+
parts.push(encodeValue(f));
|
|
102
|
+
parts.push(encodeValue(row[f]));
|
|
103
|
+
}
|
|
104
|
+
return Buffer.concat(parts);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function decodeRow(buf, off) {
|
|
108
|
+
let r = decodeValue(buf, off); off = r.off;
|
|
109
|
+
const fieldCount = r.v;
|
|
110
|
+
const row = {};
|
|
111
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
112
|
+
r = decodeValue(buf, off); const field = r.v; off = r.off;
|
|
113
|
+
r = decodeValue(buf, off); row[field] = r.v; off = r.off;
|
|
114
|
+
}
|
|
115
|
+
return { row, off };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class JSQLFile {
|
|
119
|
+
constructor(path, blockSize = BLOCK_SIZE) {
|
|
120
|
+
this.path = path;
|
|
121
|
+
this.blockSize = blockSize;
|
|
122
|
+
this.dataSize = blockSize - BLOCK_HEADER_SIZE;
|
|
123
|
+
this.fd = null;
|
|
124
|
+
this.tables = new Map();
|
|
125
|
+
this.pkIndex = new Map();
|
|
126
|
+
this.totalBlocks = 1;
|
|
127
|
+
this.freeBlocks = [];
|
|
128
|
+
this.dirty = false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_rowSize(row, schema) {
|
|
132
|
+
return encodeRow(row, schema).length + 4;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
open() {
|
|
136
|
+
if (this.fd) return;
|
|
137
|
+
if (fs.existsSync(this.path)) {
|
|
138
|
+
this.fd = fs.openSync(this.path, 'r+');
|
|
139
|
+
this._load();
|
|
140
|
+
} else {
|
|
141
|
+
this.fd = fs.openSync(this.path, 'w+');
|
|
142
|
+
this._initFile();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
close() {
|
|
147
|
+
if (!this.fd) return;
|
|
148
|
+
if (this.dirty) this._flush();
|
|
149
|
+
fs.closeSync(this.fd);
|
|
150
|
+
this.fd = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_initFile() {
|
|
154
|
+
fs.ftruncateSync(this.fd, SUPERBLOCK_SIZE);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_load() {
|
|
158
|
+
const sb = Buffer.alloc(SUPERBLOCK_SIZE);
|
|
159
|
+
fs.readSync(this.fd, sb, 0, SUPERBLOCK_SIZE, 0);
|
|
160
|
+
const magic = sb.slice(0, 4);
|
|
161
|
+
if (magic.compare(MAGIC) !== 0) throw new Error('Not a JSQL file');
|
|
162
|
+
const version = sb.readUInt16LE(4);
|
|
163
|
+
this.blockSize = sb.readUInt32LE(6) || BLOCK_SIZE;
|
|
164
|
+
this.dataSize = this.blockSize - BLOCK_HEADER_SIZE;
|
|
165
|
+
|
|
166
|
+
const tableCount = sb.readUInt16LE(10);
|
|
167
|
+
let off = 12;
|
|
168
|
+
for (let i = 0; i < tableCount; i++) {
|
|
169
|
+
const nameLen = Math.min(sb[off], 64);
|
|
170
|
+
const name = sb.toString('utf8', off + 1, off + 1 + nameLen);
|
|
171
|
+
off += 1 + nameLen;
|
|
172
|
+
const schemaLen = sb.readUInt32LE(off); off += 4;
|
|
173
|
+
const rowCount = sb.readUInt32LE(off); off += 4;
|
|
174
|
+
const pkBlock = sb.readUInt32LE(off); off += 4;
|
|
175
|
+
const dataBlock = sb.readUInt32LE(off); off += 4;
|
|
176
|
+
const schemaStr = sb.toString('utf8', off, off + schemaLen);
|
|
177
|
+
off += schemaLen;
|
|
178
|
+
|
|
179
|
+
this.tables.set(name, {
|
|
180
|
+
schema: JSON.parse(schemaStr),
|
|
181
|
+
rowCount, pkBlock, dataBlock
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Load PK index
|
|
185
|
+
this.pkIndex.set(name, this._loadPKIndex(pkBlock));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.totalBlocks = Math.max(1, Math.ceil(fs.fstatSync(this.fd).size / this.blockSize));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_loadPKIndex(blockId) {
|
|
192
|
+
const entries = [];
|
|
193
|
+
if (blockId < 1 || blockId >= this.totalBlocks) return entries;
|
|
194
|
+
const buf = Buffer.alloc(this.blockSize);
|
|
195
|
+
fs.readSync(this.fd, buf, 0, this.blockSize, blockId * this.blockSize);
|
|
196
|
+
if (!verifyBlockHash(buf)) throw new Error('PK index block corrupted');
|
|
197
|
+
|
|
198
|
+
const type = buf[36];
|
|
199
|
+
if (type !== BT.PK_INDEX) throw new Error('Not a PK index block');
|
|
200
|
+
|
|
201
|
+
let off = BLOCK_HEADER_SIZE;
|
|
202
|
+
const count = buf.readUInt32LE(off); off += 4;
|
|
203
|
+
for (let i = 0; i < count; i++) {
|
|
204
|
+
const pkHash = buf.readBigUInt64LE(off); off += 8;
|
|
205
|
+
const pkOff = buf.readUInt32LE(off); off += 4;
|
|
206
|
+
const rowLen = buf.readUInt32LE(off); off += 4;
|
|
207
|
+
const dataBlock = buf.readUInt32LE(off); off += 4;
|
|
208
|
+
const dataOff = buf.readUInt32LE(off); off += 4;
|
|
209
|
+
entries.push({ pkHash, pkOff, rowLen, dataBlock, dataOff });
|
|
210
|
+
}
|
|
211
|
+
return entries;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_savePKIndex(name) {
|
|
215
|
+
const entries = this.pkIndex.get(name) || [];
|
|
216
|
+
const table = this.tables.get(name);
|
|
217
|
+
const buf = Buffer.alloc(this.blockSize);
|
|
218
|
+
makeBlockHeader(BT.PK_INDEX, table ? table.pkBlock : 0).copy(buf);
|
|
219
|
+
|
|
220
|
+
let off = BLOCK_HEADER_SIZE;
|
|
221
|
+
buf.writeUInt32LE(entries.length, off); off += 4;
|
|
222
|
+
for (const e of entries) {
|
|
223
|
+
buf.writeBigUInt64LE(e.pkHash, off); off += 8;
|
|
224
|
+
buf.writeUInt32LE(e.pkOff, off); off += 4;
|
|
225
|
+
buf.writeUInt32LE(e.rowLen, off); off += 4;
|
|
226
|
+
buf.writeUInt32LE(e.dataBlock, off); off += 4;
|
|
227
|
+
buf.writeUInt32LE(e.dataOff, off); off += 4;
|
|
228
|
+
}
|
|
229
|
+
finalizeBlock(buf);
|
|
230
|
+
|
|
231
|
+
const blockId = table ? table.pkBlock : this._allocBlock();
|
|
232
|
+
fs.writeSync(this.fd, buf, 0, this.blockSize, blockId * this.blockSize);
|
|
233
|
+
if (table) table.pkBlock = blockId;
|
|
234
|
+
return blockId;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_allocBlock() {
|
|
238
|
+
if (this.freeBlocks.length) return this.freeBlocks.pop();
|
|
239
|
+
return this.totalBlocks++;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_flush() {
|
|
243
|
+
// Save all PK indexes and superblock
|
|
244
|
+
for (const name of this.tables.keys()) {
|
|
245
|
+
this._savePKIndex(name);
|
|
246
|
+
}
|
|
247
|
+
this._saveSuperBlock();
|
|
248
|
+
this.dirty = false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_saveSuperBlock() {
|
|
252
|
+
const sb = Buffer.alloc(SUPERBLOCK_SIZE);
|
|
253
|
+
MAGIC.copy(sb, 0);
|
|
254
|
+
sb.writeUInt16LE(VERSION, 4);
|
|
255
|
+
sb.writeUInt32LE(this.blockSize, 6);
|
|
256
|
+
sb.writeUInt16LE(this.tables.size, 10);
|
|
257
|
+
|
|
258
|
+
let off = 12;
|
|
259
|
+
for (const [name, table] of this.tables) {
|
|
260
|
+
const nameBuf = Buffer.from(name, 'utf8');
|
|
261
|
+
sb[off] = nameBuf.length; off += 1;
|
|
262
|
+
nameBuf.copy(sb, off); off += nameBuf.length;
|
|
263
|
+
const schemaStr = JSON.stringify(table.schema);
|
|
264
|
+
sb.writeUInt32LE(schemaStr.length, off); off += 4;
|
|
265
|
+
sb.writeUInt32LE(table.rowCount, off); off += 4;
|
|
266
|
+
sb.writeUInt32LE(table.pkBlock || 0, off); off += 4;
|
|
267
|
+
sb.writeUInt32LE(table.dataBlock || 0, off); off += 4;
|
|
268
|
+
Buffer.from(schemaStr, 'utf8').copy(sb, off); off += schemaStr.length;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
fs.writeSync(this.fd, sb, 0, SUPERBLOCK_SIZE, 0);
|
|
272
|
+
fs.fsyncSync(this.fd);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
writeTable(name, schema, rows, pkField) {
|
|
276
|
+
this.open();
|
|
277
|
+
const table = this.tables.get(name) || { schema, rowCount: 0, pkBlock: 0, dataBlock: 0 };
|
|
278
|
+
table.schema = schema;
|
|
279
|
+
this.tables.set(name, table);
|
|
280
|
+
|
|
281
|
+
const pkEntries = [];
|
|
282
|
+
let currentBlock = this._allocBlock();
|
|
283
|
+
let blockBuf = Buffer.alloc(this.blockSize);
|
|
284
|
+
let blockOff = BLOCK_HEADER_SIZE;
|
|
285
|
+
let seq = currentBlock;
|
|
286
|
+
let hasData = false;
|
|
287
|
+
|
|
288
|
+
makeBlockHeader(BT.DATA, seq).copy(blockBuf);
|
|
289
|
+
|
|
290
|
+
for (const row of rows) {
|
|
291
|
+
const rowData = encodeRow(row, schema);
|
|
292
|
+
const rowLen = rowData.length + 4;
|
|
293
|
+
const lenBuf = Buffer.alloc(4);
|
|
294
|
+
lenBuf.writeUInt32LE(rowData.length);
|
|
295
|
+
|
|
296
|
+
if (blockOff + rowLen > this.blockSize) {
|
|
297
|
+
// Flush current block
|
|
298
|
+
blockBuf.writeUInt16LE(0, 38); // rowCount
|
|
299
|
+
// Actually write row count
|
|
300
|
+
// We need to track rows per block properly
|
|
301
|
+
const written = blockOff - BLOCK_HEADER_SIZE;
|
|
302
|
+
blockBuf.writeUInt32LE(written, 40); // data size
|
|
303
|
+
|
|
304
|
+
// Compress
|
|
305
|
+
const rawData = blockBuf.slice(BLOCK_HEADER_SIZE, blockOff);
|
|
306
|
+
const compressed = zlib.gzipSync(rawData, { level: 6 });
|
|
307
|
+
compressed.copy(blockBuf, BLOCK_HEADER_SIZE);
|
|
308
|
+
blockBuf.writeUInt32LE(compressed.length, 44);
|
|
309
|
+
blockBuf[48] = 1; // compressed flag
|
|
310
|
+
|
|
311
|
+
// TODO: track row count properly
|
|
312
|
+
finalizeBlock(blockBuf);
|
|
313
|
+
fs.writeSync(this.fd, blockBuf, 0, this.blockSize, currentBlock * this.blockSize);
|
|
314
|
+
|
|
315
|
+
// New block
|
|
316
|
+
currentBlock = this._allocBlock();
|
|
317
|
+
seq = currentBlock;
|
|
318
|
+
blockBuf = Buffer.alloc(this.blockSize);
|
|
319
|
+
blockOff = BLOCK_HEADER_SIZE;
|
|
320
|
+
makeBlockHeader(BT.DATA, seq).copy(blockBuf);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
lenBuf.copy(blockBuf, blockOff); blockOff += 4;
|
|
324
|
+
rowData.copy(blockBuf, blockOff); blockOff += rowData.length;
|
|
325
|
+
hasData = true;
|
|
326
|
+
|
|
327
|
+
if (pkField && row[pkField] !== undefined) {
|
|
328
|
+
const pkVal = row[pkField];
|
|
329
|
+
const pkHash = BigInt(hashPK(pkVal));
|
|
330
|
+
pkEntries.push({ pkHash, pkOff: 0, rowLen: rowData.length, dataBlock: currentBlock, dataOff: blockOff - rowData.length - 4, pkVal });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (hasData) {
|
|
335
|
+
const written = blockOff - BLOCK_HEADER_SIZE;
|
|
336
|
+
blockBuf.writeUInt32LE(written, 40);
|
|
337
|
+
const rawData = blockBuf.slice(BLOCK_HEADER_SIZE, blockOff);
|
|
338
|
+
const compressed = zlib.gzipSync(rawData, { level: 6 });
|
|
339
|
+
compressed.copy(blockBuf, BLOCK_HEADER_SIZE);
|
|
340
|
+
blockBuf.writeUInt32LE(compressed.length, 44);
|
|
341
|
+
blockBuf[48] = 1;
|
|
342
|
+
finalizeBlock(blockBuf);
|
|
343
|
+
fs.writeSync(this.fd, blockBuf, 0, this.blockSize, currentBlock * this.blockSize);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
table.rowCount = rows.length;
|
|
347
|
+
table.dataBlock = seq;
|
|
348
|
+
|
|
349
|
+
// Sort PK entries by hash for binary search
|
|
350
|
+
pkEntries.sort((a, b) => a.pkHash < b.pkHash ? -1 : a.pkHash > b.pkHash ? 1 : 0);
|
|
351
|
+
this.pkIndex.set(name, pkEntries.map(e => ({ pkHash: e.pkHash, pkOff: 0, rowLen: e.rowLen, dataBlock: e.dataBlock, dataOff: e.dataOff })));
|
|
352
|
+
this._savePKIndex(name);
|
|
353
|
+
this._saveSuperBlock();
|
|
354
|
+
this.dirty = false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
readTable(name) {
|
|
358
|
+
this.open();
|
|
359
|
+
const table = this.tables.get(name);
|
|
360
|
+
if (!table) return { schema: null, rows: [] };
|
|
361
|
+
|
|
362
|
+
const rows = [];
|
|
363
|
+
const entries = this.pkIndex.get(name) || [];
|
|
364
|
+
|
|
365
|
+
// Collect unique blocks
|
|
366
|
+
const blockMap = new Map();
|
|
367
|
+
for (const e of entries) {
|
|
368
|
+
if (!blockMap.has(e.dataBlock)) blockMap.set(e.dataBlock, new Map());
|
|
369
|
+
blockMap.get(e.dataBlock).set(e.dataOff, e);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const [blockId, blockEntries] of blockMap) {
|
|
373
|
+
const buf = Buffer.alloc(this.blockSize);
|
|
374
|
+
fs.readSync(this.fd, buf, 0, this.blockSize, blockId * this.blockSize);
|
|
375
|
+
if (!verifyBlockHash(buf)) throw new Error('Block corrupted: ' + blockId);
|
|
376
|
+
|
|
377
|
+
const compressed = buf[48] === 1;
|
|
378
|
+
const compSize = buf.readUInt32LE(44);
|
|
379
|
+
let rawData;
|
|
380
|
+
if (compressed) {
|
|
381
|
+
rawData = zlib.gunzipSync(buf.slice(BLOCK_HEADER_SIZE, BLOCK_HEADER_SIZE + compSize));
|
|
382
|
+
} else {
|
|
383
|
+
rawData = buf.slice(BLOCK_HEADER_SIZE, BLOCK_HEADER_SIZE + buf.readUInt32LE(40));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Read all rows from this block
|
|
387
|
+
let off = 0;
|
|
388
|
+
while (off < rawData.length) {
|
|
389
|
+
const rowLen = rawData.readUInt32LE(off); off += 4;
|
|
390
|
+
if (rowLen === 0 || off + rowLen > rawData.length) break;
|
|
391
|
+
const r = decodeRow(rawData, off);
|
|
392
|
+
rows.push(r.row);
|
|
393
|
+
off += rowLen;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { schema: table.schema, rows };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
readRowByName(name, pkField, pkValue) {
|
|
401
|
+
this.open();
|
|
402
|
+
const table = this.tables.get(name);
|
|
403
|
+
if (!table) return null;
|
|
404
|
+
|
|
405
|
+
const entries = this.pkIndex.get(name) || [];
|
|
406
|
+
const targetHash = BigInt(hashPK(pkValue));
|
|
407
|
+
|
|
408
|
+
// Binary search
|
|
409
|
+
let lo = 0, hi = entries.length - 1;
|
|
410
|
+
while (lo <= hi) {
|
|
411
|
+
const mid = (lo + hi) >>> 1;
|
|
412
|
+
const e = entries[mid];
|
|
413
|
+
if (e.pkHash === targetHash) return this._readRowAt(e.dataBlock, e.dataOff, e.rowLen);
|
|
414
|
+
if (e.pkHash < targetHash) lo = mid + 1;
|
|
415
|
+
else hi = mid - 1;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
_readRowAt(blockId, dataOff, rowLen) {
|
|
421
|
+
const buf = Buffer.alloc(this.blockSize);
|
|
422
|
+
fs.readSync(this.fd, buf, 0, this.blockSize, blockId * this.blockSize);
|
|
423
|
+
|
|
424
|
+
const compressed = buf[48] === 1;
|
|
425
|
+
const compSize = buf.readUInt32LE(44);
|
|
426
|
+
let rawData;
|
|
427
|
+
if (compressed) {
|
|
428
|
+
rawData = zlib.gunzipSync(buf.slice(BLOCK_HEADER_SIZE, BLOCK_HEADER_SIZE + compSize));
|
|
429
|
+
} else {
|
|
430
|
+
rawData = buf.slice(BLOCK_HEADER_SIZE, BLOCK_HEADER_SIZE + buf.readUInt32LE(40));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const off = dataOff - (blockId * this.blockSize + BLOCK_HEADER_SIZE) + BLOCK_HEADER_SIZE;
|
|
434
|
+
const r = decodeRow(rawData, dataOff);
|
|
435
|
+
return r.row;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function hashPK(val) {
|
|
440
|
+
const s = String(val);
|
|
441
|
+
let h = 0;
|
|
442
|
+
for (let i = 0; i < s.length; i++) {
|
|
443
|
+
h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0;
|
|
444
|
+
}
|
|
445
|
+
return h >>> 0;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
module.exports = { JSQLFile, hashPK, encodeRow, decodeRow };
|
package/package.json
CHANGED