Skip to content

Commit d9b5184

Browse files
authored
Merge pull request #292 from HarperFast/get-db-property
Get db property
2 parents 30a5eef + b9ca327 commit d9b5184

File tree

8 files changed

+286
-3
lines changed

8 files changed

+286
-3
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,26 @@ await promise;
231231
console.log(db.getOldestSnapshotTimestamp()); // returns `0`, no snapshots
232232
```
233233

234+
### `db.getDBProperty(propertyName: string): string`
235+
Gets a RocksDB database property as a string.
236+
- `propertyName: string` The name of the property to retrieve (e.g., ) `'rocksdb.levelstats'`.
237+
```typescript
238+
const db = RocksDatabase.open('/path/to/database');
239+
const levelStats = db.getDBProperty('rocksdb.levelstats');
240+
const stats = db.getDBProperty('rocksdb.stats');
241+
```
242+
243+
244+
### `db.getDBIntProperty(propertyName: string): number`
245+
Gets a RocksDB database property as an integer.
246+
- `propertyName: string` The name of the property to retrieve (e.g., ) `'rocksdb.num-blob-files'`.
247+
```typescript
248+
const db = RocksDatabase.open('/path/to/database');
249+
const blobFiles = db.getDBIntProperty('rocksdb.num-blob-files');
250+
const numKeys = db.getDBIntProperty('rocksdb.estimate-num-keys');
251+
```
252+
253+
234254
### `db.getRange(options?: IteratorOptions): ExtendedIterable`
235255

236256
Retrieves a range of keys and their values. Supports both synchronous and

src/binding/database.cpp

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,75 @@ napi_value Database::GetOldestSnapshotTimestamp(napi_env env, napi_callback_info
504504
return result;
505505
}
506506

507+
/**
508+
* Gets a RocksDB database property as a string.
509+
*
510+
* @example
511+
* ```typescript
512+
* const db = NativeDatabase.open('path/to/db');
513+
* const levelStats = db.getDBProperty('rocksdb.levelstats');
514+
* ```
515+
*/
516+
napi_value Database::GetDBProperty(napi_env env, napi_callback_info info) {
517+
NAPI_METHOD_ARGV(1)
518+
UNWRAP_DB_HANDLE_AND_OPEN()
519+
520+
NAPI_GET_STRING(argv[0], propertyName, "Property name is required")
521+
522+
std::string value;
523+
bool success = (*dbHandle)->descriptor->db->GetProperty(
524+
(*dbHandle)->column.get(),
525+
propertyName,
526+
&value
527+
);
528+
529+
if (!success) {
530+
::napi_throw_error(env, nullptr, "Failed to get database property");
531+
NAPI_RETURN_UNDEFINED()
532+
}
533+
534+
napi_value result;
535+
NAPI_STATUS_THROWS(::napi_create_string_utf8(
536+
env,
537+
value.c_str(),
538+
value.length(),
539+
&result
540+
))
541+
return result;
542+
}
543+
544+
/**
545+
* Gets a RocksDB database property as an integer.
546+
*
547+
* @example
548+
* ```typescript
549+
* const db = NativeDatabase.open('path/to/db');
550+
* const blobFiles = db.getDBIntProperty('rocksdb.num-blob-files');
551+
* ```
552+
*/
553+
napi_value Database::GetDBIntProperty(napi_env env, napi_callback_info info) {
554+
NAPI_METHOD_ARGV(1)
555+
UNWRAP_DB_HANDLE_AND_OPEN()
556+
557+
NAPI_GET_STRING(argv[0], propertyName, "Property name is required")
558+
559+
uint64_t value = 0;
560+
bool success = (*dbHandle)->descriptor->db->GetIntProperty(
561+
(*dbHandle)->column.get(),
562+
propertyName,
563+
&value
564+
);
565+
566+
if (!success) {
567+
::napi_throw_error(env, nullptr, "Failed to get database integer property");
568+
NAPI_RETURN_UNDEFINED()
569+
}
570+
571+
napi_value result;
572+
NAPI_STATUS_THROWS(::napi_create_int64(env, value, &result))
573+
return result;
574+
}
575+
507576
/**
508577
* Gets a value from the RocksDB database.
509578
*
@@ -971,6 +1040,8 @@ void Database::Init(napi_env env, napi_value exports) {
9711040
{ "flushSync", nullptr, FlushSync, nullptr, nullptr, nullptr, napi_default, nullptr },
9721041
{ "get", nullptr, Get, nullptr, nullptr, nullptr, napi_default, nullptr },
9731042
{ "getCount", nullptr, GetCount, nullptr, nullptr, nullptr, napi_default, nullptr },
1043+
{ "getDBIntProperty", nullptr, GetDBIntProperty, nullptr, nullptr, nullptr, napi_default, nullptr },
1044+
{ "getDBProperty", nullptr, GetDBProperty, nullptr, nullptr, nullptr, napi_default, nullptr },
9741045
{ "getMonotonicTimestamp", nullptr, GetMonotonicTimestamp, nullptr, nullptr, nullptr, napi_default, nullptr },
9751046
{ "getOldestSnapshotTimestamp", nullptr, GetOldestSnapshotTimestamp, nullptr, nullptr, nullptr, napi_default, nullptr },
9761047
{ "getSync", nullptr, GetSync, nullptr, nullptr, nullptr, napi_default, nullptr },

src/binding/database.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ struct Database final {
4141
static napi_value FlushSync(napi_env env, napi_callback_info info);
4242
static napi_value Get(napi_env env, napi_callback_info info);
4343
static napi_value GetCount(napi_env env, napi_callback_info info);
44+
static napi_value GetDBIntProperty(napi_env env, napi_callback_info info);
45+
static napi_value GetDBProperty(napi_env env, napi_callback_info info);
4446
static napi_value GetMonotonicTimestamp(napi_env env, napi_callback_info info);
4547
static napi_value GetOldestSnapshotTimestamp(napi_env env, napi_callback_info info);
4648
static napi_value GetSync(napi_env env, napi_callback_info info);

src/binding/db_descriptor.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,12 @@ std::shared_ptr<DBDescriptor> DBDescriptor::open(const std::string& path, const
460460
dbOptions.IncreaseParallelism(options.parallelismThreads);
461461
dbOptions.table_factory.reset(rocksdb::NewBlockBasedTableFactory(tableOptions));
462462

463+
// Define base ColumnFamilyOptions that include blob settings
464+
rocksdb::ColumnFamilyOptions cfOptions;
465+
cfOptions.enable_blob_files = true;
466+
cfOptions.min_blob_size = 1024; // Ensure this matches your requirement
467+
cfOptions.enable_blob_garbage_collection = true;
468+
463469
// create a shared pointer to hold the weak descriptor reference for the event listener
464470
auto descriptorWeakPtr = std::make_shared<std::weak_ptr<DBDescriptor>>();
465471
auto eventListener = std::make_shared<TransactionLogEventListener>(descriptorWeakPtr);
@@ -476,13 +482,13 @@ std::shared_ptr<DBDescriptor> DBDescriptor::open(const std::string& path, const
476482
// database exists, use existing column families
477483
for (const auto& cfName : columnFamilyNames) {
478484
DEBUG_LOG("DBDescriptor::open Opening column family \"%s\"\n", cfName.c_str())
479-
cfDescriptors.emplace_back(cfName, rocksdb::ColumnFamilyOptions());
485+
cfDescriptors.emplace_back(cfName, cfOptions); // Use cfOptions here
480486
}
481487
} else {
482488
// database doesn't exist or no column families found, use default
483489
DEBUG_LOG("DBDescriptor::open Database doesn't exist or no column families found, using default\n")
484490
cfDescriptors = {
485-
rocksdb::ColumnFamilyDescriptor(rocksdb::kDefaultColumnFamilyName, rocksdb::ColumnFamilyOptions())
491+
rocksdb::ColumnFamilyDescriptor(rocksdb::kDefaultColumnFamilyName, cfOptions) // Use cfOptions here
486492
};
487493
}
488494

src/database.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,40 @@ export class RocksDatabase extends DBI<DBITransactional> {
154154
return this.store.db.getOldestSnapshotTimestamp();
155155
}
156156

157+
/**
158+
* Gets a RocksDB database property as a string.
159+
*
160+
* @param propertyName - The name of the property to retrieve (e.g., 'rocksdb.levelstats').
161+
* @returns The property value as a string.
162+
*
163+
* @example
164+
* ```typescript
165+
* const db = RocksDatabase.open('/path/to/database');
166+
* const levelStats = db.getDBProperty('rocksdb.levelstats');
167+
* const stats = db.getDBProperty('rocksdb.stats');
168+
* ```
169+
*/
170+
getDBProperty(propertyName: string): string {
171+
return this.store.db.getDBProperty(propertyName);
172+
}
173+
174+
/**
175+
* Gets a RocksDB database property as an integer.
176+
*
177+
* @param propertyName - The name of the property to retrieve (e.g., 'rocksdb.num-blob-files').
178+
* @returns The property value as a number.
179+
*
180+
* @example
181+
* ```typescript
182+
* const db = RocksDatabase.open('/path/to/database');
183+
* const blobFiles = db.getDBIntProperty('rocksdb.num-blob-files');
184+
* const numKeys = db.getDBIntProperty('rocksdb.estimate-num-keys');
185+
* ```
186+
*/
187+
getDBIntProperty(propertyName: string): number {
188+
return this.store.db.getDBIntProperty(propertyName);
189+
}
190+
157191
/**
158192
* Flushes the underlying database by performing a commit or clearing any buffered operations.
159193
*

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export { Store, type Context } from './store.js';
55
export { DBIterator } from './dbi-iterator.js';
66
export { Transaction } from './transaction.js';
77
export type { Key } from './encoding.js';
8-
export { constants, TransactionLog, shutdown } from './load-binding.js';
8+
export { constants, TransactionLog, shutdown, type TransactionEntry } from './load-binding.js';
99
export * from './parse-transaction-log.js';
1010
import './transaction-log-reader.js';
1111

src/load-binding.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ export type NativeDatabase = {
109109
notify(event: string | BufferWithDataView, args?: any[]): boolean;
110110
get(key: BufferWithDataView, resolve: ResolveCallback<Buffer>, reject: RejectCallback, txnId?: number): number;
111111
getCount(options?: RangeOptions, txnId?: number): number;
112+
getDBIntProperty(propertyName: string): number;
113+
getDBProperty(propertyName: string): string;
112114
getMonotonicTimestamp(): number;
113115
getOldestSnapshotTimestamp(): number;
114116
getSync(key: BufferWithDataView, txnId?: number): Buffer;

test/db-properties.test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { dbRunner } from './lib/util.js';
3+
describe('Database Properties', () => {
4+
it('should get string property from database', () => dbRunner(async ({ db }) => {
5+
// Put some data to ensure the database has stats
6+
await db.put('key1', 'value1');
7+
await db.put('key2', 'value2');
8+
await db.flush();
9+
10+
// Get level stats property
11+
const levelStats = db.getDBProperty('rocksdb.levelstats');
12+
expect(levelStats).toBeDefined();
13+
expect(typeof levelStats).toBe('string');
14+
expect(levelStats.length).toBeGreaterThan(0);
15+
}));
16+
17+
it('should get stats property from database', () => dbRunner(async ({ db }) => {
18+
// Put some data
19+
await db.put('foo', 'bar');
20+
await db.flush();
21+
22+
// Get stats property
23+
const stats = db.getDBProperty('rocksdb.stats');
24+
expect(stats).toBeDefined();
25+
expect(typeof stats).toBe('string');
26+
expect(stats.length).toBeGreaterThan(0);
27+
}));
28+
29+
it('should get integer property from database', () => dbRunner(async ({ db }) => {
30+
// Put some data
31+
await db.put('key1', 'value1');
32+
await db.put('key2', 'value2');
33+
34+
// Get estimated number of keys
35+
const numKeys = db.getDBIntProperty('rocksdb.estimate-num-keys');
36+
expect(numKeys).toBeDefined();
37+
expect(typeof numKeys).toBe('number');
38+
expect(numKeys).toBeGreaterThan(0);
39+
}));
40+
41+
it('should get num-files-at-level property', () => dbRunner(async ({ db }) => {
42+
// Put some data and flush to create files
43+
await db.put('key1', 'value1');
44+
await db.put('key2', 'value2');
45+
await db.flush();
46+
47+
// Get number of files at level 0, for some reason this is a string property
48+
const numFiles = +db.getDBProperty('rocksdb.num-files-at-level0');
49+
expect(numFiles).toBeDefined();
50+
expect(typeof numFiles).toBe('number');
51+
expect(numFiles).toBeGreaterThan(0);
52+
}));
53+
54+
it('should test blob files with num-blob-files property', () => dbRunner(async ({ db }) => {
55+
// Create large values that should trigger blob storage
56+
// RocksDB typically uses blobs for values larger than a threshold (often 1KB or configurable)
57+
const largeValue = 'x'.repeat(10000); // 10KB value to ensure blob creation
58+
59+
// Write multiple large values
60+
await db.put('blob1', largeValue);
61+
await db.put('blob2', largeValue);
62+
await db.put('blob3', largeValue);
63+
await db.flush();
64+
65+
// Get number of blob files
66+
const numBlobFiles = db.getDBIntProperty('rocksdb.num-blob-files');
67+
expect(numBlobFiles).toBeDefined();
68+
expect(typeof numBlobFiles).toBe('number');
69+
expect(numBlobFiles).toBeGreaterThan(0);
70+
71+
// Note: Whether blobs are actually created depends on RocksDB configuration
72+
// The test verifies the method works, not necessarily that blobs are created
73+
}));
74+
75+
it('should get live-blob-file-size property', () => dbRunner(async ({ db }) => {
76+
// Create large values
77+
const largeValue = 'x'.repeat(10000);
78+
await db.put('blob1', largeValue);
79+
await db.put('blob2', largeValue);
80+
await db.flush();
81+
82+
// Get live blob file size
83+
const blobSize = db.getDBIntProperty('rocksdb.live-blob-file-size');
84+
expect(blobSize).toBeDefined();
85+
expect(typeof blobSize).toBe('number');
86+
expect(blobSize).toBeGreaterThan(0);
87+
}));
88+
89+
it('should get total-blob-file-size property', () => dbRunner(async ({ db }) => {
90+
// Create large values
91+
const largeValue = 'x'.repeat(10000);
92+
await db.put('blob1', largeValue);
93+
await db.flush();
94+
95+
// Get total blob file size
96+
const totalBlobSize = db.getDBIntProperty('rocksdb.total-blob-file-size');
97+
expect(totalBlobSize).toBeDefined();
98+
expect(typeof totalBlobSize).toBe('number');
99+
expect(totalBlobSize).toBeGreaterThan(0);
100+
}));
101+
102+
it('should get background-errors property', () => dbRunner(async ({ db }) => {
103+
const bgErrors = db.getDBIntProperty('rocksdb.background-errors');
104+
expect(bgErrors).toBeDefined();
105+
expect(typeof bgErrors).toBe('number');
106+
expect(bgErrors).toBe(0); // Should be 0 for a healthy database
107+
}));
108+
109+
it('should get cur-size-all-mem-tables property', () => dbRunner(async ({ db }) => {
110+
await db.put('key1', 'value1');
111+
await db.put('key2', 'value2');
112+
113+
const memTableSize = db.getDBIntProperty('rocksdb.cur-size-all-mem-tables');
114+
expect(memTableSize).toBeDefined();
115+
expect(typeof memTableSize).toBe('number');
116+
expect(memTableSize).toBeGreaterThan(0);
117+
}));
118+
119+
it('should throw error for invalid string property', () => dbRunner(async ({ db }) => {
120+
expect(() => {
121+
db.getDBProperty(undefined as any);
122+
}).toThrow('Property name is required');
123+
124+
expect(() => {
125+
db.getDBProperty('invalid.property.name.that.does.not.exist');
126+
}).toThrow('Failed to get database property');
127+
}));
128+
129+
it('should throw error for invalid integer property', () => dbRunner(async ({ db }) => {
130+
expect(() => {
131+
db.getDBIntProperty(undefined as any);
132+
}).toThrow('Property name is required');
133+
134+
expect(() => {
135+
db.getDBIntProperty('invalid.property.name.that.does.not.exist');
136+
}).toThrow('Failed to get database integer property');
137+
}));
138+
139+
it('should throw error when database is not open', () => dbRunner({ skipOpen: true }, async ({ db }) => {
140+
expect(() => {
141+
db.getDBProperty('rocksdb.stats');
142+
}).toThrow('Database not open');
143+
144+
expect(() => {
145+
db.getDBIntProperty('rocksdb.estimate-num-keys');
146+
}).toThrow('Database not open');
147+
}));
148+
});

0 commit comments

Comments
 (0)