@harperfast/rocksdb-js 1.1.0 → 1.2.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.
@@ -0,0 +1,568 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { RocksDatabase, parseTransactionLog, versions } from '../dist/index.mjs';
4
+ import { existsSync, readdirSync } from 'node:fs';
5
+ import { mkdir, readdir } from 'node:fs/promises';
6
+ import { join, resolve } from 'node:path';
7
+ import { stdin, stdout } from 'node:process';
8
+ import { createInterface } from 'node:readline/promises';
9
+ import repl from 'node:repl';
10
+ import { inspect, parseArgs, styleText } from 'node:util';
11
+
12
+ const COMMANDS = {
13
+ clear: clearCommand,
14
+ columns: columnsCommand,
15
+ compact: compactCommand,
16
+ count: countCommand,
17
+ drop: dropCommand,
18
+ exit: () => process.exit(0),
19
+ get: getCommand,
20
+ help: helpCommand,
21
+ log: logCommand,
22
+ prop: propCommand,
23
+ 'purge-logs': purgeLogsCommand,
24
+ put: putCommand,
25
+ query: queryCommand,
26
+ remove: removeCommand,
27
+ repl: replCommand,
28
+ stats: statsCommand,
29
+ use: useCommand,
30
+ };
31
+
32
+ const { values: argv, positionals } = parseArgs({
33
+ allowPositionals: true,
34
+ arguments: process.argv.slice(2),
35
+ options: {
36
+ help: { type: 'boolean', short: 'h' },
37
+ readonly: { type: 'boolean', short: 'r' },
38
+ version: { type: 'boolean', short: 'v' },
39
+ },
40
+ });
41
+ const dbPath = resolve(positionals[0] || process.cwd());
42
+ const dbs = {};
43
+ let currentDB = null;
44
+ let r = null;
45
+ const defaultOpenOptions = {
46
+ disableWAL: false,
47
+ encoding: 'msgpack',
48
+ freezeData: true,
49
+ randomAccessStructure: true,
50
+ readOnly: argv.readonly,
51
+ };
52
+
53
+ function completer(line) {
54
+ const parts = line.trimStart().split(/[ \t]+/);
55
+ const command = parts[0];
56
+
57
+ if (parts.length <= 1) {
58
+ const hits = Object.keys(COMMANDS).filter((c) => c.startsWith(command));
59
+ return [hits.length ? hits : [], line];
60
+ }
61
+
62
+ if ((command === 'drop' || command === 'use') && parts.length === 2) {
63
+ const arg = parts[1];
64
+ const columns = dbs.default?.columns ?? [];
65
+ const hits = columns.filter((c) => c.startsWith(arg));
66
+ return [hits.length ? hits : columns, arg];
67
+ }
68
+
69
+ if (command === 'log' && currentDB) {
70
+ if (parts.length === 2) {
71
+ const arg = parts[1];
72
+ const logs = currentDB.listLogs();
73
+ const hits = logs.filter((l) => l.startsWith(arg));
74
+ return [hits.length ? hits : logs, arg];
75
+ } else if (parts.length === 3) {
76
+ const [name, store] = parts.slice(1);
77
+ const logs = currentDB.listLogs();
78
+ const hit = logs.find((l) => l === name);
79
+ if (hit) {
80
+ const logPath = join(currentDB.path, 'transaction_logs', name);
81
+ const logFiles = readdirSync(logPath).filter(
82
+ (f) => (!store || f.startsWith(store)) && f.endsWith('.txnlog')
83
+ );
84
+ return [logFiles.length ? logFiles : [], store, name];
85
+ }
86
+ return [[], store, name];
87
+ }
88
+ }
89
+
90
+ return [[], line];
91
+ }
92
+
93
+ function onSIGINT() {
94
+ if (ctrlC) {
95
+ console.log();
96
+ process.exit(0);
97
+ }
98
+ ctrlC = true;
99
+ console.log('\n(To exit, press Ctrl+C again or Ctrl+D or type "exit")');
100
+ currentAbortController?.abort();
101
+ }
102
+
103
+ function createRL() {
104
+ const iface = createInterface({ input: stdin, output: stdout, completer });
105
+ iface.on('SIGINT', onSIGINT);
106
+ iface.on('SIGBREAK', () => process.exit(0));
107
+ return iface;
108
+ }
109
+
110
+ let rl = createRL();
111
+ let ctrlC = false;
112
+ let currentAbortController = null;
113
+
114
+ function str(text) {
115
+ try {
116
+ if (typeof text === 'symbol') {
117
+ return `Symbol(${text.description ?? ''})`;
118
+ }
119
+ return String(text);
120
+ } catch {
121
+ return inspect(text, { colors: true, depth: 10 });
122
+ }
123
+ }
124
+
125
+ const hl = (text) => styleText('magenta', str(text));
126
+ const note = (text) => styleText('gray', str(text));
127
+ const warn = (text) => styleText('yellow', str(text));
128
+ const bad = (text) => styleText('red', str(text));
129
+
130
+ function wrapColumns(columns) {
131
+ const width = stdout.columns || 80;
132
+ const prefix = 'Columns: ';
133
+ const indent = ' '.repeat(prefix.length);
134
+ const lines = [];
135
+ let line = '';
136
+ let lineLen = 0;
137
+ for (const col of columns) {
138
+ const sepLen = line ? 2 : 0; // ', '
139
+ if (line && prefix.length + lineLen + sepLen + col.length > width) {
140
+ lines.push(line);
141
+ line = hl(col);
142
+ lineLen = col.length;
143
+ } else {
144
+ line += (line ? ', ' : '') + hl(col);
145
+ lineLen += sepLen + col.length;
146
+ }
147
+ }
148
+ if (line) lines.push(line);
149
+ return prefix + lines.join('\n' + indent);
150
+ }
151
+
152
+ async function run(fn) {
153
+ const start = Date.now();
154
+ const result = await fn();
155
+ const end = Date.now();
156
+ return { time: end - start, result };
157
+ }
158
+
159
+ async function ask(prompt) {
160
+ rl.resume();
161
+ while (true) {
162
+ try {
163
+ currentAbortController = new AbortController();
164
+ const answer = await rl.question(prompt, { signal: currentAbortController.signal });
165
+ rl.pause();
166
+ ctrlC = false;
167
+ return answer;
168
+ } catch (err) {
169
+ if (err.code !== 'ABORT_ERR') throw err;
170
+ }
171
+ }
172
+ }
173
+
174
+ async function clearCommand() {
175
+ const answer = await ask(`Are you sure you want to clear all data? (y/N) `);
176
+ if (answer !== 'y' && answer !== 'Y') {
177
+ console.log();
178
+ return;
179
+ }
180
+ const { time } = await run(() => currentDB.clear());
181
+ console.log(`Cleared ${hl(currentDB.name)} ${note(`(${time}ms)`)}\n`);
182
+ }
183
+
184
+ function columnsCommand() {
185
+ for (const column of currentDB.columns) {
186
+ console.log(hl(column));
187
+ }
188
+ console.log();
189
+ }
190
+
191
+ async function compactCommand() {
192
+ const sizeBefore = currentDB.getDBIntProperty('rocksdb.estimate-live-data-size') ?? 0;
193
+ const { time } = await run(() => currentDB.compact());
194
+ const sizeAfter = currentDB.getDBIntProperty('rocksdb.estimate-live-data-size') ?? 0;
195
+ const ratio =
196
+ sizeBefore > 0
197
+ ? `reduced by ${(((sizeBefore - sizeAfter) / sizeBefore) * 100).toFixed(2)}%`
198
+ : sizeAfter > 0
199
+ ? `increased by ${sizeAfter.toLocaleString()} bytes`
200
+ : 'no change';
201
+ console.log(`Compacted, ${ratio} ${note(`(${time}ms)`)}`);
202
+ console.log(`${hl(sizeBefore.toLocaleString())} -> ${hl(sizeAfter.toLocaleString())} bytes\n`);
203
+ console.log();
204
+ }
205
+
206
+ async function countCommand() {
207
+ const { result: count, time } = await run(() => currentDB.getKeysCount());
208
+ console.log(`Count: ${hl(count)} ${note(`(${time}ms)`)}\n`);
209
+ }
210
+
211
+ async function dropCommand(args) {
212
+ const [column] = args;
213
+ if (!column) {
214
+ console.log(`Usage: ${hl('drop <column>')}\n`);
215
+ return;
216
+ }
217
+
218
+ const { columns } = dbs.default;
219
+ if (!columns.includes(column)) {
220
+ console.log(`Column family ${hl(column)} does not exist\n`);
221
+ return;
222
+ }
223
+
224
+ if (column === 'default') {
225
+ console.log('You cannot drop the default column family');
226
+ const answer = await ask(`Are you sure you want to clear all data? (y/N) `);
227
+ if (answer !== 'y' && answer !== 'Y') {
228
+ console.log();
229
+ return;
230
+ }
231
+ const { time } = await run(() => dbs.default.drop());
232
+ console.log(`Cleared ${hl('default')} ${note(`(${time}ms)`)}\n`);
233
+ return;
234
+ }
235
+
236
+ const db = dbs[column] || RocksDatabase.open(dbPath, { name: column, ...defaultOpenOptions });
237
+ const { time } = await run(() => db.drop());
238
+ db.close();
239
+ delete dbs[column];
240
+ console.log(`Dropped ${hl(column)} ${note(`(${time}ms)`)}\n`);
241
+ if (currentDB.name === column) {
242
+ console.log(`Switching ${hl(currentDB.name)} => ${hl('default')}\n`);
243
+ currentDB = dbs.default;
244
+ }
245
+ }
246
+
247
+ async function getCommand(args) {
248
+ const [key] = args;
249
+ if (!key) {
250
+ console.log(`Usage: ${hl('get <key>')}\n`);
251
+ return;
252
+ }
253
+ const value = await currentDB.get(key);
254
+ if (value === undefined) {
255
+ console.log('Key not found\n');
256
+ return;
257
+ }
258
+ console.log(value);
259
+ console.log();
260
+ }
261
+
262
+ async function propCommand(args) {
263
+ const [key] = args;
264
+ if (!key) {
265
+ console.log(`Usage: ${hl('prop <key>')}\n`);
266
+ return;
267
+ }
268
+ const value = currentDB.getDBProperty(key);
269
+ console.log(value);
270
+ console.log();
271
+ }
272
+
273
+ function helpCommand() {
274
+ console.log(`${hl('clear')} Clear all data in the current column family`);
275
+ console.log(`${hl('columns')} List column families`);
276
+ console.log(`${hl('compact')} Compact the current column family`);
277
+ console.log(
278
+ `${hl('count')} Count the number of keys in the current column family`
279
+ );
280
+ console.log(`${hl('drop <column>')} Permanently drop a column family`);
281
+ console.log(`${hl('exit')} Exit the REPL`);
282
+ console.log(`${hl('get <key>')} Get the value of a key`);
283
+ console.log(`${hl('help')} Show this help message`);
284
+ console.log(
285
+ `${hl('log [name] [file] [entry]')} List the transaction log store names and log store files`
286
+ );
287
+ console.log(`${hl('prop <key>')} Get a RocksDB property (try "rocksdb.stats")`);
288
+ console.log(`${hl('purge-logs <name>')} Purge transaction log files older than 3 days`);
289
+ console.log(`${hl('put <key> <value>')} Set the value of a key`);
290
+ console.log(`${hl('query [start] [end]')} Query a range of keys`);
291
+ console.log(`${hl('remove <key>')} Delete a key`);
292
+ console.log(
293
+ `${hl('repl')} Open a JS sub-REPL; "db" refers to the current column family`
294
+ );
295
+ console.log(
296
+ `${hl('stats')} Show the statistics for the current column family`
297
+ );
298
+ console.log(
299
+ `${hl('use [column]')} Create a new column family or switch to an existing one`
300
+ );
301
+ console.log();
302
+ }
303
+
304
+ async function logCommand(args) {
305
+ const [name, file, entryStr] = args;
306
+ const logs = currentDB.listLogs();
307
+ if (name) {
308
+ if (!logs.includes(name)) {
309
+ console.log(`Log store ${hl(name)} does not exist\n`);
310
+ return;
311
+ }
312
+ if (file) {
313
+ const logPath = join(currentDB.path, 'transaction_logs', name, file);
314
+ const log = parseTransactionLog(logPath);
315
+ const entry = entryStr ? parseInt(entryStr) : undefined;
316
+ if (!isNaN(entry)) {
317
+ if (entry < 1 || entry > log.entries.length) {
318
+ console.log(`Entry ${hl(entry)} does not exist\n`);
319
+ return;
320
+ }
321
+ const { data } = log.entries[entry - 1];
322
+ console.log(`Entry ${entry} (${data.length.toLocaleString()} bytes)\n`);
323
+ console.log(
324
+ hl(
325
+ data
326
+ .toString('hex')
327
+ .match(/.{1,2}/g)
328
+ .join(' ')
329
+ )
330
+ );
331
+ console.log();
332
+ return;
333
+ }
334
+
335
+ console.log(`Size = ${hl(`${log.size.toLocaleString()} bytes`)}`);
336
+ console.log(`Version = ${hl(log.version)}`);
337
+ console.log(`Timestamp = ${hl(new Date(log.timestamp).toISOString())}`);
338
+ console.log(`Entries = ${hl(log.entries.length)}`);
339
+ let i = 1;
340
+ for (const entry of log.entries) {
341
+ console.log(
342
+ `${String(i++).padStart(3)}) ${hl(new Date(entry.timestamp).toISOString())} ${entry.length.toLocaleString()} bytes ${entry.flags & 0x01 ? '' : note('(continued in next file)')}`
343
+ );
344
+ }
345
+ console.log();
346
+ return;
347
+ }
348
+ const logPath = join(currentDB.path, 'transaction_logs', name);
349
+ const logFiles = await readdir(logPath);
350
+ for (const logFile of logFiles) {
351
+ if (!logFile.endsWith('.txnlog')) continue;
352
+ console.log(hl(logFile));
353
+ }
354
+ } else if (logs.length > 0) {
355
+ for (const log of logs) {
356
+ console.log(hl(log));
357
+ }
358
+ } else {
359
+ console.log('No log stores found');
360
+ }
361
+ console.log();
362
+ }
363
+
364
+ async function purgeLogsCommand(args) {
365
+ const [name] = args;
366
+ if (!name) {
367
+ console.log(`Usage: ${hl('purge-logs <name>')}\n`);
368
+ return;
369
+ }
370
+ const answer = await ask(`Are you sure you want to purge logs? (y/N) `);
371
+ if (answer !== 'y' && answer !== 'Y') {
372
+ console.log();
373
+ return;
374
+ }
375
+ const { result: removed, time } = await run(() => currentDB.purgeLogs({ name }));
376
+ console.log(
377
+ `Purged ${removed.length} log file${removed.length === 1 ? '' : 's'} ${note(`(${time}ms)`)}`
378
+ );
379
+ for (const file of removed) {
380
+ console.log(hl(file));
381
+ }
382
+ console.log();
383
+ }
384
+
385
+ async function putCommand(args) {
386
+ const [key, ...valueParts] = args;
387
+ if (!key || valueParts.length === 0) {
388
+ console.log(`Usage: ${hl('put <key> <value>')}\n`);
389
+ return;
390
+ }
391
+ let value = valueParts.join(' ');
392
+ if (value.startsWith('"') && value.endsWith('"')) {
393
+ value = value.slice(1, -1);
394
+ }
395
+ await currentDB.put(key, value);
396
+ console.log();
397
+ }
398
+
399
+ async function queryCommand(args) {
400
+ const startTime = Date.now();
401
+ let count = 0;
402
+ const [start, end] = args;
403
+ try {
404
+ for await (const { key } of currentDB.getRange({ start, end, values: false })) {
405
+ try {
406
+ const value = await currentDB.get(key);
407
+ console.log(`${str(key)} = ${inspect(value, { colors: true, depth: 10 })}`);
408
+ } catch (error) {
409
+ console.error(`${str(key)} = ${bad(error)}`);
410
+ }
411
+ count++;
412
+ }
413
+ const endTime = Date.now();
414
+ console.log(`\n${count} results ${note(`(${endTime - startTime}ms)`)}`);
415
+ } catch (err) {
416
+ console.log(bad(err.toString()));
417
+ }
418
+ console.log();
419
+ }
420
+
421
+ async function removeCommand(args) {
422
+ const [key] = args;
423
+ if (!key) {
424
+ console.log(`Usage: ${hl('remove <key>')}\n`);
425
+ return;
426
+ }
427
+ const { time } = await run(() => currentDB.remove(key));
428
+ console.log(`Removed ${hl(key)} ${note(`(${time}ms)`)}\n`);
429
+ }
430
+
431
+ async function replCommand() {
432
+ const history = [...rl.history];
433
+ rl.close();
434
+ r = repl.start({ prompt: styleText('magenta', 'js> '), useColors: true, useGlobal: false });
435
+ Object.defineProperty(r.context, 'db', { get: () => currentDB, enumerable: true });
436
+ r.on('SIGINT', () => r.close());
437
+ await new Promise((resolve) => r.on('exit', resolve));
438
+ r = null;
439
+ console.log();
440
+ rl = createRL();
441
+ rl.history = history;
442
+ }
443
+
444
+ function statsCommand() {
445
+ const stats = currentDB.getStats();
446
+ const maxlen = Math.max(...Object.keys(stats).map((key) => key.length)) - 8;
447
+ for (const [key, value] of Object.entries(stats)) {
448
+ console.log(`${key.substring(8).padEnd(maxlen)} = ${hl(value)}`);
449
+ }
450
+ console.log();
451
+ }
452
+
453
+ async function useCommand(args) {
454
+ const [name] = args;
455
+ if (!name) {
456
+ currentDB = dbs.default;
457
+ console.log(`${hl(currentDB.name)}\n`);
458
+ return;
459
+ }
460
+ if (name === currentDB.name) {
461
+ console.log(`${hl(currentDB.name)}\n`);
462
+ return;
463
+ }
464
+
465
+ const columns = currentDB.columns;
466
+ if (!dbs[name]) {
467
+ if (columns.includes(name)) {
468
+ console.log(`Switching ${hl(currentDB.name)} => ${hl(name)}\n`);
469
+ } else {
470
+ const answer = await ask(`Create new column ${hl(name)}? (y/N) `);
471
+ if (answer !== 'y' && answer !== 'Y') {
472
+ console.log();
473
+ return;
474
+ }
475
+ console.log(`Created ${hl(name)}\n`);
476
+ }
477
+ dbs[name] = RocksDatabase.open(dbPath, { name, ...defaultOpenOptions });
478
+ } else {
479
+ console.log(`Switching ${hl(currentDB.name)} => ${hl(name)}\n`);
480
+ }
481
+ currentDB = dbs[name];
482
+ }
483
+
484
+ async function main() {
485
+ console.log(`rocksdb.js CLI v${versions['rocksdb-js']} (RocksDB v${versions.rocksdb})`);
486
+
487
+ if (argv.help) {
488
+ console.log(`\nUsage: ${hl('rocksdb-js [dbpath]')}`);
489
+ console.log('\nOptions:');
490
+ console.log(` ${hl('-h, --help')} Show this help message`);
491
+ console.log(` ${hl('-r, --readonly')} Open the database in read-only mode`);
492
+ console.log(` ${hl('-v, --version')} Show the version information`);
493
+ console.log();
494
+ process.exit(0);
495
+ }
496
+
497
+ if (argv.version) {
498
+ process.exit(0);
499
+ }
500
+
501
+ console.log();
502
+ if (argv.readonly) {
503
+ console.log(`Mode: ${warn('READ-ONLY')}`);
504
+ }
505
+ console.log(`Path: ${hl(dbPath)}`);
506
+
507
+ try {
508
+ if (!existsSync(join(dbPath, 'CURRENT'))) {
509
+ console.log();
510
+ let answer = await ask('RocksDB database does not exist, create it? (y/N) ');
511
+ if (answer !== 'y' && answer !== 'Y') {
512
+ process.exit(0);
513
+ }
514
+ await mkdir(dbPath, { recursive: true });
515
+
516
+ const count = await readdir(dbPath);
517
+ if (count.length > 0) {
518
+ answer = await ask('Directory not empty, are you sure you want to continue? (y/N) ');
519
+ if (answer !== 'y' && answer !== 'Y') {
520
+ process.exit(0);
521
+ }
522
+ }
523
+ }
524
+
525
+ currentDB = RocksDatabase.open(dbPath, { ...defaultOpenOptions });
526
+ dbs[currentDB.name] = currentDB;
527
+
528
+ console.log(wrapColumns(currentDB.columns) + '\n');
529
+ console.log(`Type "help" for more information.`);
530
+
531
+ while (true) {
532
+ const line = (await ask('> ')).trim().split(/[ \t]+/);
533
+ const command = line[0];
534
+ if (!command) continue;
535
+ const commandFn = COMMANDS[command];
536
+ if (commandFn) {
537
+ try {
538
+ await commandFn(line.slice(1));
539
+ } catch (error) {
540
+ console.error(bad(error));
541
+ }
542
+ } else {
543
+ console.log(`Unknown command: ${command}\n`);
544
+ }
545
+ }
546
+ } catch (error) {
547
+ if (error.message.includes('Resource temporarily unavailable')) {
548
+ console.log();
549
+ console.error(
550
+ bad('Error: RocksDB is already open by another process, rerun with the read-only flag:\n')
551
+ );
552
+ console.log(hl(` rocksdb-js --readonly ${dbPath}\n`));
553
+ process.exit(1);
554
+ }
555
+ if (error.code !== 'ABORT_ERR') {
556
+ console.error(bad(error));
557
+ process.exit(1);
558
+ }
559
+ } finally {
560
+ r?.close();
561
+ for (const db of Object.values(dbs)) {
562
+ db?.close();
563
+ }
564
+ rl.close();
565
+ }
566
+ }
567
+
568
+ main();