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 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 — 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'),
@@ -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 };
File without changes
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
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": "2.2.1",
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.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
- "sql-like",
21
- "in-memory",
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();
@@ -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 };