@stoolap/node 0.3.4 → 0.3.7

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,11 +1,13 @@
1
1
  # @stoolap/node
2
2
 
3
- High-performance Node.js driver for [Stoolap](https://github.com/stoolap/stoolap), a modern embedded SQL database with MVCC, time-travel queries, and full ACID compliance.
3
+ High-performance JavaScript driver for [Stoolap](https://github.com/stoolap/stoolap), a modern embedded SQL database with MVCC, time-travel queries, and full ACID compliance.
4
4
 
5
- Built with a native N-API C addon for minimal overhead. Provides both async and sync APIs.
5
+ Built with a native N-API C addon for minimal overhead. Works with Node.js, Bun, and Deno. Provides both async and sync APIs.
6
6
 
7
7
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
8
8
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
9
+ [![Bun](https://img.shields.io/badge/bun-compatible-f472b6.svg)](https://bun.sh)
10
+ [![Deno](https://img.shields.io/badge/deno-compatible-000000.svg)](https://deno.com)
9
11
 
10
12
  ## Installation
11
13
 
@@ -106,6 +108,7 @@ Sync methods run on the main thread. Faster for simple operations but block the
106
108
  | Method | Returns | Description |
107
109
  |--------|---------|-------------|
108
110
  | `Database.openSync(path)` | `Database` | Open a database |
111
+ | `clone()` | `Database` | Clone handle (shared engine, own state) |
109
112
  | `executeSync(sql, params?)` | `RunResult` | Execute DML statement |
110
113
  | `execSync(sql)` | `void` | Execute a DDL statement |
111
114
  | `querySync(sql, params?)` | `Object[]` | Query rows as objects |
@@ -187,6 +190,24 @@ Controls the durability vs. performance trade-off:
187
190
  | `compression` | | Set both `wal_compression` and `snapshot_compression` |
188
191
  | `compression_threshold` | `64` | Minimum bytes before compressing an entry |
189
192
 
193
+ #### Cloning
194
+
195
+ `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.
196
+
197
+ ```js
198
+ const db = await Database.open('./mydata');
199
+ const db2 = db.clone();
200
+
201
+ // Both see the same data
202
+ await db.execute('INSERT INTO users VALUES ($1, $2)', [1, 'Alice']);
203
+ const row = db2.queryOneSync('SELECT * FROM users WHERE id = $1', [1]);
204
+ // { id: 1, name: 'Alice' }
205
+
206
+ // Each clone must be closed independently
207
+ await db2.close();
208
+ await db.close();
209
+ ```
210
+
190
211
  #### Raw Query Format
191
212
 
192
213
  `queryRaw` / `queryRawSync` return `{ columns: string[], rows: any[][] }` instead of an array of objects. Faster when you don't need named keys.
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) {
@@ -312,7 +320,7 @@ class Database {
312
320
 
313
321
  async begin() {
314
322
  const txPtr = await this._beginAsync();
315
- const tx = new Transaction(txPtr, this.#stoolap, this);
323
+ const tx = new Transaction(txPtr, this.#stoolap, this, this.#ptr);
316
324
  this._trackChild(tx);
317
325
  return tx;
318
326
  }
@@ -320,7 +328,7 @@ class Database {
320
328
  beginSync() {
321
329
  this.#checkOpen();
322
330
  const txPtr = this.#stoolap.txBegin(this.#ptr);
323
- const tx = new Transaction(txPtr, this.#stoolap, this);
331
+ const tx = new Transaction(txPtr, this.#stoolap, this, this.#ptr);
324
332
  this._trackChild(tx);
325
333
  return tx;
326
334
  }
@@ -6,15 +6,17 @@ const { parseRows, parseRaw } = require("./parse");
6
6
  class Transaction {
7
7
  #ptr = null;
8
8
  #stoolap = null;
9
+ #dbPtr = null;
9
10
  #pendingAsync = 0;
10
11
  #tail = Promise.resolve();
11
12
  #closingPromise = null;
12
13
  #owner = null;
13
14
 
14
- constructor(ptr, stoolap, owner) {
15
+ constructor(ptr, stoolap, owner, dbPtr) {
15
16
  this.#ptr = ptr;
16
17
  this.#stoolap = stoolap;
17
18
  this.#owner = owner || null;
19
+ this.#dbPtr = dbPtr || null;
18
20
  }
19
21
 
20
22
  _invalidate() {
@@ -177,7 +179,7 @@ class Transaction {
177
179
  executeBatchSync(sql, paramsArray) {
178
180
  this.#checkSyncReady();
179
181
  if (paramsArray.length > 0 && Array.isArray(paramsArray[0])) {
180
- return this.#stoolap.txExecBatch(this.#ptr, sql, paramsArray);
182
+ return this.#stoolap.txExecBatch(this.#dbPtr, this.#ptr, sql, paramsArray);
181
183
  }
182
184
  let totalChanges = 0;
183
185
  for (const params of paramsArray) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stoolap/node",
3
- "version": "0.3.4",
3
+ "version": "0.3.7",
4
4
  "description": "High-performance Node.js driver for Stoolap embedded SQL database",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -13,15 +13,16 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "install": "node-gyp rebuild",
16
+ "build": "node-gyp rebuild",
16
17
  "test": "node --test __test__/index.spec.mjs",
17
18
  "bench": "node benchmark.mjs"
18
19
  },
19
20
  "optionalDependencies": {
20
- "@stoolap/lib-darwin-arm64": "0.3.4",
21
- "@stoolap/lib-darwin-x64": "0.3.4",
22
- "@stoolap/lib-linux-arm64-gnu": "0.3.4",
23
- "@stoolap/lib-linux-x64-gnu": "0.3.4",
24
- "@stoolap/lib-win32-x64-msvc": "0.3.4"
21
+ "@stoolap/lib-darwin-arm64": "0.3.7",
22
+ "@stoolap/lib-darwin-x64": "0.3.7",
23
+ "@stoolap/lib-linux-arm64-gnu": "0.3.7",
24
+ "@stoolap/lib-linux-x64-gnu": "0.3.7",
25
+ "@stoolap/lib-win32-x64-msvc": "0.3.7"
25
26
  },
26
27
  "devDependencies": {
27
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>
@@ -68,6 +69,7 @@ typedef struct {
68
69
  typedef const char* (*fn_version)(void);
69
70
  typedef int32_t (*fn_open)(const char*, StoolapDB**);
70
71
  typedef int32_t (*fn_open_mem)(StoolapDB**);
72
+ typedef int32_t (*fn_clone)(StoolapDB*, StoolapDB**);
71
73
  typedef int32_t (*fn_close)(StoolapDB*);
72
74
  typedef const char* (*fn_errmsg)(const StoolapDB*);
73
75
  typedef int32_t (*fn_exec)(StoolapDB*, const char*, int64_t*);
@@ -85,6 +87,8 @@ typedef int32_t (*fn_tx_exec)(StoolapTx*, const char*, int64_t*);
85
87
  typedef int32_t (*fn_tx_exec_p)(StoolapTx*, const char*, const StoolapValue*, int32_t, int64_t*);
86
88
  typedef int32_t (*fn_tx_query)(StoolapTx*, const char*, StoolapRows**);
87
89
  typedef int32_t (*fn_tx_query_p)(StoolapTx*, const char*, const StoolapValue*, int32_t, StoolapRows**);
90
+ typedef int32_t (*fn_tx_stmt_exec)(StoolapTx*, StoolapStmt*, const StoolapValue*, int32_t, int64_t*);
91
+ typedef int32_t (*fn_tx_stmt_query)(StoolapTx*, StoolapStmt*, const StoolapValue*, int32_t, StoolapRows**);
88
92
  typedef int32_t (*fn_tx_commit)(StoolapTx*);
89
93
  typedef int32_t (*fn_tx_rollback)(StoolapTx*);
90
94
  typedef const char* (*fn_tx_errmsg)(const StoolapTx*);
@@ -110,6 +114,7 @@ static struct {
110
114
  fn_version version;
111
115
  fn_open open;
112
116
  fn_open_mem open_mem;
117
+ fn_clone clone;
113
118
  fn_close close;
114
119
  fn_errmsg errmsg;
115
120
  fn_exec exec;
@@ -127,6 +132,8 @@ static struct {
127
132
  fn_tx_exec_p tx_exec_p;
128
133
  fn_tx_query tx_query;
129
134
  fn_tx_query_p tx_query_p;
135
+ fn_tx_stmt_exec tx_stmt_exec;
136
+ fn_tx_stmt_query tx_stmt_query;
130
137
  fn_tx_commit tx_commit;
131
138
  fn_tx_rollback tx_rollback;
132
139
  fn_tx_errmsg tx_errmsg;
@@ -152,6 +159,8 @@ static LibHandle lib_handle = NULL;
152
159
  /* ---- Helpers ---- */
153
160
 
154
161
  #define THROW(env, msg) do { napi_throw_error(env, NULL, msg); return NULL; } while(0)
162
+ #define ERR_SAVE(msg, fallback) \
163
+ char _eb[512]; snprintf(_eb, sizeof(_eb), "%s", (msg) ? (msg) : (fallback))
155
164
  #define CHECK(env, status) if ((status) != napi_ok) return NULL
156
165
 
157
166
  static napi_value make_changes(napi_env env, int64_t n) {
@@ -1506,6 +1515,7 @@ static napi_value fn_load_library(napi_env env, napi_callback_info info) {
1506
1515
  LOAD(version, "stoolap_version");
1507
1516
  LOAD(open, "stoolap_open");
1508
1517
  LOAD(open_mem, "stoolap_open_in_memory");
1518
+ LOAD(clone, "stoolap_clone");
1509
1519
  LOAD(close, "stoolap_close");
1510
1520
  LOAD(errmsg, "stoolap_errmsg");
1511
1521
  LOAD(exec, "stoolap_exec");
@@ -1523,6 +1533,10 @@ static napi_value fn_load_library(napi_env env, napi_callback_info info) {
1523
1533
  LOAD(tx_exec_p, "stoolap_tx_exec_params");
1524
1534
  LOAD(tx_query, "stoolap_tx_query");
1525
1535
  LOAD(tx_query_p, "stoolap_tx_query_params");
1536
+ /* Optional symbols — not in all engine versions */
1537
+ S.tx_stmt_exec = (fn_tx_stmt_exec) DLSYM(lib_handle, "stoolap_tx_stmt_exec");
1538
+ S.tx_stmt_query = (fn_tx_stmt_query)DLSYM(lib_handle, "stoolap_tx_stmt_query");
1539
+
1526
1540
  LOAD(tx_commit, "stoolap_tx_commit");
1527
1541
  LOAD(tx_rollback, "stoolap_tx_rollback");
1528
1542
  LOAD(tx_errmsg, "stoolap_tx_errmsg");
@@ -1578,6 +1592,27 @@ static napi_value fn_db_open(napi_env env, napi_callback_info info) {
1578
1592
  return ext;
1579
1593
  }
1580
1594
 
1595
+ /* dbClone(external): external */
1596
+ static napi_value fn_db_clone(napi_env env, napi_callback_info info) {
1597
+ size_t argc = 1;
1598
+ napi_value argv[1];
1599
+ napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
1600
+
1601
+ StoolapDB* db;
1602
+ napi_get_value_external(env, argv[0], (void**)&db);
1603
+
1604
+ StoolapDB* cloned = NULL;
1605
+ int32_t rc = S.clone(db, &cloned);
1606
+ if (rc != STOOLAP_OK) {
1607
+ const char* msg = S.errmsg(db);
1608
+ THROW(env, msg ? msg : "Failed to clone database");
1609
+ }
1610
+
1611
+ napi_value ext;
1612
+ napi_create_external(env, cloned, NULL, NULL, &ext);
1613
+ return ext;
1614
+ }
1615
+
1581
1616
  /* dbClose(external): void */
1582
1617
  static napi_value fn_db_close(napi_env env, napi_callback_info info) {
1583
1618
  size_t argc = 1;
@@ -2948,22 +2983,37 @@ static napi_value wrap_tx_rollback_async(napi_env env, napi_callback_info info)
2948
2983
  * Batch exec in C — copies SQL once, pre-allocates param buffers,
2949
2984
  * loops param sets in C. Eliminates N × JS↔C crossings + N×malloc/free. */
2950
2985
  static napi_value wrap_tx_exec_batch(napi_env env, napi_callback_info info) {
2951
- size_t argc = 3;
2952
- napi_value argv[3];
2986
+ size_t argc = 4;
2987
+ napi_value argv[4];
2953
2988
  napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
2954
2989
 
2990
+ StoolapDB* db;
2991
+ napi_get_value_external(env, argv[0], (void**)&db);
2992
+
2955
2993
  StoolapTx* tx;
2956
- napi_get_value_external(env, argv[0], (void**)&tx);
2994
+ napi_get_value_external(env, argv[1], (void**)&tx);
2957
2995
 
2958
- GET_SQL_STRING(env, argv[1], sql);
2996
+ GET_SQL_STRING(env, argv[2], sql);
2959
2997
 
2960
2998
  uint32_t batch_len;
2961
- napi_get_array_length(env, argv[2], &batch_len);
2999
+ napi_get_array_length(env, argv[3], &batch_len);
2962
3000
  if (batch_len == 0) return make_changes(env, 0);
2963
3001
 
3002
+ /* Prepare statement once if tx_stmt_exec is available */
3003
+ StoolapStmt* stmt = NULL;
3004
+ int use_stmt = (S.tx_stmt_exec != NULL);
3005
+ int32_t rc;
3006
+ if (use_stmt) {
3007
+ rc = S.prepare(db, sql, &stmt);
3008
+ if (rc != STOOLAP_OK) {
3009
+ const char* msg = S.errmsg(db);
3010
+ THROW(env, msg ? msg : "Prepare error");
3011
+ }
3012
+ }
3013
+
2964
3014
  /* Get param count from first element — pre-allocate ONCE */
2965
3015
  napi_value first_arr;
2966
- napi_get_element(env, argv[2], 0, &first_arr);
3016
+ napi_get_element(env, argv[3], 0, &first_arr);
2967
3017
  uint32_t param_count;
2968
3018
  napi_get_array_length(env, first_arr, &param_count);
2969
3019
 
@@ -2974,7 +3024,7 @@ static napi_value wrap_tx_exec_batch(napi_env env, napi_callback_info info) {
2974
3024
 
2975
3025
  for (uint32_t i = 0; i < batch_len; i++) {
2976
3026
  napi_value params_arr;
2977
- napi_get_element(env, argv[2], i, &params_arr);
3027
+ napi_get_element(env, argv[3], i, &params_arr);
2978
3028
 
2979
3029
  /* Convert params into pre-allocated buffers */
2980
3030
  int tbuf_cnt = 0;
@@ -2982,23 +3032,28 @@ static napi_value wrap_tx_exec_batch(napi_env env, napi_callback_info info) {
2982
3032
  napi_value elem;
2983
3033
  napi_get_element(env, params_arr, j, &elem);
2984
3034
  if (js_to_value(env, elem, &vals[j], tbufs, &tbuf_cnt) != 0) {
2985
- /* Cleanup on error */
2986
3035
  for (int k = 0; k < tbuf_cnt; k++) free(tbufs[k]);
2987
3036
  free(vals); free(tbufs);
3037
+ if (stmt) S.stmt_finalize(stmt);
2988
3038
  THROW(env, "Failed to convert parameters");
2989
3039
  }
2990
3040
  }
2991
3041
 
2992
3042
  int64_t affected = 0;
2993
- int32_t rc = S.tx_exec_p(tx, sql, vals, (int32_t)param_count, &affected);
3043
+ if (use_stmt) {
3044
+ rc = S.tx_stmt_exec(tx, stmt, vals, (int32_t)param_count, &affected);
3045
+ } else {
3046
+ rc = S.tx_exec_p(tx, sql, vals, (int32_t)param_count, &affected);
3047
+ }
2994
3048
 
2995
3049
  /* Free only text buffers, keep vals/tbufs arrays for reuse */
2996
3050
  for (int k = 0; k < tbuf_cnt; k++) { free(tbufs[k]); tbufs[k] = NULL; }
2997
3051
 
2998
3052
  if (rc != STOOLAP_OK) {
3053
+ ERR_SAVE(S.tx_errmsg(tx), "Transaction batch exec error");
2999
3054
  free(vals); free(tbufs);
3000
- const char* msg = S.tx_errmsg(tx);
3001
- THROW(env, msg ? msg : "Transaction batch exec error");
3055
+ if (stmt) S.stmt_finalize(stmt);
3056
+ THROW(env, _eb);
3002
3057
  }
3003
3058
 
3004
3059
  total_affected += affected;
@@ -3006,6 +3061,7 @@ static napi_value wrap_tx_exec_batch(napi_env env, napi_callback_info info) {
3006
3061
 
3007
3062
  free(vals);
3008
3063
  free(tbufs);
3064
+ if (stmt) S.stmt_finalize(stmt);
3009
3065
  return make_changes(env, total_affected);
3010
3066
  }
3011
3067
 
@@ -3170,23 +3226,30 @@ static napi_value wrap_db_exec_batch_buf(napi_env env, napi_callback_info info)
3170
3226
  int32_t rc = S.begin(db, &tx);
3171
3227
  if (rc != STOOLAP_OK) { const char* msg = S.errmsg(db); THROW(env, msg ? msg : "Begin error"); }
3172
3228
  StoolapStmt* stmt = NULL;
3173
- rc = S.prepare(db, sql, &stmt);
3174
- if (rc != STOOLAP_OK) { S.tx_rollback(tx); const char* msg = S.errmsg(db); THROW(env, msg ? msg : "Prepare error"); }
3229
+ int use_stmt = (S.tx_stmt_exec != NULL);
3230
+ if (use_stmt) {
3231
+ rc = S.prepare(db, sql, &stmt);
3232
+ if (rc != STOOLAP_OK) { ERR_SAVE(S.errmsg(db), "Prepare error"); S.tx_rollback(tx); THROW(env, _eb); }
3233
+ }
3175
3234
  int64_t total = 0;
3176
3235
  for (uint32_t i = 0; i < row_count; i++) {
3177
3236
  StoolapValue vals[MAX_STACK_PARAMS];
3178
3237
  size_t consumed = 0;
3179
3238
  int decoded = decode_params_buf(ptr, (size_t)(end - ptr), vals, ppr, &consumed);
3180
- if (decoded != ppr) { S.stmt_finalize(stmt); S.tx_rollback(tx); THROW(env, "Batch param decode error"); }
3239
+ if (decoded != ppr) { if (stmt) S.stmt_finalize(stmt); S.tx_rollback(tx); THROW(env, "Batch param decode error"); }
3181
3240
  ptr += consumed;
3182
3241
  int64_t affected = 0;
3183
- rc = S.stmt_exec(stmt, vals, decoded, &affected);
3184
- if (rc != STOOLAP_OK) { const char* msg = S.stmt_errmsg(stmt); S.stmt_finalize(stmt); S.tx_rollback(tx); THROW(env, msg ? msg : "Batch exec error"); }
3242
+ if (use_stmt) {
3243
+ rc = S.tx_stmt_exec(tx, stmt, vals, decoded, &affected);
3244
+ } else {
3245
+ rc = S.tx_exec_p(tx, sql, vals, decoded, &affected);
3246
+ }
3247
+ 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); }
3185
3248
  total += affected;
3186
3249
  }
3187
- S.stmt_finalize(stmt);
3250
+ if (stmt) S.stmt_finalize(stmt);
3188
3251
  rc = S.tx_commit(tx);
3189
- if (rc != STOOLAP_OK) { const char* msg = S.errmsg(db); THROW(env, msg ? msg : "Commit error"); }
3252
+ if (rc != STOOLAP_OK) { THROW(env, "Commit error"); }
3190
3253
  return make_changes(env, total);
3191
3254
  }
3192
3255
 
@@ -3242,6 +3305,7 @@ static napi_value Init(napi_env env, napi_value exports) {
3242
3305
  EXPORT_FN(env, exports, "loadLibrary", fn_load_library);
3243
3306
  EXPORT_FN(env, exports, "dbOpen", fn_db_open);
3244
3307
  EXPORT_FN(env, exports, "dbOpenAsync", fn_db_open_async);
3308
+ EXPORT_FN(env, exports, "dbClone", fn_db_clone);
3245
3309
  EXPORT_FN(env, exports, "dbClose", fn_db_close);
3246
3310
  EXPORT_FN(env, exports, "dbCloseAsync", fn_db_close_async);
3247
3311
  EXPORT_FN(env, exports, "dbExec", fn_db_exec);