@stoolap/node 0.3.3 → 0.3.5

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.
@@ -0,0 +1,367 @@
1
+ "use strict";
2
+
3
+ const { loadStoolap, processParams, rewriteNamedParams } = require("./ffi");
4
+ const { parseRows, parseRaw } = require("./parse");
5
+ const { Transaction } = require("./transaction");
6
+ const { PreparedStatement, encodeBatch } = require("./statement");
7
+
8
+ function normalizeDsn(dbPath) {
9
+ let dsn = dbPath || ":memory:";
10
+ if (
11
+ dsn !== ":memory:" &&
12
+ dsn !== "memory://" &&
13
+ dsn !== "" &&
14
+ !dsn.includes("://")
15
+ ) {
16
+ dsn = `file://${dsn}`;
17
+ }
18
+ return dsn;
19
+ }
20
+
21
+ class Database {
22
+ #ptr = null;
23
+ #stoolap = null;
24
+ #pendingAsync = 0;
25
+ #closeWaiter = null;
26
+ #closingPromise = null;
27
+ #children = new Set();
28
+
29
+ constructor(ptr, stoolap) {
30
+ this.#ptr = ptr;
31
+ this.#stoolap = stoolap;
32
+ }
33
+
34
+ _trackChild(child) {
35
+ this.#children.add(child);
36
+ }
37
+
38
+ _untrackChild(child) {
39
+ this.#children.delete(child);
40
+ }
41
+
42
+ #closeChildren() {
43
+ for (const child of this.#children) {
44
+ child._invalidate();
45
+ }
46
+ this.#children.clear();
47
+ }
48
+
49
+ static async open(dbPath) {
50
+ const stoolap = loadStoolap();
51
+ const ptr = await stoolap.dbOpenAsync(normalizeDsn(dbPath));
52
+ return new Database(ptr, stoolap);
53
+ }
54
+
55
+ static openSync(dbPath) {
56
+ const stoolap = loadStoolap();
57
+ const ptr = stoolap.dbOpen(normalizeDsn(dbPath));
58
+ return new Database(ptr, stoolap);
59
+ }
60
+
61
+ #checkOpen() {
62
+ if (!this.#ptr) throw new Error("Database is closed");
63
+ }
64
+
65
+ #notifyCloseWaiter() {
66
+ if (this.#pendingAsync === 0 && this.#closeWaiter) {
67
+ const resolve = this.#closeWaiter;
68
+ this.#closeWaiter = null;
69
+ resolve();
70
+ }
71
+ }
72
+
73
+ async #runAsync(operation) {
74
+ this.#checkOpen();
75
+ if (this.#closingPromise) throw new Error("Database is closing");
76
+ const ptr = this.#ptr;
77
+ this.#pendingAsync += 1;
78
+ try {
79
+ return await operation(ptr);
80
+ } finally {
81
+ this.#pendingAsync -= 1;
82
+ this.#notifyCloseWaiter();
83
+ }
84
+ }
85
+
86
+ _executeAsync(sql, params) {
87
+ return this.#runAsync((ptr) =>
88
+ params === undefined
89
+ ? this.#stoolap.dbExecAsync(ptr, sql)
90
+ : this.#stoolap.dbExecAsync(ptr, sql, params),
91
+ );
92
+ }
93
+
94
+ _queryBufferAsync(sql, params) {
95
+ return this.#runAsync((ptr) =>
96
+ params === undefined
97
+ ? this.#stoolap.dbQueryBufAsync(ptr, sql)
98
+ : this.#stoolap.dbQueryBufAsync(ptr, sql, params),
99
+ );
100
+ }
101
+
102
+ _execAsync(sql) {
103
+ return this.#runAsync((ptr) => this.#stoolap.dbExecSimpleAsync(ptr, sql));
104
+ }
105
+
106
+ _beginAsync() {
107
+ return this.#runAsync((ptr) => this.#stoolap.txBeginAsync(ptr));
108
+ }
109
+
110
+ _stmtExecAsync(stmtPtr, params) {
111
+ return this.#runAsync(() =>
112
+ params === undefined
113
+ ? this.#stoolap.stmtExecAsync(stmtPtr)
114
+ : this.#stoolap.stmtExecAsync(stmtPtr, params),
115
+ );
116
+ }
117
+
118
+ _stmtQueryBufAsync(stmtPtr, params) {
119
+ return this.#runAsync(() =>
120
+ params === undefined
121
+ ? this.#stoolap.stmtQueryBufAsync(stmtPtr)
122
+ : this.#stoolap.stmtQueryBufAsync(stmtPtr, params),
123
+ );
124
+ }
125
+
126
+ _executeBatchBufferSync(sql, batchBuf) {
127
+ this.#checkOpen();
128
+ return this.#stoolap.dbExecBatchBuf(this.#ptr, sql, batchBuf);
129
+ }
130
+
131
+ // -- Execute (DML) --
132
+
133
+ async execute(sql, params) {
134
+ if (!params) return this._executeAsync(sql);
135
+ if (Array.isArray(params)) {
136
+ if (params.length === 0) return this._executeAsync(sql);
137
+ return this._executeAsync(sql, params);
138
+ }
139
+ const p = processParams(sql, params);
140
+ return p.values
141
+ ? this._executeAsync(p.sql, p.values)
142
+ : this._executeAsync(sql);
143
+ }
144
+
145
+ executeSync(sql, params) {
146
+ this.#checkOpen();
147
+ if (!params) return this.#stoolap.dbExec(this.#ptr, sql);
148
+ if (Array.isArray(params)) {
149
+ if (params.length === 0) return this.#stoolap.dbExec(this.#ptr, sql);
150
+ return this.#stoolap.dbExec(this.#ptr, sql, params);
151
+ }
152
+ const p = processParams(sql, params);
153
+ return p.values
154
+ ? this.#stoolap.dbExec(this.#ptr, p.sql, p.values)
155
+ : this.#stoolap.dbExec(this.#ptr, sql);
156
+ }
157
+
158
+ // -- Exec (DDL) --
159
+
160
+ async exec(sql) {
161
+ await this._execAsync(sql);
162
+ }
163
+
164
+ execSync(sql) {
165
+ this.#checkOpen();
166
+ this.#stoolap.dbExecSimple(this.#ptr, sql);
167
+ }
168
+
169
+ // -- Query --
170
+
171
+ async query(sql, params) {
172
+ let buf;
173
+ if (!params) {
174
+ buf = await this._queryBufferAsync(sql);
175
+ } else if (Array.isArray(params)) {
176
+ buf =
177
+ params.length === 0
178
+ ? await this._queryBufferAsync(sql)
179
+ : await this._queryBufferAsync(sql, params);
180
+ } else {
181
+ const p = processParams(sql, params);
182
+ buf = p.values
183
+ ? await this._queryBufferAsync(p.sql, p.values)
184
+ : await this._queryBufferAsync(sql);
185
+ }
186
+ return parseRows(buf);
187
+ }
188
+
189
+ querySync(sql, params) {
190
+ this.#checkOpen();
191
+ let result;
192
+ if (!params) {
193
+ result = this.#stoolap.dbQuery(this.#ptr, sql);
194
+ } else if (Array.isArray(params)) {
195
+ result =
196
+ params.length === 0
197
+ ? this.#stoolap.dbQuery(this.#ptr, sql)
198
+ : this.#stoolap.dbQuery(this.#ptr, sql, params);
199
+ } else {
200
+ const p = processParams(sql, params);
201
+ result = p.values
202
+ ? this.#stoolap.dbQuery(this.#ptr, p.sql, p.values)
203
+ : this.#stoolap.dbQuery(this.#ptr, sql);
204
+ }
205
+ return Array.isArray(result) ? result : parseRows(result);
206
+ }
207
+
208
+ // -- QueryOne --
209
+
210
+ async queryOne(sql, params) {
211
+ const rows = await this.query(sql, params);
212
+ return rows.length === 0 ? null : rows[0];
213
+ }
214
+
215
+ queryOneSync(sql, params) {
216
+ this.#checkOpen();
217
+ if (!params) return this.#stoolap.dbQueryOne(this.#ptr, sql);
218
+ if (Array.isArray(params)) {
219
+ if (params.length === 0) return this.#stoolap.dbQueryOne(this.#ptr, sql);
220
+ return this.#stoolap.dbQueryOne(this.#ptr, sql, params);
221
+ }
222
+ const p = processParams(sql, params);
223
+ return p.values
224
+ ? this.#stoolap.dbQueryOne(this.#ptr, p.sql, p.values)
225
+ : this.#stoolap.dbQueryOne(this.#ptr, sql);
226
+ }
227
+
228
+ // -- QueryRaw --
229
+
230
+ async queryRaw(sql, params) {
231
+ let buf;
232
+ if (!params) {
233
+ buf = await this._queryBufferAsync(sql);
234
+ } else if (Array.isArray(params)) {
235
+ buf =
236
+ params.length === 0
237
+ ? await this._queryBufferAsync(sql)
238
+ : await this._queryBufferAsync(sql, params);
239
+ } else {
240
+ const p = processParams(sql, params);
241
+ buf = p.values
242
+ ? await this._queryBufferAsync(p.sql, p.values)
243
+ : await this._queryBufferAsync(sql);
244
+ }
245
+ return parseRaw(buf);
246
+ }
247
+
248
+ queryRawSync(sql, params) {
249
+ this.#checkOpen();
250
+ let buf;
251
+ if (!params) {
252
+ buf = this.#stoolap.dbQueryBuf(this.#ptr, sql);
253
+ } else if (Array.isArray(params)) {
254
+ buf =
255
+ params.length === 0
256
+ ? this.#stoolap.dbQueryBuf(this.#ptr, sql)
257
+ : this.#stoolap.dbQueryBuf(this.#ptr, sql, params);
258
+ } else {
259
+ const p = processParams(sql, params);
260
+ buf = p.values
261
+ ? this.#stoolap.dbQueryBuf(this.#ptr, p.sql, p.values)
262
+ : this.#stoolap.dbQueryBuf(this.#ptr, sql);
263
+ }
264
+ return parseRaw(buf);
265
+ }
266
+
267
+ // -- Batch --
268
+
269
+ executeBatchSync(sql, paramsArray) {
270
+ this.#checkOpen();
271
+ if (paramsArray.length > 0 && Array.isArray(paramsArray[0])) {
272
+ const batchBuf = encodeBatch(paramsArray);
273
+ return this.#stoolap.dbExecBatchBuf(this.#ptr, sql, batchBuf);
274
+ }
275
+
276
+ const tx = this.beginSync();
277
+ try {
278
+ const result = tx.executeBatchSync(sql, paramsArray);
279
+ tx.commitSync();
280
+ return result;
281
+ } catch (e) {
282
+ try {
283
+ tx.rollbackSync();
284
+ } catch {}
285
+ throw e;
286
+ }
287
+ }
288
+
289
+ // -- Prepared statements --
290
+
291
+ prepare(sql) {
292
+ this.#checkOpen();
293
+ const prepared = rewriteNamedParams(sql);
294
+ const [stmtPtr, queryOneIntBound] = this.#stoolap.dbPrepare(
295
+ this.#ptr,
296
+ prepared.sql,
297
+ );
298
+ const stmt = new PreparedStatement(
299
+ stmtPtr,
300
+ this.#stoolap,
301
+ queryOneIntBound,
302
+ this,
303
+ sql,
304
+ prepared.sql,
305
+ prepared.names,
306
+ );
307
+ this._trackChild(stmt);
308
+ return stmt;
309
+ }
310
+
311
+ // -- Transactions --
312
+
313
+ async begin() {
314
+ const txPtr = await this._beginAsync();
315
+ const tx = new Transaction(txPtr, this.#stoolap, this, this.#ptr);
316
+ this._trackChild(tx);
317
+ return tx;
318
+ }
319
+
320
+ beginSync() {
321
+ this.#checkOpen();
322
+ const txPtr = this.#stoolap.txBegin(this.#ptr);
323
+ const tx = new Transaction(txPtr, this.#stoolap, this, this.#ptr);
324
+ this._trackChild(tx);
325
+ return tx;
326
+ }
327
+
328
+ // -- Close --
329
+
330
+ async close() {
331
+ if (this.#closingPromise) return this.#closingPromise;
332
+ if (!this.#ptr) return;
333
+
334
+ this.#closeChildren();
335
+ const ptr = this.#ptr;
336
+ this.#ptr = null;
337
+
338
+ const wait =
339
+ this.#pendingAsync === 0
340
+ ? Promise.resolve()
341
+ : new Promise((resolve) => {
342
+ this.#closeWaiter = resolve;
343
+ });
344
+
345
+ this.#closingPromise = wait
346
+ .then(() => this.#stoolap.dbCloseAsync(ptr))
347
+ .finally(() => {
348
+ this.#closingPromise = null;
349
+ this.#closeWaiter = null;
350
+ });
351
+
352
+ return this.#closingPromise;
353
+ }
354
+
355
+ closeSync() {
356
+ if (this.#closingPromise || this.#pendingAsync > 0) {
357
+ throw new Error("Database has pending async operations");
358
+ }
359
+ if (this.#ptr) {
360
+ this.#closeChildren();
361
+ this.#stoolap.dbClose(this.#ptr);
362
+ this.#ptr = null;
363
+ }
364
+ }
365
+ }
366
+
367
+ module.exports = { Database };
package/lib/ffi.js ADDED
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+
3
+ const path = require("node:path");
4
+
5
+ // -- Library path resolution --
6
+ function findLibraryPath() {
7
+ if (process.env.STOOLAP_LIB_PATH) {
8
+ return process.env.STOOLAP_LIB_PATH;
9
+ }
10
+
11
+ const platform = process.platform;
12
+ const arch = process.arch;
13
+ const ext =
14
+ platform === "win32" ? "dll" : platform === "darwin" ? "dylib" : "so";
15
+ const prefix = platform === "win32" ? "" : "lib";
16
+ const libName = `${prefix}stoolap.${ext}`;
17
+
18
+ const platformMap = {
19
+ "darwin-arm64": "@stoolap/lib-darwin-arm64",
20
+ "darwin-x64": "@stoolap/lib-darwin-x64",
21
+ "linux-arm64": "@stoolap/lib-linux-arm64-gnu",
22
+ "linux-x64": "@stoolap/lib-linux-x64-gnu",
23
+ "win32-x64": "@stoolap/lib-win32-x64-msvc",
24
+ };
25
+
26
+ const key = `${platform}-${arch}`;
27
+ const pkg = platformMap[key];
28
+ if (!pkg) {
29
+ throw new Error(`Unsupported platform: ${platform}/${arch}`);
30
+ }
31
+
32
+ try {
33
+ const pkgDir = path.dirname(require.resolve(`${pkg}/package.json`));
34
+ return path.join(pkgDir, libName);
35
+ } catch {
36
+ const localPath = path.join(__dirname, "..", libName);
37
+ try {
38
+ require("node:fs").accessSync(localPath);
39
+ return localPath;
40
+ } catch {
41
+ throw new Error(
42
+ `Could not find stoolap shared library. Install the platform package: npm install ${pkg}`,
43
+ );
44
+ }
45
+ }
46
+ }
47
+
48
+ // -- Load native stoolap module --
49
+ let _stoolap = null;
50
+
51
+ function loadStoolap() {
52
+ if (_stoolap) return _stoolap;
53
+
54
+ _stoolap = require("../build/Release/stoolap.node");
55
+ _stoolap.loadLibrary(findLibraryPath());
56
+ return _stoolap;
57
+ }
58
+
59
+ // -- Named parameter rewriting --
60
+ function isIdentStart(ch) {
61
+ return ch !== undefined && /[A-Za-z_]/.test(ch);
62
+ }
63
+
64
+ function isIdentPart(ch) {
65
+ return ch !== undefined && /[A-Za-z0-9_]/.test(ch);
66
+ }
67
+
68
+ function scanSql(sql, onParam) {
69
+ const out = [];
70
+
71
+ for (let i = 0; i < sql.length; ) {
72
+ const ch = sql[i];
73
+
74
+ if (ch === "'") {
75
+ let j = i + 1;
76
+ while (j < sql.length) {
77
+ if (sql[j] === "'" && sql[j + 1] === "'") {
78
+ j += 2;
79
+ continue;
80
+ }
81
+ if (sql[j] === "'") {
82
+ j += 1;
83
+ break;
84
+ }
85
+ j += 1;
86
+ }
87
+ out.push(sql.slice(i, j));
88
+ i = j;
89
+ continue;
90
+ }
91
+
92
+ if (ch === '"') {
93
+ let j = i + 1;
94
+ while (j < sql.length) {
95
+ if (sql[j] === '"' && sql[j + 1] === '"') {
96
+ j += 2;
97
+ continue;
98
+ }
99
+ if (sql[j] === '"') {
100
+ j += 1;
101
+ break;
102
+ }
103
+ j += 1;
104
+ }
105
+ out.push(sql.slice(i, j));
106
+ i = j;
107
+ continue;
108
+ }
109
+
110
+ if (ch === "-" && sql[i + 1] === "-") {
111
+ let j = i + 2;
112
+ while (j < sql.length && sql[j] !== "\n") j += 1;
113
+ out.push(sql.slice(i, j));
114
+ i = j;
115
+ continue;
116
+ }
117
+
118
+ if (ch === "/" && sql[i + 1] === "*") {
119
+ let j = i + 2;
120
+ while (j < sql.length && !(sql[j] === "*" && sql[j + 1] === "/")) j += 1;
121
+ j = j < sql.length ? j + 2 : j;
122
+ out.push(sql.slice(i, j));
123
+ i = j;
124
+ continue;
125
+ }
126
+
127
+ if (ch === ":" && sql[i + 1] !== ":" && isIdentStart(sql[i + 1])) {
128
+ let j = i + 2;
129
+ while (j < sql.length && isIdentPart(sql[j])) j += 1;
130
+ out.push(onParam(sql.slice(i + 1, j)));
131
+ i = j;
132
+ continue;
133
+ }
134
+
135
+ out.push(ch);
136
+ i += 1;
137
+ }
138
+
139
+ return out.join("");
140
+ }
141
+
142
+ function processParams(sql, params) {
143
+ if (!params) return { sql, values: null };
144
+
145
+ if (Array.isArray(params)) {
146
+ if (params.length === 0) return { sql, values: null };
147
+ return { sql, values: params };
148
+ }
149
+
150
+ const seen = new Map();
151
+ const values = [];
152
+ const rewritten = scanSql(sql, (name) => {
153
+ if (!seen.has(name)) {
154
+ seen.set(name, values.length + 1);
155
+ values.push(params[name]);
156
+ }
157
+ return `$${seen.get(name)}`;
158
+ });
159
+
160
+ if (values.length === 0) return { sql, values: null };
161
+ return { sql: rewritten, values };
162
+ }
163
+
164
+ function rewriteNamedParams(sql) {
165
+ const seen = new Map();
166
+ const names = [];
167
+ const rewritten = scanSql(sql, (name) => {
168
+ if (!seen.has(name)) {
169
+ seen.set(name, names.length + 1);
170
+ names.push(name);
171
+ }
172
+ return `$${seen.get(name)}`;
173
+ });
174
+
175
+ return names.length === 0 ? { sql, names: null } : { sql: rewritten, names };
176
+ }
177
+
178
+ module.exports = {
179
+ loadStoolap,
180
+ processParams,
181
+ rewriteNamedParams,
182
+ findLibraryPath,
183
+ };