@stoolap/node 0.3.5 → 0.4.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
@@ -108,6 +108,7 @@ Sync methods run on the main thread. Faster for simple operations but block the
108
108
  | Method | Returns | Description |
109
109
  |--------|---------|-------------|
110
110
  | `Database.openSync(path)` | `Database` | Open a database |
111
+ | `clone()` | `Database` | Clone handle (shared engine, own state) |
111
112
  | `executeSync(sql, params?)` | `RunResult` | Execute DML statement |
112
113
  | `execSync(sql)` | `void` | Execute a DDL statement |
113
114
  | `querySync(sql, params?)` | `Object[]` | Query rows as objects |
@@ -126,7 +127,7 @@ import { Database, RunResult } from '@stoolap/node';
126
127
 
127
128
  #### Persistence
128
129
 
129
- File-based databases persist data to disk using WAL (Write-Ahead Logging) and periodic snapshots. Data survives process restarts.
130
+ File-based databases persist data to disk using WAL (Write-Ahead Logging) and an immutable volume-based storage engine. Hot data lives in memory, cold data is sealed into columnar `.vol` files with zone maps, bloom filters, and LZ4 compression. Data survives process restarts.
130
131
 
131
132
  ```js
132
133
  const db = await Database.open('./mydata');
@@ -147,18 +148,18 @@ await db2.close();
147
148
  Pass configuration as query parameters in the path:
148
149
 
149
150
  ```js
150
- // Maximum durability: fsync on every WAL write
151
- const db = await Database.open('./mydata?sync=full');
151
+ // Maximum durability: fsync on every write
152
+ const db = await Database.open('./mydata?sync_mode=full');
152
153
 
153
- // High throughput: no fsync, larger buffers
154
- const db = await Database.open('./mydata?sync=none&wal_buffer_size=131072');
154
+ // High throughput: no fsync, data durable at checkpoint
155
+ const db = await Database.open('./mydata?sync_mode=none');
155
156
 
156
- // Custom snapshot interval with compression
157
- const db = await Database.open('./mydata?snapshot_interval=60&compression=on');
157
+ // Custom checkpoint interval with compression
158
+ const db = await Database.open('./mydata?checkpoint_interval=60&compression=on');
158
159
 
159
160
  // Multiple options
160
161
  const db = await Database.open(
161
- './mydata?sync=full&snapshot_interval=120&keep_snapshots=10&wal_max_size=134217728'
162
+ './mydata?sync_mode=normal&checkpoint_interval=120&compact_threshold=4'
162
163
  );
163
164
  ```
164
165
 
@@ -168,26 +169,40 @@ Controls the durability vs. performance trade-off:
168
169
 
169
170
  | Mode | Value | Description |
170
171
  |------|-------|-------------|
171
- | `none` | `sync=none` | No fsync. Fastest, data may be lost on crash |
172
- | `normal` | `sync=normal` | Fsync on commit batches. Good balance (default) |
173
- | `full` | `sync=full` | Fsync on every WAL write. Slowest, maximum durability |
172
+ | `none` | `sync_mode=none` | No fsync. Data durable only after checkpoint |
173
+ | `normal` | `sync_mode=normal` | Fsync every 1 second (batched). DDL fsyncs immediately (default) |
174
+ | `full` | `sync_mode=full` | Fsync on every write. Maximum durability |
174
175
 
175
176
  ##### All Configuration Parameters
176
177
 
177
178
  | Parameter | Default | Description |
178
179
  |-----------|---------|-------------|
179
- | `sync` | `normal` | Sync mode: `none`, `normal`, or `full` |
180
- | `snapshot_interval` | `300` | Seconds between automatic snapshots (5 min) |
181
- | `keep_snapshots` | `5` | Number of snapshot files to retain |
182
- | `wal_flush_trigger` | `32768` | WAL flush trigger size in bytes (32 KB) |
183
- | `wal_buffer_size` | `65536` | WAL buffer size in bytes (64 KB) |
184
- | `wal_max_size` | `67108864` | Max WAL file size before rotation (64 MB) |
185
- | `commit_batch_size` | `100` | Commits to batch before syncing (normal mode) |
186
- | `sync_interval_ms` | `10` | Minimum ms between syncs (normal mode) |
180
+ | `sync_mode` | `normal` | Sync mode: `none`, `normal`, or `full` |
181
+ | `checkpoint_interval` | `60` | Seconds between checkpoint cycles (seal + compact + WAL truncate) |
182
+ | `compact_threshold` | `4` | Number of cold volumes before compaction merges them |
183
+ | `checkpoint_on_close` | `on` | Seal all hot rows on clean shutdown for fast startup |
187
184
  | `wal_compression` | `on` | LZ4 compression for WAL entries |
188
- | `snapshot_compression` | `on` | LZ4 compression for snapshots |
189
- | `compression` | | Set both `wal_compression` and `snapshot_compression` |
190
- | `compression_threshold` | `64` | Minimum bytes before compressing an entry |
185
+ | `volume_compression` | `on` | LZ4 compression for cold volume files |
186
+ | `compression` | `on` | Shorthand: set both `wal_compression` and `volume_compression` |
187
+ | `keep_snapshots` | `5` | Number of backup snapshot files to retain |
188
+
189
+ #### Cloning
190
+
191
+ `clone()` creates a new `Database` handle that shares the same underlying engine (data, indexes, transactions) but has its own executor and error state. Useful for concurrent access patterns such as worker threads.
192
+
193
+ ```js
194
+ const db = await Database.open('./mydata');
195
+ const db2 = db.clone();
196
+
197
+ // Both see the same data
198
+ await db.execute('INSERT INTO users VALUES ($1, $2)', [1, 'Alice']);
199
+ const row = db2.queryOneSync('SELECT * FROM users WHERE id = $1', [1]);
200
+ // { id: 1, name: 'Alice' }
201
+
202
+ // Each clone must be closed independently
203
+ await db2.close();
204
+ await db.close();
205
+ ```
191
206
 
192
207
  #### Raw Query Format
193
208
 
package/index.d.ts CHANGED
@@ -17,6 +17,12 @@ export declare class Database {
17
17
  * Open a database synchronously.
18
18
  */
19
19
  static openSync(path: string): Database;
20
+ /**
21
+ * Clone this database handle. The clone shares the same underlying engine
22
+ * (data, indexes, transactions) but has its own executor and error state.
23
+ * Each clone must be closed independently.
24
+ */
25
+ clone(): Database;
20
26
  /**
21
27
  * Execute a DDL/DML statement. Returns Promise<{ changes: number }>.
22
28
  *
package/lib/database.js CHANGED
@@ -128,6 +128,14 @@ class Database {
128
128
  return this.#stoolap.dbExecBatchBuf(this.#ptr, sql, batchBuf);
129
129
  }
130
130
 
131
+ // -- Clone --
132
+
133
+ clone() {
134
+ this.#checkOpen();
135
+ const clonedPtr = this.#stoolap.dbClone(this.#ptr);
136
+ return new Database(clonedPtr, this.#stoolap);
137
+ }
138
+
131
139
  // -- Execute (DML) --
132
140
 
133
141
  async execute(sql, params) {
@@ -179,7 +179,12 @@ class Transaction {
179
179
  executeBatchSync(sql, paramsArray) {
180
180
  this.#checkSyncReady();
181
181
  if (paramsArray.length > 0 && Array.isArray(paramsArray[0])) {
182
- return this.#stoolap.txExecBatch(this.#dbPtr, this.#ptr, sql, paramsArray);
182
+ return this.#stoolap.txExecBatch(
183
+ this.#dbPtr,
184
+ this.#ptr,
185
+ sql,
186
+ paramsArray,
187
+ );
183
188
  }
184
189
  let totalChanges = 0;
185
190
  for (const params of paramsArray) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stoolap/node",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "High-performance Node.js driver for Stoolap embedded SQL database",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -18,11 +18,11 @@
18
18
  "bench": "node benchmark.mjs"
19
19
  },
20
20
  "optionalDependencies": {
21
- "@stoolap/lib-darwin-arm64": "0.3.5",
22
- "@stoolap/lib-darwin-x64": "0.3.5",
23
- "@stoolap/lib-linux-arm64-gnu": "0.3.5",
24
- "@stoolap/lib-linux-x64-gnu": "0.3.5",
25
- "@stoolap/lib-win32-x64-msvc": "0.3.5"
21
+ "@stoolap/lib-darwin-arm64": "0.4.0",
22
+ "@stoolap/lib-darwin-x64": "0.4.0",
23
+ "@stoolap/lib-linux-arm64-gnu": "0.4.0",
24
+ "@stoolap/lib-linux-x64-gnu": "0.4.0",
25
+ "@stoolap/lib-win32-x64-msvc": "0.4.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "better-sqlite3": "^12.6.2",
package/src/stoolap.c CHANGED
@@ -12,6 +12,7 @@
12
12
  #include <stdlib.h>
13
13
  #include <string.h>
14
14
  #include <stdint.h>
15
+ #include <stdio.h>
15
16
 
16
17
  #ifdef _WIN32
17
18
  #include <windows.h>
@@ -23,6 +24,7 @@
23
24
  typedef HMODULE LibHandle;
24
25
  #else
25
26
  #include <dlfcn.h>
27
+ #include <alloca.h>
26
28
  #define DLOPEN(path) dlopen(path, RTLD_NOW | RTLD_LOCAL)
27
29
  #define DLSYM(lib, name) dlsym(lib, name)
28
30
  #define DLERROR() dlerror()
@@ -68,6 +70,7 @@ typedef struct {
68
70
  typedef const char* (*fn_version)(void);
69
71
  typedef int32_t (*fn_open)(const char*, StoolapDB**);
70
72
  typedef int32_t (*fn_open_mem)(StoolapDB**);
73
+ typedef int32_t (*fn_clone)(StoolapDB*, StoolapDB**);
71
74
  typedef int32_t (*fn_close)(StoolapDB*);
72
75
  typedef const char* (*fn_errmsg)(const StoolapDB*);
73
76
  typedef int32_t (*fn_exec)(StoolapDB*, const char*, int64_t*);
@@ -112,6 +115,7 @@ static struct {
112
115
  fn_version version;
113
116
  fn_open open;
114
117
  fn_open_mem open_mem;
118
+ fn_clone clone;
115
119
  fn_close close;
116
120
  fn_errmsg errmsg;
117
121
  fn_exec exec;
@@ -156,6 +160,8 @@ static LibHandle lib_handle = NULL;
156
160
  /* ---- Helpers ---- */
157
161
 
158
162
  #define THROW(env, msg) do { napi_throw_error(env, NULL, msg); return NULL; } while(0)
163
+ #define ERR_SAVE(msg, fallback) \
164
+ char _eb[512]; snprintf(_eb, sizeof(_eb), "%s", (msg) ? (msg) : (fallback))
159
165
  #define CHECK(env, status) if ((status) != napi_ok) return NULL
160
166
 
161
167
  static napi_value make_changes(napi_env env, int64_t n) {
@@ -1510,6 +1516,7 @@ static napi_value fn_load_library(napi_env env, napi_callback_info info) {
1510
1516
  LOAD(version, "stoolap_version");
1511
1517
  LOAD(open, "stoolap_open");
1512
1518
  LOAD(open_mem, "stoolap_open_in_memory");
1519
+ LOAD(clone, "stoolap_clone");
1513
1520
  LOAD(close, "stoolap_close");
1514
1521
  LOAD(errmsg, "stoolap_errmsg");
1515
1522
  LOAD(exec, "stoolap_exec");
@@ -1586,6 +1593,27 @@ static napi_value fn_db_open(napi_env env, napi_callback_info info) {
1586
1593
  return ext;
1587
1594
  }
1588
1595
 
1596
+ /* dbClone(external): external */
1597
+ static napi_value fn_db_clone(napi_env env, napi_callback_info info) {
1598
+ size_t argc = 1;
1599
+ napi_value argv[1];
1600
+ napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
1601
+
1602
+ StoolapDB* db;
1603
+ napi_get_value_external(env, argv[0], (void**)&db);
1604
+
1605
+ StoolapDB* cloned = NULL;
1606
+ int32_t rc = S.clone(db, &cloned);
1607
+ if (rc != STOOLAP_OK) {
1608
+ const char* msg = S.errmsg(db);
1609
+ THROW(env, msg ? msg : "Failed to clone database");
1610
+ }
1611
+
1612
+ napi_value ext;
1613
+ napi_create_external(env, cloned, NULL, NULL, &ext);
1614
+ return ext;
1615
+ }
1616
+
1589
1617
  /* dbClose(external): void */
1590
1618
  static napi_value fn_db_close(napi_env env, napi_callback_info info) {
1591
1619
  size_t argc = 1;
@@ -3023,10 +3051,10 @@ static napi_value wrap_tx_exec_batch(napi_env env, napi_callback_info info) {
3023
3051
  for (int k = 0; k < tbuf_cnt; k++) { free(tbufs[k]); tbufs[k] = NULL; }
3024
3052
 
3025
3053
  if (rc != STOOLAP_OK) {
3054
+ ERR_SAVE(S.tx_errmsg(tx), "Transaction batch exec error");
3026
3055
  free(vals); free(tbufs);
3027
- const char* msg = S.tx_errmsg(tx);
3028
3056
  if (stmt) S.stmt_finalize(stmt);
3029
- THROW(env, msg ? msg : "Transaction batch exec error");
3057
+ THROW(env, _eb);
3030
3058
  }
3031
3059
 
3032
3060
  total_affected += affected;
@@ -3202,7 +3230,7 @@ static napi_value wrap_db_exec_batch_buf(napi_env env, napi_callback_info info)
3202
3230
  int use_stmt = (S.tx_stmt_exec != NULL);
3203
3231
  if (use_stmt) {
3204
3232
  rc = S.prepare(db, sql, &stmt);
3205
- if (rc != STOOLAP_OK) { S.tx_rollback(tx); const char* msg = S.errmsg(db); THROW(env, msg ? msg : "Prepare error"); }
3233
+ if (rc != STOOLAP_OK) { ERR_SAVE(S.errmsg(db), "Prepare error"); S.tx_rollback(tx); THROW(env, _eb); }
3206
3234
  }
3207
3235
  int64_t total = 0;
3208
3236
  for (uint32_t i = 0; i < row_count; i++) {
@@ -3217,12 +3245,12 @@ static napi_value wrap_db_exec_batch_buf(napi_env env, napi_callback_info info)
3217
3245
  } else {
3218
3246
  rc = S.tx_exec_p(tx, sql, vals, decoded, &affected);
3219
3247
  }
3220
- if (rc != STOOLAP_OK) { const char* msg = S.tx_errmsg(tx); if (stmt) S.stmt_finalize(stmt); S.tx_rollback(tx); THROW(env, msg ? msg : "Batch exec error"); }
3248
+ if (rc != STOOLAP_OK) { ERR_SAVE(S.tx_errmsg(tx), "Batch exec error"); if (stmt) S.stmt_finalize(stmt); S.tx_rollback(tx); THROW(env, _eb); }
3221
3249
  total += affected;
3222
3250
  }
3223
3251
  if (stmt) S.stmt_finalize(stmt);
3224
3252
  rc = S.tx_commit(tx);
3225
- if (rc != STOOLAP_OK) { const char* msg = S.errmsg(db); THROW(env, msg ? msg : "Commit error"); }
3253
+ if (rc != STOOLAP_OK) { THROW(env, "Commit error"); }
3226
3254
  return make_changes(env, total);
3227
3255
  }
3228
3256
 
@@ -3278,6 +3306,7 @@ static napi_value Init(napi_env env, napi_value exports) {
3278
3306
  EXPORT_FN(env, exports, "loadLibrary", fn_load_library);
3279
3307
  EXPORT_FN(env, exports, "dbOpen", fn_db_open);
3280
3308
  EXPORT_FN(env, exports, "dbOpenAsync", fn_db_open_async);
3309
+ EXPORT_FN(env, exports, "dbClone", fn_db_clone);
3281
3310
  EXPORT_FN(env, exports, "dbClose", fn_db_close);
3282
3311
  EXPORT_FN(env, exports, "dbCloseAsync", fn_db_close_async);
3283
3312
  EXPORT_FN(env, exports, "dbExec", fn_db_exec);