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 CHANGED
File without changes
package/index.js CHANGED
@@ -1,19 +1,24 @@
1
- // © Vexify 2026 All Rights Reserved.
2
1
  /**
3
- * JSQL-NEO — Pure JavaScript Embedded Database
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.Database('mydb.json', { wal: true, fileLock: true });
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": "2.2.0",
4
- "description": "JSQL-NEO — Pure JavaScript embedded database with B-Tree indexes, WAL, hash JOIN, Redis-compatible Cache, and MySQL-style error codes",
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
- "sql-like",
21
- "in-memory",
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
+ });