From dd806d438ad9f8863dabeffca25a2782f8f38286 Mon Sep 17 00:00:00 2001 From: Moshe-RS Date: Wed, 7 Jun 2023 17:07:40 +0300 Subject: [PATCH 1/3] - Added RESTORE functionality --- examples/dump-and-restore.js | 31 +++++ packages/client/lib/cluster/commands.ts | 3 + packages/client/lib/commands/RESTORE.spec.ts | 127 +++++++++++++++++++ packages/client/lib/commands/RESTORE.ts | 37 ++++++ 4 files changed, 198 insertions(+) create mode 100644 examples/dump-and-restore.js create mode 100644 packages/client/lib/commands/RESTORE.spec.ts create mode 100644 packages/client/lib/commands/RESTORE.ts diff --git a/examples/dump-and-restore.js b/examples/dump-and-restore.js new file mode 100644 index 00000000000..3524bd23e1e --- /dev/null +++ b/examples/dump-and-restore.js @@ -0,0 +1,31 @@ +// This example demonstrates the use of DUMP and RESTORE functionalities + +import { commandOptions, createClient } from 'redis'; + +const client = createClient(); +await client.connect(); + +// DUMP a specific key into a local variable +const firstValueDump = await client.dump( + commandOptions({ returnBuffers: true }), + 'FirstKey' +); + +// RESTORE into a new key +await client.restore('newKey', 0, firstValueDump); + +// DUMP a different key a local variable +const secondValueDump = await client.dump( + commandOptions({ returnBuffers: true }), + 'FirstKey' +); + +// RESTORE into an existing key +await client.restore('newKey', 0, secondValueDump, { + REPLACE: true +}); + +// RESTORE into an new key with TTL +await client.restore('TTLKey', 60000, secondValueDump); + +await client.quit(); diff --git a/packages/client/lib/cluster/commands.ts b/packages/client/lib/cluster/commands.ts index 58fa651be1b..84a37862772 100644 --- a/packages/client/lib/cluster/commands.ts +++ b/packages/client/lib/cluster/commands.ts @@ -110,6 +110,7 @@ import * as PTTL from '../commands/PTTL'; import * as PUBLISH from '../commands/PUBLISH'; import * as RENAME from '../commands/RENAME'; import * as RENAMENX from '../commands/RENAMENX'; +import * as RESTORE from '../commands/RESTORE'; import * as RPOP_COUNT from '../commands/RPOP_COUNT'; import * as RPOP from '../commands/RPOP'; import * as RPOPLPUSH from '../commands/RPOPLPUSH'; @@ -434,6 +435,8 @@ export default { rename: RENAME, RENAMENX, renameNX: RENAMENX, + RESTORE, + restore: RESTORE, RPOP_COUNT, rPopCount: RPOP_COUNT, RPOP, diff --git a/packages/client/lib/commands/RESTORE.spec.ts b/packages/client/lib/commands/RESTORE.spec.ts new file mode 100644 index 00000000000..54c2b3107bf --- /dev/null +++ b/packages/client/lib/commands/RESTORE.spec.ts @@ -0,0 +1,127 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './RESTORE'; + +describe('RESTORE', () => { + describe('transformArguments', () => { + it('parses ttl and value', () => { + assert.deepEqual( + transformArguments('KeyName', 0, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"'), + ['RESTORE', 'KeyName', '0', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"'] + ); + }); + + it('parses REPLACE', () => { + assert.deepEqual( + transformArguments('KeyName', 0, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { + REPLACE: true + }), + ['RESTORE', 'KeyName', '0', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'REPLACE'] + ); + }); + + it('parses ABSTTL', () => { + assert.deepEqual( + transformArguments('KeyName', 2693098555000, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { + ABSTTL: true + }), + ['RESTORE', 'KeyName', '2693098555000', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'ABSTTL'] + ); + }); + + it('parses IDLETIME', () => { + assert.deepEqual( + transformArguments('KeyName', 0, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { + IDLETIME: 5 + }), + ['RESTORE', 'KeyName', '0', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'IDLETIME', '5'] + ); + }); + + it('parses FREQ', () => { + assert.deepEqual( + transformArguments('KeyName', 0, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { + FREQ: 5 + }), + ['RESTORE', 'KeyName', '0', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'FREQ', '5'] + ); + }); + + it('parses REPLACE and ABSTTL', () => { + assert.deepEqual( + transformArguments('KeyName', 2693098555000, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { + REPLACE: true, + ABSTTL: true + }), + ['RESTORE', 'KeyName', '2693098555000', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'REPLACE', 'ABSTTL'] + ); + }); + + it('parses REPLACE and IDLETIME', () => { + assert.deepEqual( + transformArguments('KeyName', 0, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { + REPLACE: true, + IDLETIME: 5 + }), + ['RESTORE', 'KeyName', '0', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'REPLACE', 'IDLETIME', '5'] + ); + }); + + it('parses REPLACE, ABSTTL, IDLETIME and FREQ', () => { + assert.deepEqual( + transformArguments('KeyName', 2693098555000, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { + REPLACE: true, + ABSTTL: true, + IDLETIME: 5, + FREQ: 50 + }), + ['RESTORE', 'KeyName', '2693098555000', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'REPLACE', 'ABSTTL', 'IDLETIME', '5', 'FREQ', '50'] + ); + }); + }); + + describe('client.restore', () => { + testUtils.testWithClient('new key', async client => { + await client.set('oldKey', 'oldValue') + const dumpValue = await client.dump('oldKey'); + assert.equal( + await client.restore('newKey', 0, dumpValue), + 'OK' + ); + assert.equal( + await client.get('newKey'), + 'oldValue' + ) + }, GLOBAL.SERVERS.OPEN); + + + testUtils.testWithClient('crash on RESTORE for existing key', async client => { + await client.set('oldKey', 'oldValue') + await client.set('newKey', 'newValue') + const dumpValue = await client.dump('oldKey'); + assert.rejects( + client.restore('newKey', 0, dumpValue), + { + name: 'ErrorReply', + message: 'BUSYKEY Target key name already exists.' + } + ) + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('replace existing key', async client => { + await client.set('oldKey', 'oldValue') + await client.set('newKey', 'newValue') + const dumpValue = await client.dump('oldKey'); + assert.equal( + await client.restore('newKey', 0, dumpValue, { + REPLACE: true + }), + 'OK' + ); + assert.equal( + await client.get('newKey'), + 'oldValue' + ) + }, GLOBAL.SERVERS.OPEN); + }) +}); diff --git a/packages/client/lib/commands/RESTORE.ts b/packages/client/lib/commands/RESTORE.ts new file mode 100644 index 00000000000..a90b4701364 --- /dev/null +++ b/packages/client/lib/commands/RESTORE.ts @@ -0,0 +1,37 @@ +import { RedisCommandArgument, RedisCommandArguments } from '.'; + +interface RestoreOptions { + REPLACE?: true; + ABSTTL?: true; + IDLETIME?: number; + FREQ?: number; +} + +export function transformArguments( + key: RedisCommandArgument, + ttl: number, + serializedValue: RedisCommandArgument, + options?: RestoreOptions +): RedisCommandArguments { + const args = ['RESTORE', key, ttl.toString(), serializedValue]; + + if (options?.REPLACE) { + args.push('REPLACE'); + } + + if (options?.ABSTTL) { + args.push('ABSTTL'); + } + + if (options?.IDLETIME) { + args.push('IDLETIME', options.IDLETIME.toString()); + } + + if (options?.FREQ) { + args.push('FREQ', options.FREQ.toString()); + } + + return args; +} + +export declare function transformReply(): 'OK'; From 2fb65756e5f916b4fb32bca2928ce56406a7c34b Mon Sep 17 00:00:00 2001 From: Leibale Eidelman Date: Mon, 18 Sep 2023 18:18:10 -0400 Subject: [PATCH 2/3] add FIRST_KEY_INDEX, fix tests, clean example, add example to examples table --- examples/README.md | 3 +- examples/dump-and-restore.js | 21 +--- packages/client/lib/commands/RESTORE.spec.ts | 117 +++++-------------- packages/client/lib/commands/RESTORE.ts | 2 + 4 files changed, 42 insertions(+), 101 deletions(-) diff --git a/examples/README.md b/examples/README.md index 19e9df31f90..4e7655a3519 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,7 +3,7 @@ This folder contains example scripts showing how to use Node Redis in different scenarios. | File Name | Description | -| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| | `blocking-list-pop.js` | Block until an element is pushed to a list. | | `bloom-filter.js` | Space efficient set membership checks with a [Bloom Filter](https://en.wikipedia.org/wiki/Bloom_filter) using [RedisBloom](https://redisbloom.io). | | `check-connection-status.js` | Check the client's connection status. | @@ -12,6 +12,7 @@ This folder contains example scripts showing how to use Node Redis in different | `connect-to-cluster.js` | Connect to a Redis cluster. | | `count-min-sketch.js` | Estimate the frequency of a given event using the [RedisBloom](https://redisbloom.io) Count-Min Sketch. | | `cuckoo-filter.js` | Space efficient set membership checks with a [Cuckoo Filter](https://en.wikipedia.org/wiki/Cuckoo_filter) using [RedisBloom](https://redisbloom.io). | +| `dump-and-restore.js` | Demonstrates the use of the [`DUMP`](https://redis.io/commands/dump/) and [`RESTORE`](https://redis.io/commands/restore/) commands | | `get-server-time.js` | Get the time from the Redis server. | | `hyperloglog.js` | Showing use of Hyperloglog commands [PFADD, PFCOUNT and PFMERGE](https://redis.io/commands/?group=hyperloglog). | | `lua-multi-incr.js` | Define a custom lua script that allows you to perform INCRBY on multiple keys. | diff --git a/examples/dump-and-restore.js b/examples/dump-and-restore.js index 3524bd23e1e..081e44f9f9a 100644 --- a/examples/dump-and-restore.js +++ b/examples/dump-and-restore.js @@ -1,4 +1,4 @@ -// This example demonstrates the use of DUMP and RESTORE functionalities +// This example demonstrates the use of the DUMP and RESTORE commands import { commandOptions, createClient } from 'redis'; @@ -6,26 +6,17 @@ const client = createClient(); await client.connect(); // DUMP a specific key into a local variable -const firstValueDump = await client.dump( +const dump = await client.dump( commandOptions({ returnBuffers: true }), - 'FirstKey' + 'source' ); // RESTORE into a new key -await client.restore('newKey', 0, firstValueDump); +await client.restore('destination', 0, dump); -// DUMP a different key a local variable -const secondValueDump = await client.dump( - commandOptions({ returnBuffers: true }), - 'FirstKey' -); - -// RESTORE into an existing key -await client.restore('newKey', 0, secondValueDump, { +// RESTORE and REPLACE an existing key +await client.restore('destination', 0, dump, { REPLACE: true }); -// RESTORE into an new key with TTL -await client.restore('TTLKey', 60000, secondValueDump); - await client.quit(); diff --git a/packages/client/lib/commands/RESTORE.spec.ts b/packages/client/lib/commands/RESTORE.spec.ts index 54c2b3107bf..ca535ec9f05 100644 --- a/packages/client/lib/commands/RESTORE.spec.ts +++ b/packages/client/lib/commands/RESTORE.spec.ts @@ -4,124 +4,71 @@ import { transformArguments } from './RESTORE'; describe('RESTORE', () => { describe('transformArguments', () => { - it('parses ttl and value', () => { + it('simple', () => { assert.deepEqual( - transformArguments('KeyName', 0, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"'), - ['RESTORE', 'KeyName', '0', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"'] + transformArguments('key', 0, 'value'), + ['RESTORE', 'key', '0', 'value'] ); }); - it('parses REPLACE', () => { + it('with REPLACE', () => { assert.deepEqual( - transformArguments('KeyName', 0, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { + transformArguments('key', 0, 'value', { REPLACE: true }), - ['RESTORE', 'KeyName', '0', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'REPLACE'] + ['RESTORE', 'key', '0', 'value', 'REPLACE'] ); }); - it('parses ABSTTL', () => { + it('with ABSTTL', () => { assert.deepEqual( - transformArguments('KeyName', 2693098555000, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { + transformArguments('key', 0, 'value', { ABSTTL: true }), - ['RESTORE', 'KeyName', '2693098555000', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'ABSTTL'] + ['RESTORE', 'key', '0', 'value', 'ABSTTL'] ); }); - it('parses IDLETIME', () => { + it('with IDLETIME', () => { assert.deepEqual( - transformArguments('KeyName', 0, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { - IDLETIME: 5 + transformArguments('key', 0, 'value', { + IDLETIME: 1 }), - ['RESTORE', 'KeyName', '0', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'IDLETIME', '5'] + ['RESTORE', 'key', '0', 'value', 'IDLETIME', '1'] ); }); - it('parses FREQ', () => { + it('with FREQ', () => { assert.deepEqual( - transformArguments('KeyName', 0, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { - FREQ: 5 + transformArguments('key', 0, 'value', { + FREQ: 1 }), - ['RESTORE', 'KeyName', '0', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'FREQ', '5'] + ['RESTORE', 'key', '0', 'value', 'FREQ', '1'] ); }); - it('parses REPLACE and ABSTTL', () => { + it('with REPLACE, ABSTTL, IDLETIME and FREQ', () => { assert.deepEqual( - transformArguments('KeyName', 2693098555000, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { - REPLACE: true, - ABSTTL: true - }), - ['RESTORE', 'KeyName', '2693098555000', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'REPLACE', 'ABSTTL'] - ); - }); - - it('parses REPLACE and IDLETIME', () => { - assert.deepEqual( - transformArguments('KeyName', 0, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { - REPLACE: true, - IDLETIME: 5 - }), - ['RESTORE', 'KeyName', '0', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'REPLACE', 'IDLETIME', '5'] - ); - }); - - it('parses REPLACE, ABSTTL, IDLETIME and FREQ', () => { - assert.deepEqual( - transformArguments('KeyName', 2693098555000, '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', { + transformArguments('key', 0, 'value', { REPLACE: true, ABSTTL: true, - IDLETIME: 5, - FREQ: 50 + IDLETIME: 1, + FREQ: 2 }), - ['RESTORE', 'KeyName', '2693098555000', '"\x00\x0bStringValue\n\x00\b\xebpW1H\x0c,"', 'REPLACE', 'ABSTTL', 'IDLETIME', '5', 'FREQ', '50'] + ['RESTORE', 'key', '0', 'value', 'REPLACE', 'ABSTTL', 'IDLETIME', '1', 'FREQ', '2'] ); }); }); - describe('client.restore', () => { - testUtils.testWithClient('new key', async client => { - await client.set('oldKey', 'oldValue') - const dumpValue = await client.dump('oldKey'); - assert.equal( - await client.restore('newKey', 0, dumpValue), - 'OK' - ); - assert.equal( - await client.get('newKey'), - 'oldValue' - ) - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.restore', async client => { + const [, dump] = await Promise.all([ + client.set('source', 'value'), + client.dump('source') + ]); - - testUtils.testWithClient('crash on RESTORE for existing key', async client => { - await client.set('oldKey', 'oldValue') - await client.set('newKey', 'newValue') - const dumpValue = await client.dump('oldKey'); - assert.rejects( - client.restore('newKey', 0, dumpValue), - { - name: 'ErrorReply', - message: 'BUSYKEY Target key name already exists.' - } - ) - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('replace existing key', async client => { - await client.set('oldKey', 'oldValue') - await client.set('newKey', 'newValue') - const dumpValue = await client.dump('oldKey'); - assert.equal( - await client.restore('newKey', 0, dumpValue, { - REPLACE: true - }), - 'OK' - ); - assert.equal( - await client.get('newKey'), - 'oldValue' - ) - }, GLOBAL.SERVERS.OPEN); - }) + assert.equal( + await client.restore('destination', 0, dump), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/RESTORE.ts b/packages/client/lib/commands/RESTORE.ts index a90b4701364..d9ac11c424b 100644 --- a/packages/client/lib/commands/RESTORE.ts +++ b/packages/client/lib/commands/RESTORE.ts @@ -1,5 +1,7 @@ import { RedisCommandArgument, RedisCommandArguments } from '.'; +export const FIRST_KEY_INDEX = 1; + interface RestoreOptions { REPLACE?: true; ABSTTL?: true; From 4fbb80e8572461b9ab3465d927e4bb38d4f670e7 Mon Sep 17 00:00:00 2001 From: Leibale Eidelman Date: Mon, 18 Sep 2023 18:50:04 -0400 Subject: [PATCH 3/3] use returnBuffers in test --- packages/client/lib/commands/RESTORE.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/lib/commands/RESTORE.spec.ts b/packages/client/lib/commands/RESTORE.spec.ts index ca535ec9f05..89d42f3d4de 100644 --- a/packages/client/lib/commands/RESTORE.spec.ts +++ b/packages/client/lib/commands/RESTORE.spec.ts @@ -63,7 +63,7 @@ describe('RESTORE', () => { testUtils.testWithClient('client.restore', async client => { const [, dump] = await Promise.all([ client.set('source', 'value'), - client.dump('source') + client.dump(client.commandOptions({ returnBuffers: true }), 'source') ]); assert.equal(