mdbxmou 0.2.7 → 0.3.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/CMakeLists.txt +1 -0
- package/README.md +195 -2
- package/package.json +1 -1
- package/src/async/envmou_async.cpp +450 -0
- package/src/async/envmou_async.hpp +116 -0
- package/src/cursormou.cpp +316 -0
- package/src/cursormou.hpp +69 -0
- package/src/modulemou.cpp +2 -0
- package/src/txnmou.cpp +43 -0
- package/src/txnmou.hpp +8 -0
- package/src/valuemou.hpp +2 -2
package/CMakeLists.txt
CHANGED
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ High-performance Node.js binding for libmdbx — a fast, lightweight, embedded k
|
|
|
9
9
|
- **Transactions** — ACID transactions with read/write modes
|
|
10
10
|
- **Multiple key/value types** — String, binary, ordinal (integer) keys
|
|
11
11
|
- **Batch operations** — Efficient multi-key read/write
|
|
12
|
-
- **Memory-mapped** —
|
|
12
|
+
- **Memory-mapped** — High-performance memory-mapped I/O
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
@@ -266,6 +266,199 @@ dbi.drop(txn, true);
|
|
|
266
266
|
dbi.drop(txn);
|
|
267
267
|
```
|
|
268
268
|
|
|
269
|
+
### Cursor (MDBX_Cursor)
|
|
270
|
+
|
|
271
|
+
Cursors provide low-level control for database traversal with positioning and iteration capabilities.
|
|
272
|
+
|
|
273
|
+
#### Creating a Cursor
|
|
274
|
+
```javascript
|
|
275
|
+
const txn = env.startRead();
|
|
276
|
+
const dbi = txn.openMap();
|
|
277
|
+
const cursor = txn.openCursor(dbi);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### Navigation Methods
|
|
281
|
+
|
|
282
|
+
**first() → {key, value} | undefined**
|
|
283
|
+
```javascript
|
|
284
|
+
const item = cursor.first();
|
|
285
|
+
if (item) {
|
|
286
|
+
console.log(item.key, item.value);
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**last() → {key, value} | undefined**
|
|
291
|
+
```javascript
|
|
292
|
+
const item = cursor.last();
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**next() → {key, value} | undefined**
|
|
296
|
+
```javascript
|
|
297
|
+
const item = cursor.next();
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**prev() → {key, value} | undefined**
|
|
301
|
+
```javascript
|
|
302
|
+
const item = cursor.prev();
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**current() → {key, value} | undefined**
|
|
306
|
+
```javascript
|
|
307
|
+
const item = cursor.current();
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
#### Search Methods
|
|
311
|
+
|
|
312
|
+
**seek(key) → {key, value} | undefined**
|
|
313
|
+
|
|
314
|
+
Exact key match. Returns `undefined` if key not found.
|
|
315
|
+
```javascript
|
|
316
|
+
const item = cursor.seek('user:123');
|
|
317
|
+
if (item) {
|
|
318
|
+
console.log('Found:', item.value);
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**seekGE(key) → {key, value} | undefined**
|
|
323
|
+
|
|
324
|
+
Find first key greater or equal to given key (lower_bound).
|
|
325
|
+
```javascript
|
|
326
|
+
const item = cursor.seekGE('user:100');
|
|
327
|
+
// Returns first key >= 'user:100'
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### Modification Methods
|
|
331
|
+
|
|
332
|
+
**put(key, value, [flags])**
|
|
333
|
+
|
|
334
|
+
Insert or update a record at cursor position.
|
|
335
|
+
```javascript
|
|
336
|
+
cursor.put('newKey', 'newValue');
|
|
337
|
+
|
|
338
|
+
// With flags (MDBX_NOOVERWRITE, etc.)
|
|
339
|
+
cursor.put('key', 'value', MDBX_Param.queryMode.insertUnique);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**del([flags]) → boolean**
|
|
343
|
+
|
|
344
|
+
Delete record at current cursor position. Returns `true` if deleted, `false` if not found.
|
|
345
|
+
```javascript
|
|
346
|
+
cursor.seek('keyToDelete');
|
|
347
|
+
const deleted = cursor.del();
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
#### Control Methods
|
|
351
|
+
|
|
352
|
+
**close()**
|
|
353
|
+
```javascript
|
|
354
|
+
cursor.close();
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### Cursor Examples
|
|
358
|
+
|
|
359
|
+
**Iterate all records:**
|
|
360
|
+
```javascript
|
|
361
|
+
const cursor = txn.openCursor(dbi);
|
|
362
|
+
|
|
363
|
+
for (let item = cursor.first(); item; item = cursor.next()) {
|
|
364
|
+
console.log(item.key, item.value.toString());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
cursor.close();
|
|
368
|
+
txn.abort();
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Range iteration:**
|
|
372
|
+
```javascript
|
|
373
|
+
const cursor = txn.openCursor(dbi);
|
|
374
|
+
|
|
375
|
+
// Find all keys starting with 'user:'
|
|
376
|
+
for (let item = cursor.seekGE('user:'); item; item = cursor.next()) {
|
|
377
|
+
if (!item.key.startsWith('user:')) break;
|
|
378
|
+
console.log(item.key, item.value.toString());
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
cursor.close();
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Pagination:**
|
|
385
|
+
```javascript
|
|
386
|
+
function getPage(cursor, offset, limit) {
|
|
387
|
+
const results = [];
|
|
388
|
+
|
|
389
|
+
let item = cursor.first();
|
|
390
|
+
|
|
391
|
+
// Skip offset
|
|
392
|
+
for (let i = 0; i < offset && item; i++) {
|
|
393
|
+
item = cursor.next();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Collect limit items
|
|
397
|
+
for (let i = 0; i < limit && item; i++) {
|
|
398
|
+
results.push({ key: item.key, value: item.value.toString() });
|
|
399
|
+
item = cursor.next();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return results;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const txn = env.startRead();
|
|
406
|
+
const dbi = txn.openMap();
|
|
407
|
+
const cursor = txn.openCursor(dbi);
|
|
408
|
+
|
|
409
|
+
const page1 = getPage(cursor, 0, 10); // First 10 items
|
|
410
|
+
const page2 = getPage(cursor, 10, 10); // Next 10 items
|
|
411
|
+
|
|
412
|
+
cursor.close();
|
|
413
|
+
txn.abort();
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Reverse iteration:**
|
|
417
|
+
```javascript
|
|
418
|
+
const cursor = txn.openCursor(dbi);
|
|
419
|
+
|
|
420
|
+
for (let item = cursor.last(); item; item = cursor.prev()) {
|
|
421
|
+
console.log(item.key, item.value.toString());
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
cursor.close();
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Bulk insert with cursor:**
|
|
428
|
+
```javascript
|
|
429
|
+
const txn = env.startWrite();
|
|
430
|
+
const dbi = txn.createMap();
|
|
431
|
+
const cursor = txn.openCursor(dbi);
|
|
432
|
+
|
|
433
|
+
for (let i = 0; i < 1000; i++) {
|
|
434
|
+
cursor.put(`key${i}`, `value${i}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
cursor.close();
|
|
438
|
+
txn.commit();
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Delete with cursor:**
|
|
442
|
+
```javascript
|
|
443
|
+
const txn = env.startWrite();
|
|
444
|
+
const dbi = txn.openMap();
|
|
445
|
+
const cursor = txn.openCursor(dbi);
|
|
446
|
+
|
|
447
|
+
// Delete specific key
|
|
448
|
+
if (cursor.seek('keyToDelete')) {
|
|
449
|
+
cursor.del();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Delete range
|
|
453
|
+
for (let item = cursor.seekGE('prefix:'); item; item = cursor.next()) {
|
|
454
|
+
if (!item.key.startsWith('prefix:')) break;
|
|
455
|
+
cursor.del();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
cursor.close();
|
|
459
|
+
txn.commit();
|
|
460
|
+
```
|
|
461
|
+
|
|
269
462
|
## Key and Value Types
|
|
270
463
|
|
|
271
464
|
### Key Modes (MDBX_Param.keyMode)
|
|
@@ -623,7 +816,7 @@ Note: For ordinal (integer) keys, use keyFlag.number or keyFlag.bigint to specif
|
|
|
623
816
|
1. **Use ordinal keys** for integer data - much faster than string keys
|
|
624
817
|
2. **Batch operations** - Use query API for bulk operations
|
|
625
818
|
3. **Reuse transactions** - Keep read transactions open for multiple operations
|
|
626
|
-
4. **Memory mapping** - MDBX uses memory-mapped files for
|
|
819
|
+
4. **Memory mapping** - MDBX uses memory-mapped files for fast I/O
|
|
627
820
|
5. **Transaction scope** - Always pass transaction object to DBI methods
|
|
628
821
|
|
|
629
822
|
## License
|
package/package.json
CHANGED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#include "envmou.hpp"
|
|
2
|
+
#include "txnmou.hpp"
|
|
3
|
+
#include "async/envmou_copy_to.hpp"
|
|
4
|
+
#include "async/envmou_query.hpp"
|
|
5
|
+
#include "async/envmou_open.hpp"
|
|
6
|
+
#include "async/envmou_keys.hpp"
|
|
7
|
+
#include "async/envmou_close.hpp"
|
|
8
|
+
|
|
9
|
+
#ifdef _WIN32
|
|
10
|
+
#include <windows.h>
|
|
11
|
+
#else
|
|
12
|
+
#include <pthread.h>
|
|
13
|
+
#endif
|
|
14
|
+
|
|
15
|
+
namespace mdbxmou {
|
|
16
|
+
|
|
17
|
+
Napi::FunctionReference envmou::ctor{};
|
|
18
|
+
|
|
19
|
+
void envmou::init(const char *class_name, Napi::Env env, Napi::Object exports)
|
|
20
|
+
{
|
|
21
|
+
auto func = DefineClass(env, class_name, {
|
|
22
|
+
InstanceMethod("open", &envmou::open),
|
|
23
|
+
InstanceMethod("openSync", &envmou::open_sync),
|
|
24
|
+
InstanceMethod("close", &envmou::close),
|
|
25
|
+
InstanceMethod("closeSync", &envmou::close_sync),
|
|
26
|
+
InstanceMethod("copyTo", &envmou::copy_to),
|
|
27
|
+
InstanceMethod("copyToSync", &envmou::copy_to_sync),
|
|
28
|
+
InstanceMethod("version", &envmou::get_version),
|
|
29
|
+
InstanceMethod("startRead", &envmou::start_read),
|
|
30
|
+
InstanceMethod("startWrite", &envmou::start_write),
|
|
31
|
+
InstanceMethod("query", &envmou::query),
|
|
32
|
+
InstanceMethod("keys", &envmou::keys)
|
|
33
|
+
});
|
|
34
|
+
ctor = Napi::Persistent(func);
|
|
35
|
+
ctor.SuppressDestruct();
|
|
36
|
+
exports.Set(class_name, func);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
mdbx::env::geometry envmou::parse_geometry(const Napi::Value& arg0)
|
|
40
|
+
{
|
|
41
|
+
mdbx::env::geometry geom{};
|
|
42
|
+
|
|
43
|
+
auto obj = arg0.As<Napi::Object>();
|
|
44
|
+
|
|
45
|
+
if (obj.Has("fixedSize")) {
|
|
46
|
+
auto value = obj.Get("fixedSize").As<Napi::Number>();
|
|
47
|
+
auto fixed_size = static_cast<intptr_t>(value.Int64Value());
|
|
48
|
+
return geom.make_fixed(fixed_size);
|
|
49
|
+
} else if (obj.Has("dynamicSize")) {
|
|
50
|
+
auto arr = obj.Get("dynamicSize").As<Napi::Array>();
|
|
51
|
+
if (arr.Length() != 2) {
|
|
52
|
+
throw Napi::TypeError::New(obj.Env(), "dynamicSize must be an array of two numbers");
|
|
53
|
+
}
|
|
54
|
+
auto v1 = arr.Get(0u).As<Napi::Number>();
|
|
55
|
+
auto v2 = arr.Get(1u).As<Napi::Number>();
|
|
56
|
+
auto size_lower = static_cast<intptr_t>(v1.Int64Value());
|
|
57
|
+
auto size_upper = static_cast<intptr_t>(v2.Int64Value());
|
|
58
|
+
return geom.make_dynamic(size_lower, size_upper);
|
|
59
|
+
} else {
|
|
60
|
+
if (obj.Has("sizeNow")) {
|
|
61
|
+
auto value = obj.Get("sizeNow").As<Napi::Number>();
|
|
62
|
+
geom.size_now = static_cast<intptr_t>(value.Int64Value());
|
|
63
|
+
}
|
|
64
|
+
if (obj.Has("sizeUpper")) {
|
|
65
|
+
auto value = obj.Get("sizeUpper").As<Napi::Number>();
|
|
66
|
+
geom.size_upper = static_cast<intptr_t>(value.Int64Value());
|
|
67
|
+
}
|
|
68
|
+
if (obj.Has("growthStep")) {
|
|
69
|
+
auto value = obj.Get("growthStep").As<Napi::Number>();
|
|
70
|
+
geom.growth_step = static_cast<intptr_t>(value.Int64Value());
|
|
71
|
+
}
|
|
72
|
+
if (obj.Has("shrinkThreshold")) {
|
|
73
|
+
auto value = obj.Get("shrinkThreshold").As<Napi::Number>();
|
|
74
|
+
geom.shrink_threshold = static_cast<intptr_t>(value.Int64Value());
|
|
75
|
+
}
|
|
76
|
+
if (obj.Has("pageSize")) {
|
|
77
|
+
auto value = obj.Get("pageSize").As<Napi::Number>();
|
|
78
|
+
geom.pagesize = static_cast<intptr_t>(value.Int64Value());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return geom;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
env_arg0 envmou::parse(const Napi::Value& arg0)
|
|
85
|
+
{
|
|
86
|
+
env_arg0 rc;
|
|
87
|
+
|
|
88
|
+
auto obj = arg0.As<Napi::Object>();
|
|
89
|
+
|
|
90
|
+
rc.path = obj.Get("path").As<Napi::String>().Utf8Value();
|
|
91
|
+
if (obj.Has("maxDbi")) {
|
|
92
|
+
auto value = obj.Get("maxDbi").As<Napi::Number>();
|
|
93
|
+
rc.max_dbi = static_cast<MDBX_dbi>(value.Uint32Value());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (obj.Has("geometry")) {
|
|
97
|
+
rc.geom = parse_geometry(obj.Get("geometry"));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (obj.Has("flags")) {
|
|
101
|
+
rc.flag = env_flag::parse(obj.Get("flags"));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (obj.Has("mode")) {
|
|
105
|
+
auto value = obj.Get("mode").As<Napi::Number>();
|
|
106
|
+
rc.file_mode = static_cast<mdbx_mode_t>(value.Int32Value());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (obj.Has("keyFlag")) {
|
|
110
|
+
rc.key_flag = base_flag::parse_key(obj.Get("keyFlag"));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (obj.Has("valueFlag")) {
|
|
114
|
+
rc.value_flag = base_flag::parse_value(obj.Get("valueFlag"));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return rc;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
Napi::Value envmou::open(const Napi::CallbackInfo& info)
|
|
121
|
+
{
|
|
122
|
+
auto env = info.Env();
|
|
123
|
+
auto arg0 = parse(info[0].As<Napi::Object>());
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// асинхронный вызов разлочится внутри worker'a
|
|
127
|
+
if (!try_lock()) {
|
|
128
|
+
throw std::runtime_error("in progress");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (is_open()) {
|
|
132
|
+
throw std::runtime_error("already opened");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
auto* worker = new async_open(env, *this, arg0);
|
|
136
|
+
Napi::Promise promise = worker->GetPromise();
|
|
137
|
+
worker->Queue();
|
|
138
|
+
return promise;
|
|
139
|
+
} catch (const std::exception& e) {
|
|
140
|
+
unlock();
|
|
141
|
+
throw Napi::Error::New(env, e.what());
|
|
142
|
+
} catch (...) {
|
|
143
|
+
unlock();
|
|
144
|
+
throw;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return env.Undefined();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Napi::Value envmou::open_sync(const Napi::CallbackInfo& info)
|
|
151
|
+
{
|
|
152
|
+
auto env = info.Env();
|
|
153
|
+
auto arg0 = parse(info[0].As<Napi::Object>());
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
lock_guard l(*this);
|
|
157
|
+
if (is_open()) {
|
|
158
|
+
throw std::runtime_error("already opened");
|
|
159
|
+
}
|
|
160
|
+
attach(create_and_open(arg0), arg0);
|
|
161
|
+
} catch (const std::exception& e) {
|
|
162
|
+
throw Napi::Error::New(env, e.what());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return env.Undefined();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
MDBX_env* envmou::create_and_open(const env_arg0& arg0)
|
|
169
|
+
{
|
|
170
|
+
MDBX_env *env;
|
|
171
|
+
auto rc = mdbx_env_create(&env);
|
|
172
|
+
if (rc != MDBX_SUCCESS) {
|
|
173
|
+
throw std::runtime_error(mdbx_strerror(rc));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
rc = mdbx_env_set_maxdbs(env, arg0.max_dbi);
|
|
177
|
+
if (rc != MDBX_SUCCESS) {
|
|
178
|
+
mdbx_env_close(env);
|
|
179
|
+
throw std::runtime_error(mdbx_strerror(rc));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
rc = mdbx_env_set_maxreaders(env, arg0.max_readers);
|
|
183
|
+
if (rc != MDBX_SUCCESS) {
|
|
184
|
+
mdbx_env_close(env);
|
|
185
|
+
throw std::runtime_error(mdbx_strerror(rc));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
auto& geom = arg0.geom;
|
|
189
|
+
rc = mdbx_env_set_geometry(env, geom.size_lower, geom.size_now,
|
|
190
|
+
geom.size_upper, geom.growth_step, geom.shrink_threshold, geom.pagesize);
|
|
191
|
+
if (rc != MDBX_SUCCESS) {
|
|
192
|
+
mdbx_env_close(env);
|
|
193
|
+
throw std::runtime_error(mdbx_strerror(rc));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#ifdef _WIN32
|
|
197
|
+
auto id = static_cast<std::uint32_t>(GetCurrentThreadId());
|
|
198
|
+
#else
|
|
199
|
+
auto id = static_cast<std::uint32_t>(pthread_self());
|
|
200
|
+
#endif
|
|
201
|
+
// выдадим параметры mode, flag и id потока в котором открывается env
|
|
202
|
+
rc = mdbx_env_open(env, arg0.path.c_str(), arg0.flag, arg0.file_mode);
|
|
203
|
+
if (rc != MDBX_SUCCESS) {
|
|
204
|
+
mdbx_env_close(env);
|
|
205
|
+
throw std::runtime_error(mdbx_strerror(rc));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return env;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
void envmou::attach(MDBX_env* env, const env_arg0& arg0)
|
|
212
|
+
{
|
|
213
|
+
arg0_ = arg0;
|
|
214
|
+
|
|
215
|
+
auto rc = mdbx_env_set_userctx(env, &arg0_);
|
|
216
|
+
if (rc != MDBX_SUCCESS) {
|
|
217
|
+
mdbx_env_close(env);
|
|
218
|
+
throw std::runtime_error(mdbx_strerror(rc));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
env_.reset(env);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
Napi::Value envmou::close(const Napi::CallbackInfo& info)
|
|
225
|
+
{
|
|
226
|
+
auto env = info.Env();
|
|
227
|
+
try {
|
|
228
|
+
// асинхронный вызов разлочится внутри worker'a
|
|
229
|
+
if (!try_lock()) {
|
|
230
|
+
throw std::runtime_error("in progress");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!is_open()) {
|
|
234
|
+
return env.Undefined();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (trx_count_.load() > 0) {
|
|
238
|
+
throw std::runtime_error("active transactions");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
auto* worker = new async_close(env, *this);
|
|
242
|
+
Napi::Promise promise = worker->GetPromise();
|
|
243
|
+
worker->Queue();
|
|
244
|
+
return promise;
|
|
245
|
+
} catch (const std::exception& e) {
|
|
246
|
+
unlock();
|
|
247
|
+
throw Napi::Error::New(env, e.what());
|
|
248
|
+
} catch (...) {
|
|
249
|
+
unlock();
|
|
250
|
+
throw;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return env.Undefined();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
Napi::Value envmou::close_sync(const Napi::CallbackInfo& info)
|
|
257
|
+
{
|
|
258
|
+
auto env = info.Env();
|
|
259
|
+
try {
|
|
260
|
+
lock_guard l(*this);
|
|
261
|
+
|
|
262
|
+
do_close();
|
|
263
|
+
|
|
264
|
+
} catch (const std::exception& e) {
|
|
265
|
+
throw Napi::Error::New(env, e.what());
|
|
266
|
+
}
|
|
267
|
+
return env.Undefined();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Napi::Value envmou::copy_to_sync(const Napi::CallbackInfo& info)
|
|
271
|
+
{
|
|
272
|
+
auto env = info.Env();
|
|
273
|
+
|
|
274
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
275
|
+
throw Napi::TypeError::New(env, "expected a string argument for the destination path");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
MDBX_copy_flags_t flags{MDBX_CP_COMPACT};
|
|
279
|
+
if ((info.Length() > 1) && info[1].IsNumber()) {
|
|
280
|
+
flags = static_cast<MDBX_copy_flags_t>(info[1].As<Napi::Number>().Uint32Value());
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
auto dest_path = info[0].As<Napi::String>().Utf8Value();
|
|
285
|
+
|
|
286
|
+
lock_guard l(*this);
|
|
287
|
+
|
|
288
|
+
check();
|
|
289
|
+
|
|
290
|
+
auto rc = mdbx_env_copy(*this, dest_path.c_str(), flags);
|
|
291
|
+
if (rc != MDBX_SUCCESS) {
|
|
292
|
+
throw Napi::Error::New(env, mdbx_strerror(rc));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
} catch (const std::exception& e) {
|
|
296
|
+
throw Napi::Error::New(env, e.what());
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return env.Undefined();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
Napi::Value envmou::copy_to(const Napi::CallbackInfo& info)
|
|
303
|
+
{
|
|
304
|
+
Napi::Env env = info.Env();
|
|
305
|
+
|
|
306
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
307
|
+
throw Napi::TypeError::New(env, "copyTo(path: string[, flags?: number]) -> Promise<void>");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
MDBX_copy_flags_t flags{MDBX_CP_COMPACT};
|
|
311
|
+
if (info.Length() > 1 && info[1].IsNumber()) {
|
|
312
|
+
flags = static_cast<MDBX_copy_flags_t>(info[1].As<Napi::Number>().Uint32Value());
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
auto dest = info[0].As<Napi::String>().Utf8Value();
|
|
317
|
+
|
|
318
|
+
if (!try_lock()) {
|
|
319
|
+
throw std::runtime_error("in progress");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
check();
|
|
323
|
+
|
|
324
|
+
auto* worker = new mdbxmou::async_copy(env, *this, std::move(dest), flags);
|
|
325
|
+
Napi::Promise promise = worker->GetPromise();
|
|
326
|
+
worker->Queue();
|
|
327
|
+
return promise;
|
|
328
|
+
} catch (const std::exception& e) {
|
|
329
|
+
throw Napi::Error::New(env, e.what());
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return env.Undefined();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
Napi::Value envmou::get_version(const Napi::CallbackInfo& info)
|
|
336
|
+
{
|
|
337
|
+
std::string version = "mdbx v" + std::to_string(MDBX_VERSION_MAJOR);
|
|
338
|
+
version += "." + std::to_string(MDBX_VERSION_MINOR);
|
|
339
|
+
return Napi::Value::From(info.Env(), version);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
Napi::Value envmou::start_transaction(const Napi::CallbackInfo& info, txn_mode mode)
|
|
343
|
+
{
|
|
344
|
+
auto env = info.Env();
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
lock_guard l(*this);
|
|
348
|
+
|
|
349
|
+
check();
|
|
350
|
+
|
|
351
|
+
MDBX_txn* txn;
|
|
352
|
+
auto rc = mdbx_txn_begin(*this, nullptr, mode, &txn);
|
|
353
|
+
if (rc != MDBX_SUCCESS) {
|
|
354
|
+
throw Napi::Error::New(env, std::string("Env: ") + mdbx_strerror(rc));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Создаем новый объект txnmou
|
|
358
|
+
auto txn_obj = txnmou::ctor.New({});
|
|
359
|
+
auto txn_wrapper = txnmou::Unwrap(txn_obj);
|
|
360
|
+
txn_wrapper->attach(*this, txn, mode, nullptr);
|
|
361
|
+
|
|
362
|
+
return txn_obj;
|
|
363
|
+
} catch (const std::exception& e) {
|
|
364
|
+
throw Napi::Error::New(env, e.what());
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
Napi::Value envmou::query(const Napi::CallbackInfo& info)
|
|
369
|
+
{
|
|
370
|
+
Napi::Env env = info.Env();
|
|
371
|
+
|
|
372
|
+
txn_mode mode{};
|
|
373
|
+
|
|
374
|
+
if (info.Length() < 1) {
|
|
375
|
+
throw Napi::TypeError::New(env, "expected array of requests");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (info.Length() > 1 || info[1].IsNumber()) {
|
|
379
|
+
mode = txn_mode::parse(info[1].As<Napi::Number>());
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try
|
|
383
|
+
{
|
|
384
|
+
lock_guard lock(*this);
|
|
385
|
+
|
|
386
|
+
check();
|
|
387
|
+
|
|
388
|
+
auto conf = get_env_userctx(*this);
|
|
389
|
+
|
|
390
|
+
auto arg0 = info[0];
|
|
391
|
+
query_request query = parse_query(mode, arg0);
|
|
392
|
+
auto* worker = new async_query(env, *this, mode,
|
|
393
|
+
std::move(query), arg0.IsObject());
|
|
394
|
+
auto promise = worker->GetPromise();
|
|
395
|
+
worker->Queue();
|
|
396
|
+
|
|
397
|
+
// Увеличиваем счетчик транзакций после успешного создания
|
|
398
|
+
++(*this);
|
|
399
|
+
|
|
400
|
+
return promise;
|
|
401
|
+
} catch (const std::exception& e) {
|
|
402
|
+
throw Napi::Error::New(env, e.what());
|
|
403
|
+
} catch (...) {
|
|
404
|
+
throw Napi::Error::New(env, "envmou::query");
|
|
405
|
+
}
|
|
406
|
+
return env.Undefined();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
Napi::Value envmou::keys(const Napi::CallbackInfo& info)
|
|
410
|
+
{
|
|
411
|
+
Napi::Env env = info.Env();
|
|
412
|
+
|
|
413
|
+
txn_mode mode{};
|
|
414
|
+
|
|
415
|
+
if (info.Length() < 1) {
|
|
416
|
+
throw Napi::TypeError::New(env,
|
|
417
|
+
"expected array of requests: [{ db: String, db_mode: Number, key_mode: Number, key_flag: Number, value_mode: Number, value_flag: Number }, ...]");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (info.Length() > 1 || info[1].IsNumber()) {
|
|
421
|
+
mode = txn_mode::parse(info[1].As<Napi::Number>());
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
try
|
|
425
|
+
{
|
|
426
|
+
lock_guard lock(*this);
|
|
427
|
+
|
|
428
|
+
check();
|
|
429
|
+
|
|
430
|
+
auto arg0 = info[0];
|
|
431
|
+
keys_request query = parse_keys(arg0);
|
|
432
|
+
|
|
433
|
+
auto* worker = new async_keys(env, *this, mode,
|
|
434
|
+
std::move(query), arg0.IsObject());
|
|
435
|
+
auto promise = worker->GetPromise();
|
|
436
|
+
worker->Queue();
|
|
437
|
+
|
|
438
|
+
// Увеличиваем счетчик транзакций после успешного создания
|
|
439
|
+
++(*this);
|
|
440
|
+
|
|
441
|
+
return promise;
|
|
442
|
+
} catch (const std::exception& e) {
|
|
443
|
+
throw Napi::Error::New(env, e.what());
|
|
444
|
+
} catch (...) {
|
|
445
|
+
throw Napi::Error::New(env, "envmou::keys");
|
|
446
|
+
}
|
|
447
|
+
return env.Undefined();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
} // namespace mdbxmou
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "txnmou.hpp"
|
|
4
|
+
#include <memory>
|
|
5
|
+
#include <atomic>
|
|
6
|
+
#include <mutex>
|
|
7
|
+
|
|
8
|
+
namespace mdbxmou {
|
|
9
|
+
|
|
10
|
+
// Forward declaration
|
|
11
|
+
class txnmou;
|
|
12
|
+
|
|
13
|
+
class envmou final
|
|
14
|
+
: public Napi::ObjectWrap<envmou>
|
|
15
|
+
{
|
|
16
|
+
static Napi::FunctionReference ctor;
|
|
17
|
+
static mdbx::env::geometry parse_geometry(const Napi::Value& obj);
|
|
18
|
+
static env_arg0 parse(const Napi::Value& obj);
|
|
19
|
+
|
|
20
|
+
struct free_env {
|
|
21
|
+
void operator()(MDBX_env* env) const {
|
|
22
|
+
auto rc = mdbx_env_close(env);
|
|
23
|
+
if (rc != MDBX_SUCCESS) {
|
|
24
|
+
fprintf(stderr, "mdbx_env_close %s\n", mdbx_strerror(rc));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
std::unique_ptr<MDBX_env, free_env> env_{};
|
|
30
|
+
std::atomic<std::size_t> trx_count_{};
|
|
31
|
+
env_arg0 arg0_{};
|
|
32
|
+
std::mutex lock_{};
|
|
33
|
+
|
|
34
|
+
bool is_open() const {
|
|
35
|
+
return env_ != nullptr;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Inline метод для проверки что база данных открыта
|
|
39
|
+
void check() const {
|
|
40
|
+
if (!is_open()) {
|
|
41
|
+
throw std::runtime_error("closed");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Общий метод для создания транзакций
|
|
46
|
+
Napi::Value start_transaction(const Napi::CallbackInfo& info, txn_mode mode);
|
|
47
|
+
|
|
48
|
+
public:
|
|
49
|
+
envmou(const Napi::CallbackInfo& i)
|
|
50
|
+
: ObjectWrap{i}
|
|
51
|
+
{ }
|
|
52
|
+
|
|
53
|
+
static MDBX_env* create_and_open(const env_arg0& arg0);
|
|
54
|
+
void attach(MDBX_env* env, const env_arg0& arg0);
|
|
55
|
+
|
|
56
|
+
operator MDBX_env*() const noexcept {
|
|
57
|
+
return env_.get();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static void init(const char *class_name, Napi::Env env, Napi::Object exports);
|
|
61
|
+
|
|
62
|
+
envmou& operator++() noexcept {
|
|
63
|
+
++trx_count_;
|
|
64
|
+
return *this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
envmou& operator--() noexcept {
|
|
68
|
+
--trx_count_;
|
|
69
|
+
return *this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Napi::Value open(const Napi::CallbackInfo&);
|
|
73
|
+
Napi::Value open_sync(const Napi::CallbackInfo&);
|
|
74
|
+
Napi::Value close(const Napi::CallbackInfo&);
|
|
75
|
+
Napi::Value close_sync(const Napi::CallbackInfo&);
|
|
76
|
+
Napi::Value get_version(const Napi::CallbackInfo&);
|
|
77
|
+
Napi::Value copy_to_sync(const Napi::CallbackInfo&);
|
|
78
|
+
Napi::Value copy_to(const Napi::CallbackInfo&);
|
|
79
|
+
// метод для групповых вставок или чтения
|
|
80
|
+
// внутри транзакция, получение db и чтение/запись
|
|
81
|
+
Napi::Value query(const Napi::CallbackInfo&);
|
|
82
|
+
Napi::Value keys(const Napi::CallbackInfo&);
|
|
83
|
+
|
|
84
|
+
// Методы для создания транзакций
|
|
85
|
+
Napi::Value start_read(const Napi::CallbackInfo& info) {
|
|
86
|
+
return start_transaction(info, {txn_mode::ro});
|
|
87
|
+
}
|
|
88
|
+
Napi::Value start_write(const Napi::CallbackInfo& info) {
|
|
89
|
+
return start_transaction(info, {});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
using lock_guard = std::lock_guard<envmou>;
|
|
93
|
+
// для защиты асинхронных операций
|
|
94
|
+
void lock() {
|
|
95
|
+
auto rc = lock_.try_lock();
|
|
96
|
+
if (!rc) {
|
|
97
|
+
throw std::runtime_error("operation in progress");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
void unlock() {
|
|
101
|
+
lock_.unlock();
|
|
102
|
+
}
|
|
103
|
+
bool try_lock() {
|
|
104
|
+
bool locked = lock_.try_lock();
|
|
105
|
+
return locked;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
void do_close() {
|
|
109
|
+
if (trx_count_.load() > 0) {
|
|
110
|
+
throw std::runtime_error("transaction in progress");
|
|
111
|
+
}
|
|
112
|
+
env_.reset();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
} // namespace mdbxmou
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#include "cursormou.hpp"
|
|
2
|
+
#include "dbimou.hpp"
|
|
3
|
+
#include "txnmou.hpp"
|
|
4
|
+
|
|
5
|
+
namespace mdbxmou {
|
|
6
|
+
|
|
7
|
+
Napi::FunctionReference cursormou::ctor{};
|
|
8
|
+
|
|
9
|
+
void cursormou::init(const char *class_name, Napi::Env env)
|
|
10
|
+
{
|
|
11
|
+
auto func = DefineClass(env, class_name, {
|
|
12
|
+
InstanceMethod("first", &cursormou::first),
|
|
13
|
+
InstanceMethod("last", &cursormou::last),
|
|
14
|
+
InstanceMethod("next", &cursormou::next),
|
|
15
|
+
InstanceMethod("prev", &cursormou::prev),
|
|
16
|
+
InstanceMethod("seek", &cursormou::seek),
|
|
17
|
+
InstanceMethod("seekGE", &cursormou::seek_ge),
|
|
18
|
+
InstanceMethod("current", &cursormou::current),
|
|
19
|
+
InstanceMethod("put", &cursormou::put),
|
|
20
|
+
InstanceMethod("del", &cursormou::del),
|
|
21
|
+
InstanceMethod("forEach", &cursormou::for_each),
|
|
22
|
+
InstanceMethod("close", &cursormou::close),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
ctor = Napi::Persistent(func);
|
|
26
|
+
ctor.SuppressDestruct();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
void cursormou::do_close() noexcept
|
|
30
|
+
{
|
|
31
|
+
if (cursor_) {
|
|
32
|
+
::mdbx_cursor_close(std::exchange(cursor_, nullptr));
|
|
33
|
+
if (txn_) {
|
|
34
|
+
--(*txn_);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
cursormou::~cursormou()
|
|
40
|
+
{
|
|
41
|
+
do_close();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void cursormou::attach(txnmou& txn, dbimou& dbi, MDBX_cursor* cursor)
|
|
45
|
+
{
|
|
46
|
+
txn_ = &txn;
|
|
47
|
+
dbi_ = &dbi;
|
|
48
|
+
cursor_ = cursor;
|
|
49
|
+
++(*txn_);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Внутренний хелпер для навигации
|
|
53
|
+
Napi::Value cursormou::move(const Napi::Env& env, MDBX_cursor_op op)
|
|
54
|
+
{
|
|
55
|
+
if (!cursor_) {
|
|
56
|
+
throw Napi::Error::New(env, "cursor closed");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
keymou key{};
|
|
60
|
+
valuemou val{};
|
|
61
|
+
auto rc = mdbx_cursor_get(cursor_, key, val, op);
|
|
62
|
+
if (rc == MDBX_NOTFOUND) {
|
|
63
|
+
return env.Undefined();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (rc != MDBX_SUCCESS) {
|
|
67
|
+
throw Napi::Error::New(env, mdbx_strerror(rc));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Возвращаем {key, value}
|
|
71
|
+
auto result = Napi::Object::New(env);
|
|
72
|
+
|
|
73
|
+
auto key_mode = dbi_->get_key_mode();
|
|
74
|
+
auto key_flag = dbi_->get_key_flag();
|
|
75
|
+
auto value_flag = dbi_->get_value_flag();
|
|
76
|
+
|
|
77
|
+
// Ключ
|
|
78
|
+
if (key_mode.val & key_mode::ordinal) {
|
|
79
|
+
if (key_flag.val & base_flag::bigint) {
|
|
80
|
+
result.Set("key", key.to_bigint(env));
|
|
81
|
+
} else {
|
|
82
|
+
result.Set("key", key.to_number(env));
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
result.Set("key", key.to_string(env));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Значение
|
|
89
|
+
if (value_flag.val & base_flag::string) {
|
|
90
|
+
result.Set("value", val.to_string(env));
|
|
91
|
+
} else {
|
|
92
|
+
result.Set("value", val.to_buffer(env));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Napi::Value cursormou::first(const Napi::CallbackInfo& info) {
|
|
99
|
+
return move(info.Env(), MDBX_FIRST);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Napi::Value cursormou::last(const Napi::CallbackInfo& info) {
|
|
103
|
+
return move(info.Env(), MDBX_LAST);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Napi::Value cursormou::next(const Napi::CallbackInfo& info) {
|
|
107
|
+
return move(info.Env(), MDBX_NEXT);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Napi::Value cursormou::prev(const Napi::CallbackInfo& info) {
|
|
111
|
+
return move(info.Env(), MDBX_PREV);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
Napi::Value cursormou::current(const Napi::CallbackInfo& info) {
|
|
115
|
+
return move(info.Env(), MDBX_GET_CURRENT);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Хелпер для поиска
|
|
119
|
+
Napi::Value cursormou::seek_impl(const Napi::CallbackInfo& info, MDBX_cursor_op op)
|
|
120
|
+
{
|
|
121
|
+
Napi::Env env = info.Env();
|
|
122
|
+
|
|
123
|
+
if (!cursor_) {
|
|
124
|
+
throw Napi::Error::New(env, "cursor closed");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (info.Length() < 1) {
|
|
128
|
+
throw Napi::Error::New(env, "key required");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
auto key_mode = dbi_->get_key_mode();
|
|
132
|
+
|
|
133
|
+
keymou key = (key_mode.val & key_mode::ordinal) ?
|
|
134
|
+
keymou::from(info[0], env, key_num_) :
|
|
135
|
+
keymou::from(info[0], env, key_buf_);
|
|
136
|
+
|
|
137
|
+
valuemou val{};
|
|
138
|
+
auto rc = mdbx_cursor_get(cursor_, key, val, op);
|
|
139
|
+
if (MDBX_NOTFOUND == rc) {
|
|
140
|
+
return env.Undefined();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (MDBX_SUCCESS != rc) {
|
|
144
|
+
throw Napi::Error::New(env, mdbx_strerror(rc));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Возвращаем {key, value}
|
|
148
|
+
auto result = Napi::Object::New(env);
|
|
149
|
+
|
|
150
|
+
auto key_flag = dbi_->get_key_flag();
|
|
151
|
+
auto value_flag = dbi_->get_value_flag();
|
|
152
|
+
|
|
153
|
+
// Ключ
|
|
154
|
+
if (key_mode.val & key_mode::ordinal) {
|
|
155
|
+
if (key_flag.val & base_flag::bigint) {
|
|
156
|
+
result.Set("key", key.to_bigint(env));
|
|
157
|
+
} else {
|
|
158
|
+
result.Set("key", key.to_number(env));
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
result.Set("key", key.to_string(env));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Значение
|
|
165
|
+
if (value_flag.val & base_flag::string) {
|
|
166
|
+
result.Set("value", val.to_string(env));
|
|
167
|
+
} else {
|
|
168
|
+
result.Set("value", val.to_buffer(env));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Napi::Value cursormou::seek(const Napi::CallbackInfo& info) {
|
|
175
|
+
return seek_impl(info, MDBX_SET_KEY);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Napi::Value cursormou::seek_ge(const Napi::CallbackInfo& info) {
|
|
179
|
+
return seek_impl(info, MDBX_SET_RANGE);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
Napi::Value cursormou::put(const Napi::CallbackInfo& info) {
|
|
183
|
+
Napi::Env env = info.Env();
|
|
184
|
+
|
|
185
|
+
if (!cursor_) {
|
|
186
|
+
throw Napi::Error::New(env, "cursor closed");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (info.Length() < 2) {
|
|
190
|
+
throw Napi::Error::New(env, "key and value required");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
auto key_mode = dbi_->get_key_mode();
|
|
194
|
+
|
|
195
|
+
keymou key = (key_mode.val & key_mode::ordinal) ?
|
|
196
|
+
keymou::from(info[0], env, key_num_) :
|
|
197
|
+
keymou::from(info[0], env, key_buf_);
|
|
198
|
+
|
|
199
|
+
valuemou val = valuemou::from(info[1], env, val_buf_);
|
|
200
|
+
|
|
201
|
+
// Опциональный флаг put_mode (по умолчанию MDBX_UPSERT)
|
|
202
|
+
MDBX_put_flags_t flags = MDBX_UPSERT;
|
|
203
|
+
if (info.Length() > 2 && info[2].IsNumber()) {
|
|
204
|
+
flags = static_cast<MDBX_put_flags_t>(info[2].As<Napi::Number>().Int32Value());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
auto rc = mdbx_cursor_put(cursor_, key, val, flags);
|
|
208
|
+
if (MDBX_SUCCESS != rc) {
|
|
209
|
+
throw Napi::Error::New(env, mdbx_strerror(rc));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return env.Undefined();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
Napi::Value cursormou::del(const Napi::CallbackInfo& info) {
|
|
216
|
+
Napi::Env env = info.Env();
|
|
217
|
+
|
|
218
|
+
if (!cursor_) {
|
|
219
|
+
throw Napi::Error::New(env, "cursor closed");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Опциональный флаг (MDBX_NODUPDATA для удаления только текущего значения в multi-value)
|
|
223
|
+
MDBX_put_flags_t flags = MDBX_CURRENT;
|
|
224
|
+
if (info.Length() > 0 && info[0].IsNumber()) {
|
|
225
|
+
flags = static_cast<MDBX_put_flags_t>(info[0].As<Napi::Number>().Int32Value());
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
auto rc = mdbx_cursor_del(cursor_, flags);
|
|
229
|
+
if (MDBX_NOTFOUND == rc) {
|
|
230
|
+
return Napi::Boolean::New(env, false);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (MDBX_SUCCESS != rc) {
|
|
234
|
+
throw Napi::Error::New(env, mdbx_strerror(rc));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return Napi::Boolean::New(env, true);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// forEach(callback, backward = false)
|
|
241
|
+
// callback({key, value}) => true продолжить, false/undefined остановить
|
|
242
|
+
Napi::Value cursormou::for_each(const Napi::CallbackInfo& info) {
|
|
243
|
+
Napi::Env env = info.Env();
|
|
244
|
+
|
|
245
|
+
if (!cursor_) {
|
|
246
|
+
throw Napi::Error::New(env, "cursor closed");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (info.Length() < 1 || !info[0].IsFunction()) {
|
|
250
|
+
throw Napi::TypeError::New(env, "forEach: callback function required");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
auto callback = info[0].As<Napi::Function>();
|
|
254
|
+
bool backward = info.Length() > 1 && info[1].ToBoolean().Value();
|
|
255
|
+
|
|
256
|
+
auto key_mode = dbi_->get_key_mode();
|
|
257
|
+
auto key_flag = dbi_->get_key_flag();
|
|
258
|
+
auto value_flag = dbi_->get_value_flag();
|
|
259
|
+
|
|
260
|
+
MDBX_cursor_op start_op = backward ? MDBX_LAST : MDBX_FIRST;
|
|
261
|
+
MDBX_cursor_op move_op = backward ? MDBX_PREV : MDBX_NEXT;
|
|
262
|
+
|
|
263
|
+
keymou key{};
|
|
264
|
+
valuemou val{};
|
|
265
|
+
// Первое позиционирование
|
|
266
|
+
auto rc = mdbx_cursor_get(cursor_, key, val, start_op);
|
|
267
|
+
while (MDBX_SUCCESS == rc)
|
|
268
|
+
{
|
|
269
|
+
auto result = Napi::Object::New(env);
|
|
270
|
+
|
|
271
|
+
// Ключ
|
|
272
|
+
if (key_mode.val & key_mode::ordinal) {
|
|
273
|
+
if (key_flag.val & base_flag::bigint) {
|
|
274
|
+
result.Set("key", key.to_bigint(env));
|
|
275
|
+
} else {
|
|
276
|
+
result.Set("key", key.to_number(env));
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
result.Set("key", key.to_string(env));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Значение
|
|
283
|
+
if (value_flag.val & base_flag::string) {
|
|
284
|
+
result.Set("value", val.to_string(env));
|
|
285
|
+
} else {
|
|
286
|
+
result.Set("value", val.to_buffer(env));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Вызов callback
|
|
290
|
+
auto ret = callback.Call({result});
|
|
291
|
+
|
|
292
|
+
// Если вернул false или undefined - остановить
|
|
293
|
+
if (ret.IsBoolean() && !ret.As<Napi::Boolean>().Value()) {
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
if (ret.IsUndefined() || ret.IsNull()) {
|
|
297
|
+
// undefined/null = продолжаем (как forEach в JS)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Следующий элемент
|
|
301
|
+
rc = mdbx_cursor_get(cursor_, key, val, move_op);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (rc != MDBX_SUCCESS && rc != MDBX_NOTFOUND) {
|
|
305
|
+
throw Napi::Error::New(env, mdbx_strerror(rc));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return env.Undefined();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
Napi::Value cursormou::close(const Napi::CallbackInfo& info) {
|
|
312
|
+
do_close();
|
|
313
|
+
return info.Env().Undefined();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
} // namespace mdbxmou
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "typemou.hpp"
|
|
4
|
+
#include "valuemou.hpp"
|
|
5
|
+
|
|
6
|
+
namespace mdbxmou {
|
|
7
|
+
|
|
8
|
+
class dbimou;
|
|
9
|
+
class txnmou;
|
|
10
|
+
|
|
11
|
+
class cursormou final
|
|
12
|
+
: public Napi::ObjectWrap<cursormou>
|
|
13
|
+
{
|
|
14
|
+
private:
|
|
15
|
+
txnmou* txn_{nullptr};
|
|
16
|
+
dbimou* dbi_{nullptr};
|
|
17
|
+
MDBX_cursor* cursor_{nullptr};
|
|
18
|
+
|
|
19
|
+
// Буферы для ключей/значений
|
|
20
|
+
buffer_type key_buf_{};
|
|
21
|
+
buffer_type val_buf_{};
|
|
22
|
+
std::uint64_t key_num_{};
|
|
23
|
+
|
|
24
|
+
// Внутренний хелпер для навигации
|
|
25
|
+
Napi::Value move(const Napi::Env& env, MDBX_cursor_op op);
|
|
26
|
+
|
|
27
|
+
// Хелпер для поиска
|
|
28
|
+
Napi::Value seek_impl(const Napi::CallbackInfo& info, MDBX_cursor_op op);
|
|
29
|
+
|
|
30
|
+
// Закрытие курсора
|
|
31
|
+
void do_close() noexcept;
|
|
32
|
+
|
|
33
|
+
public:
|
|
34
|
+
static Napi::FunctionReference ctor;
|
|
35
|
+
static void init(const char *class_name, Napi::Env env);
|
|
36
|
+
|
|
37
|
+
cursormou(const Napi::CallbackInfo& info)
|
|
38
|
+
: Napi::ObjectWrap<cursormou>(info)
|
|
39
|
+
{ }
|
|
40
|
+
|
|
41
|
+
~cursormou();
|
|
42
|
+
|
|
43
|
+
// Навигация
|
|
44
|
+
Napi::Value first(const Napi::CallbackInfo&);
|
|
45
|
+
Napi::Value last(const Napi::CallbackInfo&);
|
|
46
|
+
Napi::Value next(const Napi::CallbackInfo&);
|
|
47
|
+
Napi::Value prev(const Napi::CallbackInfo&);
|
|
48
|
+
|
|
49
|
+
// Поиск
|
|
50
|
+
Napi::Value seek(const Napi::CallbackInfo&); // exact match
|
|
51
|
+
Napi::Value seek_ge(const Napi::CallbackInfo&); // >= key (lower_bound)
|
|
52
|
+
|
|
53
|
+
// Текущая позиция
|
|
54
|
+
Napi::Value current(const Napi::CallbackInfo&);
|
|
55
|
+
|
|
56
|
+
// Модификация
|
|
57
|
+
Napi::Value put(const Napi::CallbackInfo&);
|
|
58
|
+
Napi::Value del(const Napi::CallbackInfo&);
|
|
59
|
+
|
|
60
|
+
// Итерация
|
|
61
|
+
Napi::Value for_each(const Napi::CallbackInfo&);
|
|
62
|
+
|
|
63
|
+
// Закрытие
|
|
64
|
+
Napi::Value close(const Napi::CallbackInfo&);
|
|
65
|
+
|
|
66
|
+
void attach(txnmou& txn, dbimou& dbi, MDBX_cursor* cursor);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
} // namespace mdbxmou
|
package/src/modulemou.cpp
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "envmou.hpp"
|
|
2
|
+
#include "cursormou.hpp"
|
|
2
3
|
|
|
3
4
|
using namespace Napi;
|
|
4
5
|
|
|
@@ -106,6 +107,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports)
|
|
|
106
107
|
mdbxmou::envmou::init("MDBX_Env", env, exports);
|
|
107
108
|
mdbxmou::txnmou::init("MDBX_Txn", env);
|
|
108
109
|
mdbxmou::dbimou::init("MDBX_Dbi", env);
|
|
110
|
+
mdbxmou::cursormou::init("MDBX_Cursor", env);
|
|
109
111
|
|
|
110
112
|
return exports;
|
|
111
113
|
}
|
package/src/txnmou.cpp
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#include "txnmou.hpp"
|
|
2
2
|
#include "envmou.hpp"
|
|
3
|
+
#include "cursormou.hpp"
|
|
3
4
|
|
|
4
5
|
namespace mdbxmou {
|
|
5
6
|
|
|
@@ -11,6 +12,7 @@ void txnmou::init(const char *class_name, Napi::Env env) {
|
|
|
11
12
|
InstanceMethod("abort", &txnmou::abort),
|
|
12
13
|
InstanceMethod("openMap", &txnmou::open_map),
|
|
13
14
|
InstanceMethod("createMap", &txnmou::create_map),
|
|
15
|
+
InstanceMethod("openCursor", &txnmou::open_cursor),
|
|
14
16
|
InstanceMethod("isActive", &txnmou::is_active),
|
|
15
17
|
});
|
|
16
18
|
|
|
@@ -25,6 +27,11 @@ Napi::Value txnmou::commit(const Napi::CallbackInfo& info) {
|
|
|
25
27
|
throw Napi::Error::New(env, "txn already completed");
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
if (cursor_count_ > 0) {
|
|
31
|
+
throw Napi::Error::New(env,
|
|
32
|
+
"txn commit: " + std::to_string(cursor_count_) + " cursor(s) still open");
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
dec_counter();
|
|
29
36
|
auto rc = mdbx_txn_commit(txn_.release());
|
|
30
37
|
if (rc != MDBX_SUCCESS) {
|
|
@@ -41,6 +48,11 @@ Napi::Value txnmou::abort(const Napi::CallbackInfo& info) {
|
|
|
41
48
|
throw Napi::Error::New(env, "txn already completed");
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
if (cursor_count_ > 0) {
|
|
52
|
+
throw Napi::Error::New(env,
|
|
53
|
+
"txn abort: " + std::to_string(cursor_count_) + " cursor(s) still open");
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
dec_counter();
|
|
45
57
|
auto rc = mdbx_txn_abort(txn_.release());
|
|
46
58
|
if (rc != MDBX_SUCCESS) {
|
|
@@ -124,6 +136,37 @@ Napi::Value txnmou::get_dbi(const Napi::CallbackInfo& info, db_mode db_mode)
|
|
|
124
136
|
return obj;
|
|
125
137
|
}
|
|
126
138
|
|
|
139
|
+
Napi::Value txnmou::open_cursor(const Napi::CallbackInfo& info) {
|
|
140
|
+
Napi::Env env = info.Env();
|
|
141
|
+
|
|
142
|
+
if (!txn_) {
|
|
143
|
+
throw Napi::Error::New(env, "txn not active");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
147
|
+
throw Napi::Error::New(env, "dbi required");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
auto arg0 = info[0].As<Napi::Object>();
|
|
151
|
+
if (!arg0.InstanceOf(dbimou::ctor.Value())) {
|
|
152
|
+
throw Napi::TypeError::New(env, "openCursor: first argument must be MDBX_Dbi instance");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
auto* dbi = dbimou::Unwrap(arg0);
|
|
156
|
+
|
|
157
|
+
MDBX_cursor* cursor{};
|
|
158
|
+
int rc = mdbx_cursor_open(txn_.get(), dbi->get_id(), &cursor);
|
|
159
|
+
|
|
160
|
+
if (rc != MDBX_SUCCESS) {
|
|
161
|
+
throw Napi::Error::New(env, std::string("mdbx_cursor_open: ") + mdbx_strerror(rc));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
auto obj = cursormou::ctor.New({});
|
|
165
|
+
auto ptr = cursormou::Unwrap(obj);
|
|
166
|
+
ptr->attach(*this, *dbi, cursor);
|
|
167
|
+
return obj;
|
|
168
|
+
}
|
|
169
|
+
|
|
127
170
|
Napi::Value txnmou::is_active(const Napi::CallbackInfo& info) {
|
|
128
171
|
Napi::Env env = info.Env();
|
|
129
172
|
return Napi::Boolean::New(env, txn_ != nullptr);
|
package/src/txnmou.hpp
CHANGED
|
@@ -22,6 +22,7 @@ private:
|
|
|
22
22
|
|
|
23
23
|
std::unique_ptr<MDBX_txn, free_txn> txn_{};
|
|
24
24
|
txn_mode mode_{};
|
|
25
|
+
std::size_t cursor_count_{};
|
|
25
26
|
|
|
26
27
|
// Уменьшает счетчик транзакций
|
|
27
28
|
void dec_counter() noexcept;
|
|
@@ -51,11 +52,18 @@ public:
|
|
|
51
52
|
Napi::Value create_map(const Napi::CallbackInfo& info) {
|
|
52
53
|
return get_dbi(info, {db_mode::create});
|
|
53
54
|
}
|
|
55
|
+
|
|
56
|
+
Napi::Value open_cursor(const Napi::CallbackInfo&);
|
|
54
57
|
|
|
55
58
|
operator MDBX_txn*() noexcept {
|
|
56
59
|
return txn_.get();
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
// Cursor counting
|
|
63
|
+
txnmou& operator++() noexcept { ++cursor_count_; return *this; }
|
|
64
|
+
txnmou& operator--() noexcept { --cursor_count_; return *this; }
|
|
65
|
+
std::size_t cursor_count() const noexcept { return cursor_count_; }
|
|
66
|
+
|
|
59
67
|
Napi::Value is_active(const Napi::CallbackInfo&);
|
|
60
68
|
|
|
61
69
|
void attach(envmou& env, MDBX_txn* txn, txn_mode mode);
|
package/src/valuemou.hpp
CHANGED
|
@@ -46,10 +46,10 @@ struct valuemou
|
|
|
46
46
|
throw Napi::Error::New(env, "napi_get_value_string_utf8 length");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
mem.
|
|
49
|
+
mem.resize(length + 1);
|
|
50
50
|
|
|
51
51
|
status = napi_get_value_string_utf8(
|
|
52
|
-
env, arg0, mem.data(), mem.
|
|
52
|
+
env, arg0, mem.data(), mem.size(), nullptr);
|
|
53
53
|
if (status != napi_ok) {
|
|
54
54
|
throw Napi::Error::New(env, "napi_get_value_string_utf8 copyout");
|
|
55
55
|
}
|