jsql-neo 2.2.1 → 3.0.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/README.md +0 -0
- package/bin/jsql-neo-server +0 -0
- package/bin/linux-x64/jsql-neo-server +0 -0
- package/index.js +9 -5
- package/lib/btree.js +0 -0
- package/lib/cache.js +0 -0
- package/lib/client.js +125 -0
- package/lib/compress_pool.js +0 -0
- package/lib/compress_worker.js +0 -0
- package/lib/database.js +0 -0
- package/lib/date-types.js +0 -0
- package/lib/errors.js +0 -0
- package/lib/jsql_format.js +0 -0
- package/lib/query.js +0 -0
- package/lib/table.js +0 -0
- package/package.json +12 -10
- package/postinstall.js +38 -0
- package/lib/jsql_format2.js +0 -448
package/README.md
CHANGED
|
File without changes
|
|
Binary file
|
|
Binary file
|
package/index.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
// © Vexify 2026 All Rights Reserved.
|
|
2
1
|
/**
|
|
3
|
-
* JSQL-NEO —
|
|
4
|
-
* v2.1.0 — B-Tree 索引、WAL、哈希 JOIN、Redis 兼容 Cache、事务隔离
|
|
2
|
+
* JSQL-NEO v3.0.0 — Rust-Powered Embedded Database
|
|
5
3
|
*
|
|
6
4
|
* @example
|
|
7
5
|
* const jsql = require('jsql-neo');
|
|
8
|
-
* const db = new jsql.
|
|
6
|
+
* const db = new jsql.JSQL({ port: 6379 });
|
|
7
|
+
* await db.start();
|
|
8
|
+
* await db.createTable('users', { name: { type: 'string' }, age: { type: 'integer' } });
|
|
9
|
+
* await db.insert('users', { name: 'Alice', age: 30 });
|
|
10
|
+
* const user = await db.findById('users', 1);
|
|
11
|
+
* await db.stop();
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
module.exports = {
|
|
15
|
+
JSQL: require('./lib/client').JSQL,
|
|
12
16
|
Database: require('./lib/database'),
|
|
13
17
|
Table: require('./lib/table'),
|
|
14
18
|
Query: require('./lib/query'),
|
|
@@ -17,4 +21,4 @@ module.exports = {
|
|
|
17
21
|
JSQL_Error: require('./lib/errors').JSQL_Error,
|
|
18
22
|
ErrorCodes: require('./lib/errors').ErrorCodes,
|
|
19
23
|
JSQLFormat: require('./lib/jsql_format')
|
|
20
|
-
};
|
|
24
|
+
};
|
package/lib/btree.js
CHANGED
|
File without changes
|
package/lib/cache.js
CHANGED
|
File without changes
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
function binPath() {
|
|
7
|
+
const dir = path.join(__dirname, '..', 'bin');
|
|
8
|
+
const name = process.platform === 'win32' ? 'jsql-neo-server.exe' : 'jsql-neo-server';
|
|
9
|
+
return path.join(dir, name);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class JSQL {
|
|
13
|
+
constructor(opts = {}) {
|
|
14
|
+
this.host = opts.host || '127.0.0.1';
|
|
15
|
+
this.port = opts.port || 6379;
|
|
16
|
+
this.dataDir = opts.dataDir || null;
|
|
17
|
+
this._process = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async start() {
|
|
21
|
+
if (this._process) return;
|
|
22
|
+
const bp = binPath();
|
|
23
|
+
if (!fs.existsSync(bp)) {
|
|
24
|
+
throw new Error(`jsql-neo-server binary not found at ${bp}. Run 'npm run postinstall' to build it.`);
|
|
25
|
+
}
|
|
26
|
+
const args = [];
|
|
27
|
+
if (this.dataDir) {
|
|
28
|
+
args.push('--data-dir', this.dataDir);
|
|
29
|
+
}
|
|
30
|
+
this._process = spawn(bp, args, {
|
|
31
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
32
|
+
env: {
|
|
33
|
+
...process.env,
|
|
34
|
+
JSQL_HOST: this.host,
|
|
35
|
+
JSQL_PORT: String(this.port),
|
|
36
|
+
...(this.dataDir ? { JSQL_DATA_DIR: this.dataDir } : {}),
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
this._process.stdout.on('data', d => process.stdout.write(d));
|
|
40
|
+
this._process.stderr.on('data', d => process.stderr.write(d));
|
|
41
|
+
this._process.on('exit', (code) => { this._process = null; });
|
|
42
|
+
await this._waitForServer();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async stop() {
|
|
46
|
+
if (!this._process) return;
|
|
47
|
+
this._process.kill();
|
|
48
|
+
await new Promise(r => setTimeout(r, 500));
|
|
49
|
+
this._process = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_waitForServer(retries = 20) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const tryConnect = (n) => {
|
|
55
|
+
if (n <= 0) return reject(new Error('server did not start'));
|
|
56
|
+
const req = http.get(`http://${this.host}:${this.port}/health`, (res) => {
|
|
57
|
+
if (res.statusCode === 200) resolve();
|
|
58
|
+
else setTimeout(() => tryConnect(n - 1), 200);
|
|
59
|
+
});
|
|
60
|
+
req.on('error', () => setTimeout(() => tryConnect(n - 1), 200));
|
|
61
|
+
req.setTimeout(500, () => { req.destroy(); setTimeout(() => tryConnect(n - 1), 200); });
|
|
62
|
+
};
|
|
63
|
+
tryConnect(retries);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async _request(method, path, body) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const opts = {
|
|
70
|
+
hostname: this.host,
|
|
71
|
+
port: this.port,
|
|
72
|
+
path,
|
|
73
|
+
method,
|
|
74
|
+
headers: body ? { 'Content-Type': 'application/json' } : {},
|
|
75
|
+
};
|
|
76
|
+
const req = http.request(opts, (res) => {
|
|
77
|
+
let data = '';
|
|
78
|
+
res.on('data', c => data += c);
|
|
79
|
+
res.on('end', () => {
|
|
80
|
+
try { resolve(JSON.parse(data)); }
|
|
81
|
+
catch (e) { reject(new Error(`invalid JSON: ${data}`)); }
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
req.on('error', reject);
|
|
85
|
+
if (body) req.write(JSON.stringify(body));
|
|
86
|
+
req.end();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async createTable(name, schema) {
|
|
91
|
+
return this._request('POST', `/db/${name}`, schema);
|
|
92
|
+
}
|
|
93
|
+
async dropTable(name) {
|
|
94
|
+
return this._request('DELETE', `/db/${name}`);
|
|
95
|
+
}
|
|
96
|
+
async insert(table, data) {
|
|
97
|
+
return this._request('POST', `/db/${table}/insert`, { data: Array.isArray(data) ? data : [data] });
|
|
98
|
+
}
|
|
99
|
+
async insertMany(table, data) {
|
|
100
|
+
return this._request('POST', `/db/${table}/insertMany`, { data });
|
|
101
|
+
}
|
|
102
|
+
async findById(table, id) {
|
|
103
|
+
return this._request('GET', `/db/${table}/${id}`);
|
|
104
|
+
}
|
|
105
|
+
async find(table, query = {}) {
|
|
106
|
+
return this._request('POST', `/db/${table}/find`, query);
|
|
107
|
+
}
|
|
108
|
+
async count(table) {
|
|
109
|
+
return this._request('GET', `/db/${table}/count`);
|
|
110
|
+
}
|
|
111
|
+
async updateById(table, id, data) {
|
|
112
|
+
return this._request('PUT', `/db/${table}/${id}`, { data });
|
|
113
|
+
}
|
|
114
|
+
async removeById(table, id) {
|
|
115
|
+
return this._request('DELETE', `/db/${table}/${id}`);
|
|
116
|
+
}
|
|
117
|
+
async updateByFilter(table, filter, data) {
|
|
118
|
+
return this._request('POST', `/db/${table}/update`, { filter, data });
|
|
119
|
+
}
|
|
120
|
+
async removeByFilter(table, filter) {
|
|
121
|
+
return this._request('POST', `/db/${table}/remove`, { filter });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { JSQL };
|
package/lib/compress_pool.js
CHANGED
|
File without changes
|
package/lib/compress_worker.js
CHANGED
|
File without changes
|
package/lib/database.js
CHANGED
|
File without changes
|
package/lib/date-types.js
CHANGED
|
File without changes
|
package/lib/errors.js
CHANGED
|
File without changes
|
package/lib/jsql_format.js
CHANGED
|
File without changes
|
package/lib/query.js
CHANGED
|
File without changes
|
package/lib/table.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "JSQL-NEO —
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "JSQL-NEO — Rust-powered embedded database with REST API, B-Tree indexes, WAL, crash recovery",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"index.js",
|
|
8
8
|
"lib/",
|
|
9
|
+
"bin/",
|
|
10
|
+
"postinstall.js",
|
|
9
11
|
"README.md",
|
|
10
12
|
"LICENSE"
|
|
11
13
|
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"postinstall": "node postinstall.js",
|
|
16
|
+
"postinstall:build": "cd ../jsql-neo-server && cargo build --release"
|
|
17
|
+
},
|
|
12
18
|
"keywords": [
|
|
13
19
|
"database",
|
|
14
20
|
"sql",
|
|
15
21
|
"embedded",
|
|
22
|
+
"rust",
|
|
16
23
|
"json",
|
|
17
24
|
"javascript",
|
|
18
25
|
"jsql",
|
|
19
26
|
"nosql",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"file-based",
|
|
23
|
-
"btree",
|
|
24
|
-
"wal",
|
|
25
|
-
"index",
|
|
26
|
-
"query-optimizer"
|
|
27
|
+
"rest-api",
|
|
28
|
+
"high-performance"
|
|
27
29
|
],
|
|
28
30
|
"author": "Vexify",
|
|
29
31
|
"license": "Apache-2.0",
|
|
30
32
|
"engines": {
|
|
31
33
|
"node": ">=14.0.0"
|
|
32
34
|
}
|
|
33
|
-
}
|
|
35
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const BIN_NAME = process.platform === 'win32' ? 'jsql-neo-server.exe' : 'jsql-neo-server';
|
|
6
|
+
const BP = path.join(__dirname, 'bin', BIN_NAME);
|
|
7
|
+
|
|
8
|
+
function main() {
|
|
9
|
+
if (fs.existsSync(BP)) {
|
|
10
|
+
fs.chmodSync(BP, 0o755);
|
|
11
|
+
console.log(`[jsql-neo] binary ready: ${BP}`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Build from source
|
|
16
|
+
const serverDir = path.join(__dirname, '..', 'jsql-neo-server');
|
|
17
|
+
if (fs.existsSync(path.join(serverDir, 'Cargo.toml'))) {
|
|
18
|
+
console.log('[jsql-neo] building Rust server from source...');
|
|
19
|
+
try {
|
|
20
|
+
execSync('cargo build --release', { cwd: serverDir, stdio: 'inherit' });
|
|
21
|
+
const src = path.join(serverDir, 'target', 'release', BIN_NAME);
|
|
22
|
+
if (fs.existsSync(src)) {
|
|
23
|
+
fs.mkdirSync(path.join(__dirname, 'bin'), { recursive: true });
|
|
24
|
+
fs.copyFileSync(src, BP);
|
|
25
|
+
fs.chmodSync(BP, 0o755);
|
|
26
|
+
console.log(`[jsql-neo] binary built: ${BP}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.warn('[jsql-neo] cargo build failed:', e.message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.warn(`[jsql-neo] binary not found at ${BP}`);
|
|
35
|
+
console.warn('[jsql-neo] install Rust from https://rustup.rs and run: npm run build');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
main();
|
package/lib/jsql_format2.js
DELETED
|
@@ -1,448 +0,0 @@
|
|
|
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 };
|