jsql-neo 4.0.0 → 4.0.2

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
@@ -1,78 +1,101 @@
1
- # JSQL-NEO v3.4.3
1
+ # JSQL-NEO v4.0.1
2
2
 
3
- Rust-powered embedded database with **three engines** in one npm package.
3
+ Rust-powered embedded database with **three engines + SQL + Redis-style storage** in one npm package.
4
4
 
5
5
  ## Engines
6
6
 
7
- | Engine | Entry | Use Case | Speed |
8
- |--------|-------|----------|-------|
9
- | **WASM** (Rust → wasm-pack) | `JSQL` | Zero native deps, Node.js | 1.4M rows/sec insert |
10
- | **HTTP** (Rust actix-web) | `HttpJSQL` | Multi-process / remote | 340K rows/sec insert |
11
- | **Pure JS** (Database class) | `Database` | Local JSON file, SQLite-like | 617K rows/sec insert |
7
+ | Engine | Entry | Use Case |
8
+ |--------|-------|----------|
9
+ | **Native** (Rust → N-API, fastest) | `NativeJSQL` | Node.js, best performance |
10
+ | **WASM** (Rust → wasm-pack, zero native deps) | `JSQL` | Node.js / browsers, no native addon |
11
+ | **Pure JS** (local JSON file, SQLite-like) | `Database` | Local file persistence, legacy API |
12
12
 
13
- ## Quick Start
13
+ All engines share the same API (`createTable` / `insert` / `findById` / `find` / `updateById` / `removeById` / `dropTable`) plus `executeSQL()`.
14
14
 
15
- ### WASM (no server, zero deps)
15
+ ## SQL
16
16
 
17
17
  ```js
18
- const { JSQL } = require('jsql-neo');
18
+ const jsql = require('jsql-neo');
19
+ const db = new jsql.NativeJSQL();
20
+ await db.start();
21
+
22
+ await jsql.executeSQL(db, 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name STRING, age INTEGER)');
23
+ await jsql.executeSQL(db, "INSERT INTO users (name, age) VALUES ('Alice', 30), ('Bob', 25)");
24
+ await jsql.executeSQL(db, "INSERT INTO users VALUES (5, 'Carol', 35) ON DUPLICATE KEY UPDATE age = 30");
25
+ const r = await jsql.executeSQL(db, 'SELECT name, age FROM users WHERE age > 26 ORDER BY age DESC');
26
+ // → rows: [["Carol",35],["Alice",30]]
27
+
28
+ await jsql.executeSQL(db, 'UPDATE users SET age = 31 WHERE id = 1');
29
+ await jsql.executeSQL(db, 'DELETE FROM users WHERE id = 2');
30
+ await db.stop();
31
+ ```
19
32
 
20
- const db = new JSQL();
33
+ Supported: `CREATE/DROP TABLE`, `INSERT` (multi-row, `ON DUPLICATE KEY UPDATE`), `SELECT` (`WHERE`/`ORDER BY`/`LIMIT`/`OFFSET`/`GROUP BY`/`HAVING`/aggregates), `UPDATE`, `DELETE`, prepared statements with `?` placeholders.
34
+
35
+ ## Redis-Style Storage Modes
36
+
37
+ Native and Pure JS engines support three persistence modes (Redis-compatible model: memory-first, async flush, LRU eviction, lazy reload):
38
+
39
+ ```js
40
+ const db = new jsql.NativeJSQL({
41
+ path: '/var/lib/jsql', // storage directory (required for hybrid/disk)
42
+ mode: 'hybrid', // 'memory' (default) | 'hybrid' | 'disk'
43
+ memReserveMB: 512, // keep 512MB RAM headroom before evicting
44
+ flushInterval: 200, // ms between async flushes (hybrid 200 / disk 50)
45
+ evictInterval: 1000, // ms between memory-pressure checks
46
+ });
47
+ await db.start();
48
+ ```
49
+
50
+ - **`memory`** — pure in-memory (default, no path needed)
51
+ - **`hybrid`** — writes go to memory first, async incremental flush to disk; cold tables are LRU-evicted when memory pressure exceeds `total - memReserveMB`, and lazily reloaded on next access
52
+ - **`disk`** — fast flush (50ms), memory acts as read/write cache
53
+
54
+ Data is stored per-table as `<dir>/<table>.jsql.json` + `meta.json`; writes are atomic (tmp + rename).
55
+
56
+ ```js
57
+ // Pure JS engine (same options)
58
+ const db2 = new jsql.Database({ path: '/var/lib/jsql', mode: 'hybrid' });
59
+ ```
60
+
61
+ ## Quick Start
62
+
63
+ ### Native (fastest)
64
+
65
+ ```js
66
+ const { NativeJSQL } = require('jsql-neo');
67
+ const db = new NativeJSQL();
21
68
  await db.start();
22
69
 
23
70
  await db.createTable('users', {
24
71
  name: { type: 'string' },
25
72
  age: { type: 'integer' }
26
73
  });
27
-
28
74
  const [id] = await db.insert('users', { name: 'Alice', age: 30 });
29
75
  const user = await db.findById('users', id);
30
76
  // → { id: 1, fields: { name: 'Alice', age: 30 }, created_at: '...', updated_at: '...' }
31
-
32
77
  await db.stop();
33
78
  ```
34
79
 
35
- ### Pure JS (local file, SQLite-like)
80
+ ### Pure JS (local file)
36
81
 
37
82
  ```js
38
83
  const { Database } = require('jsql-neo');
39
-
40
- const db = new Database('/tmp/mydb.json');
84
+ const db = new Database('/tmp/mydb.json'); // or { path, mode } for hybrid/disk
41
85
  const users = db.createTable('users', {
42
86
  id: { type: 'integer', autoIncrement: true, primaryKey: true },
43
87
  name: { type: 'string', length: 32 },
44
88
  age: { type: 'integer' }
45
89
  });
46
-
47
90
  const ids = users.insertMany([{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]);
48
91
  users.updateById(ids[0], { age: 31 });
49
- const user = users.findById(ids[0]);
50
92
  db.save();
51
93
  ```
52
94
 
53
- ### HTTP Server
54
-
55
- ```bash
56
- JSQL_DATA_DIR=/tmp/jsql-neo npx jsql-neo
57
- ```
58
-
59
- ```js
60
- const { HttpJSQL } = require('jsql-neo');
61
-
62
- const db = new HttpJSQL({ host: '127.0.0.1', port: 6379 });
63
- await db.start();
64
-
65
- await db.createTable('users', { name: { type: 'string' }, age: { type: 'integer' } });
66
- const [id] = await db.insert('users', { name: 'Alice', age: 30 });
67
- const user = await db.findById('users', id);
68
-
69
- await db.stop();
70
- ```
71
-
72
95
  ## API
73
96
 
74
- | Method | WASM | HTTP | Pure JS | Description |
75
- |--------|------|------|---------|-------------|
97
+ | Method | Native | WASM | Pure JS | Description |
98
+ |--------|--------|------|---------|-------------|
76
99
  | `createTable(name, schema)` | ✅ | ✅ | ✅ | Define table with typed fields |
77
100
  | `insert(table, data)` | ✅ | ✅ | ✅ | Insert row(s), returns IDs |
78
101
  | `findById(table, id)` | ✅ | ✅ | ✅ | O(1) PK lookup |
@@ -81,19 +104,7 @@ await db.stop();
81
104
  | `updateById(table, id, data)` | ✅ | ✅ | ✅ | O(1) PK update |
82
105
  | `removeById(table, id)` | ✅ | ✅ | ✅ | O(1) PK delete |
83
106
  | `dropTable(name)` | ✅ | ✅ | ✅ | Remove table |
84
-
85
- All engines share the same async API for CRUD operations.
86
-
87
- ## Performance (Pure JS, 100K rows)
88
-
89
- | Operation | Time | Rate |
90
- |-----------|------|------|
91
- | Insert 100K | 128 ms | 781K rows/sec |
92
- | findById × 10,000 | 3 ms | 0.3 μs each |
93
- | updateById × 10,000 | 82 ms | 8.2 μs each (O(1) hash index) |
94
- | count × 100 | 0 ms | — |
95
- | findAll × 5 | 16 ms | — |
96
- | Filtered query × 100 | 407 ms | — |
107
+ | `executeSQL(db, sql, params?)` | ✅ | ✅ | ✅ | Run SQL statements |
97
108
 
98
109
  ## Schema Field Options
99
110
 
@@ -110,11 +121,12 @@ All engines share the same async API for CRUD operations.
110
121
 
111
122
  ## Features
112
123
 
113
- - Three engines: WASM (native Rust), HTTP (actix-web), Pure JS (local JSON)
124
+ - Three engines: Native (N-API Rust), WASM (wasm-pack Rust), Pure JS (local JSON)
125
+ - SQL engine with prepared statements
126
+ - Redis-style hybrid/disk storage: memory-first + async flush + LRU eviction + lazy reload
114
127
  - O(1) primary key hash index (`FxHashMap` / `Map`)
115
128
  - B-Tree indexing for range queries
116
- - WAL + snapshot crash recovery (HTTP engine)
117
- - Strings Pool for memory-efficient storage
129
+ - WAL + snapshot crash recovery (server engine)
118
130
  - Batch insert / update / delete
119
131
  - Cursor-based pagination
120
- - Transaction support (HTTP engine)
132
+ - Transaction support (server engine)
@@ -11,9 +11,16 @@ function encodeBatch(rows) {
11
11
  if (rows.length === 0) return new Uint8Array(0);
12
12
  const fieldNames = Object.keys(rows[0]);
13
13
  const nFields = fieldNames.length;
14
- const est = 100 + rows.length * 120;
15
- const buf = Buffer.allocUnsafe(est);
14
+ const est = 100 + rows.length * 160;
15
+ let buf = Buffer.allocUnsafe(est);
16
16
  let off = 0;
17
+ const ensure = (n) => {
18
+ if (off + n > buf.length) {
19
+ const nb = Buffer.allocUnsafe(Math.max(buf.length * 2, off + n));
20
+ buf.copy(nb, 0, 0, off);
21
+ buf = nb;
22
+ }
23
+ };
17
24
 
18
25
  off = buf.writeUInt8(nFields, off);
19
26
  for (let fi = 0; fi < nFields; fi++) {
@@ -34,8 +41,9 @@ function encodeBatch(rows) {
34
41
  if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
35
42
  else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
36
43
  } else if (typeof v === 'string') {
37
- off = buf.writeUInt8(STR_TAG, off);
38
44
  const sl = Buffer.byteLength(v, 'utf8');
45
+ ensure(5 + sl);
46
+ off = buf.writeUInt8(STR_TAG, off);
39
47
  off = buf.writeUInt32LE(sl, off);
40
48
  off += buf.write(v, off, sl, 'utf8');
41
49
  } else { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
@@ -47,8 +55,9 @@ function encodeBatch(rows) {
47
55
  if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
48
56
  else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
49
57
  } else if (typeof v === 'string') {
50
- off = buf.writeUInt8(STR_TAG, off);
51
58
  const sl = Buffer.byteLength(v, 'utf8');
59
+ ensure(5 + sl);
60
+ off = buf.writeUInt8(STR_TAG, off);
52
61
  off = buf.writeUInt32LE(sl, off);
53
62
  off += buf.write(v, off, sl, 'utf8');
54
63
  } else { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
@@ -60,8 +69,9 @@ function encodeBatch(rows) {
60
69
  if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
61
70
  else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
62
71
  } else if (typeof v === 'string') {
63
- off = buf.writeUInt8(STR_TAG, off);
64
72
  const sl = Buffer.byteLength(v, 'utf8');
73
+ ensure(5 + sl);
74
+ off = buf.writeUInt8(STR_TAG, off);
65
75
  off = buf.writeUInt32LE(sl, off);
66
76
  off += buf.write(v, off, sl, 'utf8');
67
77
  } else { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
@@ -73,8 +83,9 @@ function encodeBatch(rows) {
73
83
  if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
74
84
  else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
75
85
  } else if (typeof v === 'string') {
76
- off = buf.writeUInt8(STR_TAG, off);
77
86
  const sl = Buffer.byteLength(v, 'utf8');
87
+ ensure(5 + sl);
88
+ off = buf.writeUInt8(STR_TAG, off);
78
89
  off = buf.writeUInt32LE(sl, off);
79
90
  off += buf.write(v, off, sl, 'utf8');
80
91
  } else { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
@@ -86,8 +97,9 @@ function encodeBatch(rows) {
86
97
  if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
87
98
  else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
88
99
  } else if (typeof v === 'string') {
89
- off = buf.writeUInt8(STR_TAG, off);
90
100
  const sl = Buffer.byteLength(v, 'utf8');
101
+ ensure(5 + sl);
102
+ off = buf.writeUInt8(STR_TAG, off);
91
103
  off = buf.writeUInt32LE(sl, off);
92
104
  off += buf.write(v, off, sl, 'utf8');
93
105
  } else { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
@@ -99,8 +111,9 @@ function encodeBatch(rows) {
99
111
  if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
100
112
  else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
101
113
  } else if (typeof v === 'string') {
102
- off = buf.writeUInt8(STR_TAG, off);
103
114
  const sl = Buffer.byteLength(v, 'utf8');
115
+ ensure(5 + sl);
116
+ off = buf.writeUInt8(STR_TAG, off);
104
117
  off = buf.writeUInt32LE(sl, off);
105
118
  off += buf.write(v, off, sl, 'utf8');
106
119
  } else { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
@@ -328,16 +341,11 @@ class JSQL {
328
341
  if (!this._runHooks('beforeInsert', [table, filtered])) return [];
329
342
  if (arr.length > 1) {
330
343
  await this._flush();
331
- let result;
332
- for (let i = 0; i < arr.length; i += this._flushThreshold) {
333
- const chunk = arr.slice(i, i + this._flushThreshold);
334
- const r = await this._insertBatch(table, chunk);
335
- if (r && r.error) throw new Error(r.error);
336
- if (!result) result = r;
337
- }
338
- this._emit('insert', { table, count: arr.length, ids: result });
339
- this._runHooks('afterInsert', [table, filtered, result]);
340
- return result;
344
+ const r = await this._insertBatch(table, arr);
345
+ if (r && r.error) throw new Error(r.error);
346
+ this._emit('insert', { table, count: arr.length, ids: r });
347
+ this._runHooks('afterInsert', [table, filtered, r]);
348
+ return r;
341
349
  }
342
350
  if (!this._buffer[table]) this._buffer[table] = [];
343
351
  this._buffer[table].push(arr[0]);
package/lib/sql.js CHANGED
@@ -678,7 +678,7 @@ function extractEqualPushdown(expr, schema) {
678
678
  if (node.type === 'and') { walk(node.left); walk(node.right); return; }
679
679
  if (node.type === 'compare' && node.op === '=') {
680
680
  const col = node.left && node.left.type === 'column' ? node.left.name : null;
681
- const val = node.right && node.right.type === 'literal' ? node.right.value : undefined;
681
+ const val = node.right && (node.right.type === 'literal' || node.right.type === 'value' || node.right.type === 'param') ? node.right.value : undefined;
682
682
  if (col && val !== undefined && val !== null && schema[col] && !(schema[col].primaryKey && schema[col].autoIncrement === false)) {
683
683
  filter[col] = val;
684
684
  return;
@@ -728,9 +728,10 @@ function normalizeRow(row, schema) {
728
728
  if (schema) {
729
729
  const pkCols = Object.keys(schema).filter(k => schema[k].primaryKey);
730
730
  for (const c of pkCols) {
731
- if ((flat[c] === undefined || flat[c] === null) && row.id !== undefined) flat[c] = row.id;
731
+ const v = flat[c];
732
+ if (row.id !== undefined && (v === undefined || v === null || v === 0 || v === '')) flat[c] = row.id;
732
733
  }
733
- } else if (row.id !== undefined && flat.id === undefined) {
734
+ } else if (row.id !== undefined && (flat.id === undefined || flat.id === null || flat.id === 0 || flat.id === '')) {
734
735
  flat.id = row.id;
735
736
  }
736
737
  flat._rid = row.id;
@@ -858,6 +859,11 @@ class SQLExecutor {
858
859
  if (this.engine.hasTable && !this.engine.hasTable(statement.table)) {
859
860
  throw new Error(`Table '${statement.table}' does not exist`);
860
861
  }
862
+ if (statement.aggregate && statement.aggregate.type === 'COUNT' && !statement.where && !statement.distinct) {
863
+ const aggName = statement.aggregate.alias || 'COUNT(*)';
864
+ const n = this.engine.count ? await this.engine.count(statement.table) : 0;
865
+ return { ok: true, type: 'select', table: statement.table, columns: [aggName], rows: [[n]], aggregate: statement.aggregate };
866
+ }
861
867
  schema = this.engine.getTableSchema
862
868
  ? await this.engine.getTableSchema(statement.table)
863
869
  : (this.engine._schemas ? this.engine._schemas[statement.table] : null);
@@ -905,7 +911,13 @@ class SQLExecutor {
905
911
  const rowsOnly = rows;
906
912
  const aggName = agg.alias || (agg.type === 'COUNT' ? 'COUNT(*)' : agg.type + '(' + agg.column + ')');
907
913
  if (agg.type === 'COUNT') {
908
- return { ok: true, type: 'select', table: statement.table, columns: [aggName], rows: [[rowsOnly.length]], aggregate: agg };
914
+ let n;
915
+ if (!statement.where && !statement.distinct) {
916
+ n = this.engine.count ? await this.engine.count(statement.table) : rowsOnly.length;
917
+ } else {
918
+ n = rowsOnly.length;
919
+ }
920
+ return { ok: true, type: 'select', table: statement.table, columns: [aggName], rows: [[n]], aggregate: agg };
909
921
  }
910
922
  const values = rowsOnly.map(r => r[agg.column]).filter(v => v !== null && v !== undefined);
911
923
  let value;
Binary file
@@ -235,7 +235,7 @@ impl Table {
235
235
 
236
236
  if let Some(ref pk_key) = pk_auto_key {
237
237
  match fields.get(pk_key) {
238
- Some(v) if v.is_null() => {
238
+ Some(v) if v.is_null() || v.as_i64() == Some(0) => {
239
239
  fields.insert(pk_key.clone(), serde_json::Value::Number(id.into()));
240
240
  }
241
241
  Some(v) => {
@@ -307,7 +307,7 @@ impl Table {
307
307
  if let Some(pi) = pk_idx {
308
308
  if pi < row_vals.len() {
309
309
  match row_vals[pi] {
310
- FieldValue::Null => row_vals[pi] = FieldValue::Int(id as i64),
310
+ FieldValue::Null | FieldValue::Int(0) => row_vals[pi] = FieldValue::Int(id as i64),
311
311
  FieldValue::Int(e) if e > 0 => {
312
312
  self.next_id = self.next_id.max((e + 1) as u64);
313
313
  }
@@ -344,7 +344,13 @@ impl Table {
344
344
  for (i, remapped_si) in remap.iter().enumerate() {
345
345
  if let Some(si) = *remapped_si {
346
346
  if si < row_vals.len() {
347
- self.values.push(std::mem::take(&mut row_vals[si]));
347
+ let v = std::mem::take(&mut row_vals[si]);
348
+ let is_auto_pk = pk_auto_key.is_some() && self.field_order[i] == *pk_auto_key.as_ref().unwrap();
349
+ if is_auto_pk && matches!(v, FieldValue::Null | FieldValue::Int(0)) {
350
+ self.values.push(FieldValue::Int(id as i64));
351
+ } else {
352
+ self.values.push(v);
353
+ }
348
354
  continue;
349
355
  }
350
356
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "bin": {