jsql-neo 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -0
- package/index.js +11 -6
- package/lib/btree.js +0 -0
- package/lib/cache.js +0 -0
- package/lib/client.js +127 -0
- package/lib/compress_pool.js +122 -0
- package/lib/compress_worker.js +32 -0
- package/lib/database.js +32 -0
- package/lib/date-types.js +0 -0
- package/lib/errors.js +0 -0
- package/lib/jsql_format.js +478 -0
- package/lib/query.js +0 -0
- package/lib/table.js +0 -0
- package/package.json +11 -10
- package/postinstall.js +83 -0
package/README.md
CHANGED
|
File without changes
|
package/index.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
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'),
|
|
15
19
|
BTree: require('./lib/btree'),
|
|
16
20
|
Cache: require('./lib/cache'),
|
|
17
21
|
JSQL_Error: require('./lib/errors').JSQL_Error,
|
|
18
|
-
ErrorCodes: require('./lib/errors').ErrorCodes
|
|
19
|
-
|
|
22
|
+
ErrorCodes: require('./lib/errors').ErrorCodes,
|
|
23
|
+
JSQLFormat: require('./lib/jsql_format')
|
|
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,127 @@
|
|
|
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 platform = process.platform;
|
|
8
|
+
const arch = process.arch;
|
|
9
|
+
const dir = path.join(__dirname, '..', 'bin');
|
|
10
|
+
const name = platform === 'win32' ? 'jsql-neo-server.exe' : 'jsql-neo-server';
|
|
11
|
+
return path.join(dir, `${platform}-${arch}`, name);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class JSQL {
|
|
15
|
+
constructor(opts = {}) {
|
|
16
|
+
this.host = opts.host || '127.0.0.1';
|
|
17
|
+
this.port = opts.port || 6379;
|
|
18
|
+
this.dataDir = opts.dataDir || null;
|
|
19
|
+
this._process = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async start() {
|
|
23
|
+
if (this._process) return;
|
|
24
|
+
const bp = binPath();
|
|
25
|
+
if (!fs.existsSync(bp)) {
|
|
26
|
+
throw new Error(`jsql-neo-server binary not found at ${bp}. Run 'npm run postinstall' to build it.`);
|
|
27
|
+
}
|
|
28
|
+
const args = [];
|
|
29
|
+
if (this.dataDir) {
|
|
30
|
+
args.push('--data-dir', this.dataDir);
|
|
31
|
+
}
|
|
32
|
+
this._process = spawn(bp, args, {
|
|
33
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
JSQL_HOST: this.host,
|
|
37
|
+
JSQL_PORT: String(this.port),
|
|
38
|
+
...(this.dataDir ? { JSQL_DATA_DIR: this.dataDir } : {}),
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
this._process.stdout.on('data', d => process.stdout.write(d));
|
|
42
|
+
this._process.stderr.on('data', d => process.stderr.write(d));
|
|
43
|
+
this._process.on('exit', (code) => { this._process = null; });
|
|
44
|
+
await this._waitForServer();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async stop() {
|
|
48
|
+
if (!this._process) return;
|
|
49
|
+
this._process.kill();
|
|
50
|
+
await new Promise(r => setTimeout(r, 500));
|
|
51
|
+
this._process = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_waitForServer(retries = 20) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const tryConnect = (n) => {
|
|
57
|
+
if (n <= 0) return reject(new Error('server did not start'));
|
|
58
|
+
const req = http.get(`http://${this.host}:${this.port}/health`, (res) => {
|
|
59
|
+
if (res.statusCode === 200) resolve();
|
|
60
|
+
else setTimeout(() => tryConnect(n - 1), 200);
|
|
61
|
+
});
|
|
62
|
+
req.on('error', () => setTimeout(() => tryConnect(n - 1), 200));
|
|
63
|
+
req.setTimeout(500, () => { req.destroy(); setTimeout(() => tryConnect(n - 1), 200); });
|
|
64
|
+
};
|
|
65
|
+
tryConnect(retries);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async _request(method, path, body) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const opts = {
|
|
72
|
+
hostname: this.host,
|
|
73
|
+
port: this.port,
|
|
74
|
+
path,
|
|
75
|
+
method,
|
|
76
|
+
headers: body ? { 'Content-Type': 'application/json' } : {},
|
|
77
|
+
};
|
|
78
|
+
const req = http.request(opts, (res) => {
|
|
79
|
+
let data = '';
|
|
80
|
+
res.on('data', c => data += c);
|
|
81
|
+
res.on('end', () => {
|
|
82
|
+
try { resolve(JSON.parse(data)); }
|
|
83
|
+
catch (e) { reject(new Error(`invalid JSON: ${data}`)); }
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
req.on('error', reject);
|
|
87
|
+
if (body) req.write(JSON.stringify(body));
|
|
88
|
+
req.end();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async createTable(name, schema) {
|
|
93
|
+
return this._request('POST', `/db/${name}`, schema);
|
|
94
|
+
}
|
|
95
|
+
async dropTable(name) {
|
|
96
|
+
return this._request('DELETE', `/db/${name}`);
|
|
97
|
+
}
|
|
98
|
+
async insert(table, data) {
|
|
99
|
+
return this._request('POST', `/db/${table}/insert`, { data: Array.isArray(data) ? data : [data] });
|
|
100
|
+
}
|
|
101
|
+
async insertMany(table, data) {
|
|
102
|
+
return this._request('POST', `/db/${table}/insertMany`, { data });
|
|
103
|
+
}
|
|
104
|
+
async findById(table, id) {
|
|
105
|
+
return this._request('GET', `/db/${table}/${id}`);
|
|
106
|
+
}
|
|
107
|
+
async find(table, query = {}) {
|
|
108
|
+
return this._request('POST', `/db/${table}/find`, query);
|
|
109
|
+
}
|
|
110
|
+
async count(table) {
|
|
111
|
+
return this._request('GET', `/db/${table}/count`);
|
|
112
|
+
}
|
|
113
|
+
async updateById(table, id, data) {
|
|
114
|
+
return this._request('PUT', `/db/${table}/${id}`, { data });
|
|
115
|
+
}
|
|
116
|
+
async removeById(table, id) {
|
|
117
|
+
return this._request('DELETE', `/db/${table}/${id}`);
|
|
118
|
+
}
|
|
119
|
+
async updateByFilter(table, filter, data) {
|
|
120
|
+
return this._request('POST', `/db/${table}/update`, { filter, data });
|
|
121
|
+
}
|
|
122
|
+
async removeByFilter(table, filter) {
|
|
123
|
+
return this._request('POST', `/db/${table}/remove`, { filter });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { JSQL };
|
|
@@ -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
|
}
|
package/lib/date-types.js
CHANGED
|
File without changes
|
package/lib/errors.js
CHANGED
|
File without changes
|
|
@@ -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;
|
package/lib/query.js
CHANGED
|
File without changes
|
package/lib/table.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "JSQL-NEO —
|
|
3
|
+
"version": "3.0.0",
|
|
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
|
+
"postinstall.js",
|
|
9
10
|
"README.md",
|
|
10
11
|
"LICENSE"
|
|
11
12
|
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"postinstall": "node postinstall.js",
|
|
15
|
+
"postinstall:build": "cd ../jsql-neo-server && cargo build --release"
|
|
16
|
+
},
|
|
12
17
|
"keywords": [
|
|
13
18
|
"database",
|
|
14
19
|
"sql",
|
|
15
20
|
"embedded",
|
|
21
|
+
"rust",
|
|
16
22
|
"json",
|
|
17
23
|
"javascript",
|
|
18
24
|
"jsql",
|
|
19
25
|
"nosql",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"file-based",
|
|
23
|
-
"btree",
|
|
24
|
-
"wal",
|
|
25
|
-
"index",
|
|
26
|
-
"query-optimizer"
|
|
26
|
+
"rest-api",
|
|
27
|
+
"high-performance"
|
|
27
28
|
],
|
|
28
29
|
"author": "Vexify",
|
|
29
30
|
"license": "Apache-2.0",
|
|
30
31
|
"engines": {
|
|
31
32
|
"node": ">=14.0.0"
|
|
32
33
|
}
|
|
33
|
-
}
|
|
34
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const pkg = require('./package.json');
|
|
8
|
+
const BIN_NAME = process.platform === 'win32' ? 'jsql-neo-server.exe' : 'jsql-neo-server';
|
|
9
|
+
const PLATFORM = `${process.platform}-${process.arch}`;
|
|
10
|
+
|
|
11
|
+
function binDir() {
|
|
12
|
+
return path.join(__dirname, 'bin', PLATFORM);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function binPath() {
|
|
16
|
+
return path.join(binDir(), BIN_NAME);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function download(url, dest) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const file = fs.createWriteStream(dest);
|
|
22
|
+
https.get(url, (res) => {
|
|
23
|
+
if (res.statusCode >= 300 && res.headers.location) {
|
|
24
|
+
file.close();
|
|
25
|
+
fs.unlinkSync(dest);
|
|
26
|
+
return download(res.headers.location, dest).then(resolve).catch(reject);
|
|
27
|
+
}
|
|
28
|
+
if (res.statusCode !== 200) {
|
|
29
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
res.pipe(file);
|
|
33
|
+
file.on('finish', () => { file.close(); fs.chmodSync(dest, 0o755); resolve(); });
|
|
34
|
+
}).on('error', (e) => { fs.unlinkSync(dest); reject(e); });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
const bp = binPath();
|
|
40
|
+
if (fs.existsSync(bp)) {
|
|
41
|
+
console.log(`[jsql-neo] binary already exists at ${bp}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fs.mkdirSync(binDir(), { recursive: true });
|
|
46
|
+
|
|
47
|
+
// Try to build from cargo source
|
|
48
|
+
const serverDir = path.join(__dirname, '..', 'jsql-neo-server');
|
|
49
|
+
if (fs.existsSync(path.join(serverDir, 'Cargo.toml'))) {
|
|
50
|
+
console.log('[jsql-neo] building Rust server from source...');
|
|
51
|
+
try {
|
|
52
|
+
execSync('cargo build --release', { cwd: serverDir, stdio: 'inherit' });
|
|
53
|
+
const src = path.join(serverDir, 'target', 'release', BIN_NAME);
|
|
54
|
+
if (fs.existsSync(src)) {
|
|
55
|
+
fs.copyFileSync(src, bp);
|
|
56
|
+
console.log(`[jsql-neo] binary built: ${bp}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.warn('[jsql-neo] cargo build failed:', e.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback: download from GitHub releases
|
|
65
|
+
const version = pkg.version;
|
|
66
|
+
const url = `https://github.com/vexify/jsql-neo/releases/download/v${version}/${BIN_NAME}-${PLATFORM}`;
|
|
67
|
+
console.log(`[jsql-neo] downloading binary from ${url}...`);
|
|
68
|
+
try {
|
|
69
|
+
await download(url, bp);
|
|
70
|
+
console.log(`[jsql-neo] binary downloaded: ${bp}`);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.warn(`[jsql-neo] download failed: ${e.message}`);
|
|
73
|
+
console.warn('[jsql-neo] falling back to JS-only mode');
|
|
74
|
+
// Create a stub that errors with a helpful message
|
|
75
|
+
fs.writeFileSync(bp, '#!/bin/sh\necho "jsql-neo-server binary not installed"\nexit 1\n');
|
|
76
|
+
fs.chmodSync(bp, 0o755);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
main().catch(e => {
|
|
81
|
+
console.error('[jsql-neo] postinstall failed:', e.message);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|