Skip to content

Commit 81f7d55

Browse files
authored
Merge pull request #79 from HarperDB/bug-fixes
Bug fixes
2 parents b818479 + 28d50e4 commit 81f7d55

13 files changed

Lines changed: 254 additions & 91 deletions

README.md

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,17 @@ await db.put('foo', 'bar');
302302
console.log(await db.get('foo'));
303303
```
304304

305+
> [!IMPORTANT]
306+
> If your custom store overrides `putSync()` without calling `super.putSync()`
307+
> and it performs its own `this.encodeKey(key)`, then you MUST encode the VALUE
308+
> before you encode the KEY.
309+
>
310+
> Keys are encoded into a shared buffer. If the database is opened with the
311+
> `sharedStructuresKey` option, encoding the value will load and save the
312+
> structures which encodes the `sharedStructuresKey` overwriting the encoded
313+
> key in the shared key buffer, so it's ultra important that you encode the
314+
> value first!
315+
305316
## Interfaces
306317

307318
### `RocksDBOptions`
@@ -343,19 +354,19 @@ This package requires Node.js 18 or higher, pnpm, and a C++ compiler.
343354
> pnpm config set stream true
344355
> ```
345356
346-
### Building the Native Binding
347-
348-
To compile everything including the native binding and the TypeScript source, run:
349-
350-
```bash
351-
pnpm build
352-
```
357+
### Building
353358
354-
To configure and compile only the native binding, run:
359+
There are two things being built: the native binding and the TypeScript code.
360+
Each of those can be built to be debug friendly.
355361
356-
```bash
357-
pnpm rebuild
358-
```
362+
| Description | Command |
363+
| --- | --- |
364+
| Production build (minified + native binding) | `pnpm build` |
365+
| TypeScript only (minified) | `pnpm build:bundle` |
366+
| TypeScript only (unminified) | `pnpm build:debug` |
367+
| Native binding only (prod) | `pnpm rebuild` |
368+
| Native binding only (with debug logging) | `pnpm rebuild:debug` |
369+
| Debug build everything | `pnpm build:debug && pnpm rebuild:debug` |
359370
360371
When building the native binding, it will download the appropriate prebuilt
361372
RocksDB library for your platform and architecture from the

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"scripts": {
1717
"build": "pnpm build:bundle && pnpm rebuild",
1818
"build:bundle": "rimraf dist && rollup -c rollup.config.ts --configPlugin typescript && pnpm build:types",
19-
"build:debug": "pnpm build:bundle && pnpm rebuild:debug",
19+
"build:debug": "SKIP_MINIFY=1 pnpm build:bundle",
2020
"build:types": "pnpm build:types:temp && pnpm build:types:roll && pnpm build:types:check",
2121
"build:types:temp": "tsc --declaration --emitDeclarationOnly --outDir temp --project tsconfig.build.json",
2222
"build:types:roll": "rollup --config rollup.dts.config.ts --configPlugin typescript && rimraf temp",

rollup.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function generateConfig(format: 'es' | 'cjs') {
2525
entryFileNames: format === 'es' ? 'index.js' : 'index.cjs'
2626
},
2727
plugins: [
28-
esbuildMinifyPlugin({
28+
process.env.SKIP_MINIFY ? null : esbuildMinifyPlugin({
2929
minify: true,
3030
minifySyntax: true
3131
}),

src/binding/db_registry.cpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,23 @@ std::unique_ptr<DBHandle> DBRegistry::OpenDB(const std::string& path, const DBOp
6262
}
6363

6464
DEBUG_LOG("%p DBRegistry::OpenDB Database %s already open\n", instance.get(), path.c_str())
65+
DEBUG_LOG("%p DBRegistry::OpenDB Checking for column family %s\n", instance.get(), name.c_str())
6566

6667
dbExists = true;
6768

6869
// manually copy the columns because we don't know which ones are valid
6970
bool columnExists = false;
7071
for (auto& column : descriptor->columns) {
71-
std::shared_ptr<rocksdb::ColumnFamilyHandle> existingColumn = column.second;
72-
if (existingColumn) {
73-
columns[column.first] = existingColumn;
74-
if (column.first == name) {
75-
columnExists = true;
76-
}
72+
columns[column.first] = column.second;
73+
if (column.first == name) {
74+
DEBUG_LOG("%p DBRegistry::OpenDB Column family %s already exists\n", instance.get(), name.c_str())
75+
columnExists = true;
7776
}
7877
}
7978
if (!columnExists) {
79+
DEBUG_LOG("DBRegistry::OpenDB Creating column family \"%s\"\n", name.c_str())
8080
columns[name] = createColumn(descriptor->db, name);
81+
descriptor->columns[name] = columns[name];
8182
}
8283
}
8384
}

src/binding/transaction.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
#include <sstream>
2+
#include <thread>
13
#include "database.h"
24
#include "db_handle.h"
35
#include "db_iterator.h"
46
#include "macros.h"
57
#include "transaction.h"
68
#include "transaction_handle.h"
79
#include "util.h"
8-
#include <sstream>
910

1011
#define UNWRAP_TRANSACTION_HANDLE(fnName) \
1112
std::shared_ptr<TransactionHandle>* txnHandle = nullptr; \
@@ -315,6 +316,19 @@ napi_value Transaction::PutSync(napi_env env, napi_callback_info info) {
315316
rocksdb::Slice keySlice(key + keyStart, keyEnd - keyStart);
316317
rocksdb::Slice valueSlice(value + valueStart, valueEnd - valueStart);
317318

319+
#ifdef DEBUG
320+
fprintf(stderr, "[%04zu] Transaction::PutSync() Key:", std::hash<std::thread::id>{}(std::this_thread::get_id()) % 10000);
321+
for (size_t i = 0; i < keySlice.size(); i++) {
322+
fprintf(stderr, " %02x", (unsigned char)keySlice.data()[i]);
323+
}
324+
fprintf(stderr, "\n");
325+
fprintf(stderr, "[%04zu] Transaction::PutSync() Value:", std::hash<std::thread::id>{}(std::this_thread::get_id()) % 10000);
326+
for (size_t i = 0; i < valueSlice.size(); i++) {
327+
fprintf(stderr, " %02x", (unsigned char)valueSlice.data()[i]);
328+
}
329+
fprintf(stderr, "\n");
330+
#endif
331+
318332
ROCKSDB_STATUS_THROWS_ERROR_LIKE((*txnHandle)->putSync(keySlice, valueSlice), "Transaction put failed")
319333

320334
NAPI_RETURN_UNDEFINED()

src/database.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { Store, type StoreOptions } from './store.js';
44
import { config, type TransactionOptions, type RocksDatabaseConfig } from './load-binding.js';
55
import * as orderedBinary from 'ordered-binary';
66
import { Encoder as MsgpackEncoder } from 'msgpackr';
7-
import type { Key } from './encoding.js';
7+
import type { EncoderFunction, Key } from './encoding.js';
88

9-
interface RocksDatabaseOptions extends StoreOptions {
9+
export interface RocksDatabaseOptions extends StoreOptions {
1010
/**
1111
* The column family name.
1212
*
@@ -183,8 +183,11 @@ export class RocksDatabase extends DBI<DBITransactional> {
183183
* 4. encoding === `ordered-binary`
184184
* 5. encoder.writeKey()
185185
*/
186-
let EncoderClass = store.encoder?.Encoder;
187-
if (store.encoding === false || typeof EncoderClass === 'function') {
186+
let EncoderClass: EncoderFunction | undefined = store.encoder?.Encoder;
187+
if (store.encoding === false) {
188+
store.encoder = null;
189+
EncoderClass = undefined;
190+
} else if (typeof EncoderClass === 'function') {
188191
store.encoder = null;
189192
} else if (
190193
typeof store.encoder?.encode !== 'function' &&
@@ -206,8 +209,12 @@ export class RocksDatabase extends DBI<DBITransactional> {
206209
};
207210
opts.saveStructures = (structures: any, isCompatible: boolean | ((existingStructures: any) => boolean)) => {
208211
this.transactionSync((txn: Transaction) => {
209-
const existingStructuresBuffer = txn.getBinarySync(sharedStructuresKey);
210-
const existingStructures = existingStructuresBuffer && store.decoder?.decode ? store.decoder.decode(existingStructuresBuffer) : undefined;
212+
// note: we need to get a fresh copy of the shared structures,
213+
// so we don't want to use the transaction's getBinarySync()
214+
const existingStructuresBuffer = this.getBinarySync(sharedStructuresKey);
215+
const existingStructures = existingStructuresBuffer && store.decoder?.decode
216+
? store.decoder.decode(existingStructuresBuffer)
217+
: undefined;
211218
if (typeof isCompatible == 'function') {
212219
if (!isCompatible(existingStructures)) {
213220
return false;
@@ -228,7 +235,6 @@ export class RocksDatabase extends DBI<DBITransactional> {
228235
if (!store.decoder) {
229236
store.decoder = store.encoder;
230237
}
231-
store.decoderCopies = !store.encoder.needsStableBuffer;
232238
} else if (store.encoding === 'ordered-binary') {
233239
store.encoder = {
234240
readKey: orderedBinary.readKey,
@@ -246,6 +252,11 @@ export class RocksDatabase extends DBI<DBITransactional> {
246252
return store.encodeBuffer.subarray(0, bytesWritten);
247253
}
248254
};
255+
store.encoder.copyBuffers = true;
256+
}
257+
258+
if (store.decoder?.needsStableBuffer !== true) {
259+
store.decoderCopies = true;
249260
}
250261

251262
if (store.decoder?.readKey && !store.decoder.decode) {
@@ -261,11 +272,15 @@ export class RocksDatabase extends DBI<DBITransactional> {
261272
return this;
262273
}
263274

275+
get path() {
276+
return this.store.path;
277+
}
278+
264279
/**
265280
* Executes all operations in the callback as a single transaction.
266281
*
267282
* @param callback - A async function that receives the transaction as an argument.
268-
* @returns A promise that resolves when the transaction is committed or aborted.
283+
* @returns A promise that resolves the `callback` return value.
269284
*
270285
* @example
271286
* ```ts
@@ -275,7 +290,7 @@ export class RocksDatabase extends DBI<DBITransactional> {
275290
* });
276291
* ```
277292
*/
278-
async transaction(callback: (txn: Transaction) => Promise<any>, options?: TransactionOptions) {
293+
async transaction(callback: (txn: Transaction) => PromiseLike<any>, options?: TransactionOptions) {
279294
if (typeof callback !== 'function') {
280295
throw new TypeError('Callback must be a function');
281296
}
@@ -292,7 +307,24 @@ export class RocksDatabase extends DBI<DBITransactional> {
292307
}
293308
}
294309

295-
transactionSync(callback: (txn: Transaction) => void, options?: TransactionOptions) {
310+
/**
311+
* Executes all operations in the callback as a single transaction.
312+
*
313+
* @param callback - A function that receives the transaction as an
314+
* argument. If the callback return promise-like value, it is awaited
315+
* before committing the transaction. Otherwise, the callback is treated as
316+
* synchronous.
317+
* @returns The `callback` return value.
318+
*
319+
* @example
320+
* ```ts
321+
* const db = RocksDatabase.open('/path/to/database');
322+
* await db.transaction(async (txn) => {
323+
* await txn.put('key', 'value');
324+
* });
325+
* ```
326+
*/
327+
transactionSync<T>(callback: (txn: Transaction) => T | PromiseLike<T>, options?: TransactionOptions): T | PromiseLike<T> {
296328
if (typeof callback !== 'function') {
297329
throw new TypeError('Callback must be a function');
298330
}
@@ -301,6 +333,20 @@ export class RocksDatabase extends DBI<DBITransactional> {
301333

302334
try {
303335
const result = callback(txn);
336+
let committed = false;
337+
338+
// despite being 'sync', we need to support async operations
339+
if (result && typeof result === 'object' && 'then' in result && typeof result.then === 'function') {
340+
return result.then((value) => {
341+
if (committed) {
342+
throw new Error('Transaction already committed');
343+
}
344+
committed = true;
345+
txn.commitSync();
346+
return value as T;
347+
});
348+
}
349+
304350
txn.commitSync();
305351
return result;
306352
} catch (error) {

src/dbi.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { getTxnId, type GetOptions, type PutOptions, type Store } from './store.js';
21
import { when, withResolvers, type MaybePromise } from './util.js';
32
import { NativeDatabase, NativeTransaction } from './load-binding.js';
3+
import type { GetOptions, PutOptions, Store } from './store.js';
44
import type { Key } from './encoding.js';
55
import type { Transaction } from './transaction.js';
66

@@ -173,7 +173,17 @@ export class DBI<T extends DBITransactional | unknown = unknown> {
173173
if (this.store.decoderCopies) {
174174
return when(
175175
() => this.getBinaryFast(key, options),
176-
result => result === undefined ? undefined : this.store.decodeValue(result as Buffer)
176+
result => {
177+
if (result === undefined) {
178+
return undefined;
179+
}
180+
181+
if (options?.skipDecode) {
182+
return result;
183+
}
184+
185+
return this.store.decodeValue(result as Buffer);
186+
}
177187
);
178188
}
179189

@@ -242,7 +252,7 @@ export class DBI<T extends DBITransactional | unknown = unknown> {
242252
error = err;
243253
reject?.(err);
244254
},
245-
getTxnId(options)
255+
this.store.getTxnId(options)
246256
);
247257

248258
if (error) {
@@ -300,7 +310,7 @@ export class DBI<T extends DBITransactional | unknown = unknown> {
300310
error = err;
301311
reject?.(err);
302312
},
303-
getTxnId(options)
313+
this.store.getTxnId(options)
304314
);
305315

306316
if (error) {
@@ -327,10 +337,9 @@ export class DBI<T extends DBITransactional | unknown = unknown> {
327337

328338
return this.store.getSync(
329339
this.#context,
330-
this.store.encodeKey(key),
340+
key,
331341
options
332342
);
333-
// TODO: return UNMODIFIED if the value is not modified
334343
}
335344

336345
/**
@@ -356,17 +365,7 @@ export class DBI<T extends DBITransactional | unknown = unknown> {
356365
* ```
357366
*/
358367
getKeysCount(options?: RangeOptions & T): number {
359-
const startKey = options?.start ? this.store.encodeKey(options?.start) : undefined;
360-
const start = startKey ? Buffer.from(startKey.subarray(startKey.start, startKey.end)) : undefined;
361-
362-
const endKey = options?.end ? this.store.encodeKey(options.end) : undefined;
363-
const end = endKey ? Buffer.from(endKey.subarray(endKey.start, endKey.end)) : undefined;
364-
365-
return this.store.getCount(this.#context, {
366-
...options,
367-
start,
368-
end,
369-
}, getTxnId(options));
368+
return this.store.getCount(this.#context, options);
370369
}
371370

372371
/**
@@ -438,7 +437,7 @@ export class DBI<T extends DBITransactional | unknown = unknown> {
438437
* ```
439438
*/
440439
async remove(key: Key, options?: T): Promise<void> {
441-
return this.store.removeSync(this.#context, key, options);
440+
return this.store.removeSync(this.#context, key, options as DBITransactional);
442441
}
443442

444443
/**
@@ -455,6 +454,6 @@ export class DBI<T extends DBITransactional | unknown = unknown> {
455454
* ```
456455
*/
457456
removeSync(key: Key, options?: T): void {
458-
return this.store.removeSync(this.#context, key, options);
457+
return this.store.removeSync(this.#context, key, options as DBITransactional);
459458
}
460459
}

0 commit comments

Comments
 (0)