1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-06 02:15:48 +03:00

Support RedisTimeSeries (#1757)

* Implement missing commands and add test

* Update DECRBY.spec.ts

* Small changes

* clean code

* Update MGET_WITHLABELS.ts

Use map in transformReply

Co-authored-by: leibale <leibale1998@gmail.com>
This commit is contained in:
Avital Fine
2021-12-12 14:41:44 +01:00
committed by GitHub
parent bb75b06d67
commit 7110f23369
30 changed files with 1310 additions and 47 deletions

View File

@@ -0,0 +1,81 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './DECRBY';
describe('DECRBY', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('key', 1),
['TS.DECRBY', 'key', '1']
);
});
it('with TIMESTAMP', () => {
assert.deepEqual(
transformArguments('key', 1, {
TIMESTAMP: '*'
}),
['TS.DECRBY', 'key', '1', 'TIMESTAMP', '*']
);
});
it('with RETENTION', () => {
assert.deepEqual(
transformArguments('key', 1, {
RETENTION: 1
}),
['TS.DECRBY', 'key', '1', 'RETENTION', '1']
);
});
it('with UNCOMPRESSED', () => {
assert.deepEqual(
transformArguments('key', 1, {
UNCOMPRESSED: true
}),
['TS.DECRBY', 'key', '1', 'UNCOMPRESSED']
);
});
it('with CHUNK_SIZE', () => {
assert.deepEqual(
transformArguments('key', 1, {
CHUNK_SIZE: 100
}),
['TS.DECRBY', 'key', '1', 'CHUNK_SIZE', '100']
);
});
it('with LABELS', () => {
assert.deepEqual(
transformArguments('key', 1, {
LABELS: { label: 'value' }
}),
['TS.DECRBY', 'key', '1', 'LABELS', 'label', 'value']
);
});
it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => {
assert.deepEqual(
transformArguments('key', 1, {
TIMESTAMP: '*',
RETENTION: 1,
UNCOMPRESSED: true,
CHUNK_SIZE: 2,
LABELS: { label: 'value' }
}),
['TS.DECRBY', 'key', '1', 'TIMESTAMP', '*', 'RETENTION', '1', 'UNCOMPRESSED', 'CHUNK_SIZE', '2', 'LABELS', 'label', 'value']
);
});
});
testUtils.testWithClient('client.ts.decrBy', async client => {
assert.equal(
await client.ts.decrBy('key', 1, {
TIMESTAMP: 0
}),
0
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,21 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './DEL';
describe('DEL', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', '-', '+'),
['TS.DEL', 'key', '-', '+']
);
});
testUtils.testWithClient('client.ts.del', async client => {
await client.ts.create('key');
assert.equal(
await client.ts.del('key', '-', '+'),
0
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TimeSeriesAggregationType } from '.';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './DELETERULE';
describe('DELETERULE', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('source', 'destination'),
['TS.DELETERULE', 'source', 'destination']
);
});
testUtils.testWithClient('client.ts.deleteRule', async client => {
await Promise.all([
client.ts.create('source'),
client.ts.create('destination'),
client.ts.createRule('source', 'destination', TimeSeriesAggregationType.AVARAGE, 1)
]);
assert.equal(
await client.ts.deleteRule('source', 'destination'),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,4 +1,6 @@
export function transformArguments(sourceKey: string,destinationKey: string,): Array<string> { export const FIRST_KEY_INDEX = 1;
export function transformArguments(sourceKey: string, destinationKey: string): Array<string> {
return [ return [
'TS.DELETERULE', 'TS.DELETERULE',
sourceKey, sourceKey,

View File

@@ -0,0 +1,35 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './GET';
describe('GET', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key'),
['TS.GET', 'key']
);
});
describe('client.ts.get', () => {
testUtils.testWithClient('null', async client => {
await client.ts.create('key');
assert.equal(
await client.ts.get('key'),
null
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('with samples', async client => {
await client.ts.add('key', 0, 1);
assert.deepEqual(
await client.ts.get('key'),
{
timestamp: 0,
value: 1
}
);
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -0,0 +1,91 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './INCRBY';
describe('INCRBY', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('key', 1),
['TS.INCRBY', 'key', '1']
);
});
it('with TIMESTAMP', () => {
assert.deepEqual(
transformArguments('key', 1, {
TIMESTAMP: '*'
}),
['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*']
);
});
it('with RETENTION', () => {
assert.deepEqual(
transformArguments('key', 1, {
RETENTION: 1
}),
['TS.INCRBY', 'key', '1', 'RETENTION', '1']
);
});
it('with UNCOMPRESSED', () => {
assert.deepEqual(
transformArguments('key', 1, {
UNCOMPRESSED: true
}),
['TS.INCRBY', 'key', '1', 'UNCOMPRESSED']
);
});
it('without UNCOMPRESSED', () => {
assert.deepEqual(
transformArguments('key', 1, {
UNCOMPRESSED: false
}),
['TS.INCRBY', 'key', '1']
);
});
it('with CHUNK_SIZE', () => {
assert.deepEqual(
transformArguments('key', 1, {
CHUNK_SIZE: 1
}),
['TS.INCRBY', 'key', '1', 'CHUNK_SIZE', '1']
);
});
it('with LABELS', () => {
assert.deepEqual(
transformArguments('key', 1, {
LABELS: { label: 'value' }
}),
['TS.INCRBY', 'key', '1', 'LABELS', 'label', 'value']
);
});
it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => {
assert.deepEqual(
transformArguments('key', 1, {
TIMESTAMP: '*',
RETENTION: 1,
UNCOMPRESSED: true,
CHUNK_SIZE: 1,
LABELS: { label: 'value' }
}),
['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*', 'RETENTION', '1', 'UNCOMPRESSED',
'CHUNK_SIZE', '1', 'LABELS', 'label', 'value']
);
});
});
testUtils.testWithClient('client.ts.incrBy', async client => {
assert.equal(
await client.ts.incrBy('key', 1, {
TIMESTAMP: 0
}),
0
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,50 @@
import { strict as assert } from 'assert';
import { TimeSeriesAggregationType, TimeSeriesDuplicatePolicies } from '.';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './INFO';
describe('INFO', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key'),
['TS.INFO', 'key']
);
});
testUtils.testWithClient('client.ts.info', async client => {
await Promise.all([
client.ts.create('key', {
LABELS: { id: "2" },
DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.LAST
}),
client.ts.create('key2'),
client.ts.createRule('key', 'key2', TimeSeriesAggregationType.COUNT, 5),
client.ts.add('key', 1, 10)
]);
assert.deepEqual(
await client.ts.info('key'),
{
totalSamples: 1,
memoryUsage: 4261,
firstTimestamp: 1,
lastTimestamp: 1,
retentionTime: 0,
chunkCount: 1,
chunkSize: 4096,
chunkType: 'compressed',
duplicatePolicy: 'last',
labels: [{
name: 'id',
value: '2'
}],
rules: [{
aggregationType: 'COUNT',
key: 'key2',
timeBucket: 5
}],
sourceKey: null
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,57 @@
import { strict as assert } from 'assert';
import { TimeSeriesAggregationType, TimeSeriesDuplicatePolicies } from '.';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './INFO_DEBUG';
describe('INFO_DEBUG', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key'),
['TS.INFO', 'key', 'DEBUG']
);
});
testUtils.testWithClient('client.ts.get', async client => {
await Promise.all([
client.ts.create('key', {
LABELS: { id: "2" },
DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.LAST
}),
client.ts.create('key2'),
client.ts.createRule('key', 'key2', TimeSeriesAggregationType.COUNT, 5),
client.ts.add('key', 1, 10)
]);
assert.deepEqual(
await client.ts.infoDebug('key'),
{
totalSamples: 1,
memoryUsage: 4261,
firstTimestamp: 1,
lastTimestamp: 1,
retentionTime: 0,
chunkCount: 1,
chunkSize: 4096,
chunkType: 'compressed',
duplicatePolicy: 'last',
labels: [{
name: 'id',
value: '2'
}],
sourceKey: null,
rules: [{
aggregationType: 'COUNT',
key: 'key2',
timeBucket: 5
}],
chunks: [{
startTimestamp: 1,
endTimestamp: 1,
samples: 1,
size: 4096,
bytesPerSample: '4096'
}]
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,39 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './MADD';
describe('MADD', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments([{
key: '1',
timestamp: 0,
value: 0
}, {
key: '2',
timestamp: 1,
value: 1
}]),
['TS.MADD', '1', '0', '0', '2', '1', '1']
);
});
// Should we check empty array?
testUtils.testWithClient('client.ts.mAdd', async client => {
await client.ts.create('key');
assert.deepEqual(
await client.ts.mAdd([{
key: 'key',
timestamp: 0,
value: 0
}, {
key: 'key',
timestamp: 1,
value: 1
}]),
[0, 1]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,5 +1,7 @@
import { Timestamp, transformTimestampArgument } from '.'; import { Timestamp, transformTimestampArgument } from '.';
export const FIRST_KEY_INDEX = 1;
interface MAddSample { interface MAddSample {
key: string; key: string;
timestamp: Timestamp; timestamp: Timestamp;

View File

@@ -0,0 +1,29 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './MGET';
describe('MGET', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('label=value'),
['TS.MGET', 'FILTER', 'label=value']
);
});
testUtils.testWithClient('client.ts.mGet', async client => {
await client.ts.add('key', 0, 0, {
LABELS: { label: 'value' }
});
assert.deepEqual(
await client.ts.mGet('label=value'),
[{
key: 'key',
sample: {
timestamp: 0,
value: 0
}
}]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,31 +1,26 @@
import { pushVerdictArgument, pushVerdictArguments } from '@node-redis/client/lib/commands/generic-transformers'; import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { Filter, pushFilterArgument, RawLabels, SampleRawReply, SampleReply, transformSampleReply } from '.';
export const IS_READ_ONLY = true; export const IS_READ_ONLY = true;
interface WithLabels { export function transformArguments(filter: Filter): RedisCommandArguments {
WITHLABELS: true; return pushFilterArgument(['TS.MGET'], filter);
} }
interface SelectedLabels { export type MGetRawReply = Array<[
SELECTED_LABELS: string | Array<string>; key: string,
labels: RawLabels,
sample: SampleRawReply
]>;
export interface MGetReply {
key: string,
sample: SampleReply
} }
type MGetOptions = WithLabels & SelectedLabels; export function transformReply(reply: MGetRawReply): Array<MGetReply> {
return reply.map(([key, _, sample]) => ({
export function transformArguments(filter: string, options?: MGetOptions): Array<string> { key,
const args = ['TS.MGET']; sample: transformSampleReply(sample)
}));
if (options?.WITHLABELS) {
args.push('WITHLABELS');
} else if (options?.SELECTED_LABELS) {
pushVerdictArguments(args, options.SELECTED_LABELS);
}
args.push('FILTER', filter);
return args;
}
export function transformReply() {
} }

View File

@@ -0,0 +1,39 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './MGET_WITHLABELS';
describe('MGET_WITHLABELS', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('label=value'),
['TS.MGET', 'WITHLABELS', 'FILTER', 'label=value']
);
});
it('with SELECTED_LABELS', () => {
assert.deepEqual(
transformArguments('label=value', { SELECTED_LABELS: 'label' }),
['TS.MGET', 'SELECTED_LABELS', 'label', 'FILTER', 'label=value']
);
});
});
testUtils.testWithClient('client.ts.mGetWithLabels', async client => {
await client.ts.add('key', 0, 0, {
LABELS: { label: 'value' }
});
assert.deepEqual(
await client.ts.mGetWithLabels('label=value'),
[{
key: 'key',
labels: { label: 'value'},
sample: {
timestamp: 0,
value: 0
}
}]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,41 @@
import {
SelectedLabels,
pushWithLabelsArgument,
Labels,
transformLablesReply,
transformSampleReply,
Filter,
pushFilterArgument
} from '.';
import { MGetRawReply, MGetReply } from './MGET';
export const IS_READ_ONLY = true;
interface MGetWithLabelsOptions {
SELECTED_LABELS?: SelectedLabels;
}
export function transformArguments(
filter: Filter,
options?: MGetWithLabelsOptions
): Array<string> {
const args = ['TS.MGET'];
pushWithLabelsArgument(args, options?.SELECTED_LABELS);
pushFilterArgument(args, filter);
return args;
}
export interface MGetWithLabelsReply extends MGetReply {
labels: Labels;
};
export function transformReply(reply: MGetRawReply): Array<MGetWithLabelsReply> {
return reply.map(([key, labels, sample]) => ({
key,
labels: transformLablesReply(labels),
sample: transformSampleReply(sample)
}));
}

View File

@@ -0,0 +1,50 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './MRANGE';
import { TimeSeriesAggregationType, TimeSeriesReducers } from '.';
describe('MRANGE', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('-', '+', 'label=value', {
FILTER_BY_TS: [0],
FILTER_BY_VALUE: {
min: 0,
max: 1
},
COUNT: 1,
ALIGN: '-',
AGGREGATION: {
type: TimeSeriesAggregationType.AVARAGE,
timeBucket: 1
},
GROUPBY: {
label: 'label',
reducer: TimeSeriesReducers.SUM
},
}),
['TS.MRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1',
'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'avg', '1', 'FILTER', 'label=value',
'GROUPBY', 'label', 'REDUCE', 'sum']
);
});
testUtils.testWithClient('client.ts.mRange', async client => {
await client.ts.add('key', 0, 0, {
LABELS: { label: 'value'}
});
assert.deepEqual(
await client.ts.mRange('-', '+', 'label=value', {
COUNT: 1
}),
[{
key: 'key',
samples: [{
timestamp: 0,
value: 0
}]
}]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,21 @@
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { MRangeOptions, Timestamp, pushMRangeArguments, Filter } from '.';
export const IS_READ_ONLY = true;
export function transformArguments(
fromTimestamp: Timestamp,
toTimestamp: Timestamp,
filters: Filter,
options?: MRangeOptions
): RedisCommandArguments {
return pushMRangeArguments(
['TS.MRANGE'],
fromTimestamp,
toTimestamp,
filters,
options
);
}
export { transformMRangeReply as transformReply } from '.';

View File

@@ -0,0 +1,52 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './MRANGE_WITHLABELS';
import { TimeSeriesAggregationType, TimeSeriesReducers } from '.';
describe('MRANGE_WITHLABELS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('-', '+', 'label=value', {
FILTER_BY_TS: [0],
FILTER_BY_VALUE: {
min: 0,
max: 1
},
SELECTED_LABELS: ['label'],
COUNT: 1,
ALIGN: '-',
AGGREGATION: {
type: TimeSeriesAggregationType.AVARAGE,
timeBucket: 1
},
GROUPBY: {
label: 'label',
reducer: TimeSeriesReducers.SUM
},
}),
['TS.MRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1',
'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'avg', '1', 'SELECTED_LABELS', 'label',
'FILTER', 'label=value', 'GROUPBY', 'label', 'REDUCE', 'sum']
);
});
testUtils.testWithClient('client.ts.mRangeWithLabels', async client => {
await client.ts.add('key', 0, 0, {
LABELS: { label: 'value'}
});
assert.deepEqual(
await client.ts.mRangeWithLabels('-', '+', 'label=value', {
COUNT: 1
}),
[{
key: 'key',
labels: { label: 'value' },
samples: [{
timestamp: 0,
value: 0
}]
}]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,21 @@
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { Timestamp, MRangeWithLabelsOptions, pushMRangeWithLabelsArguments } from '.';
export const IS_READ_ONLY = true;
export function transformArguments(
fromTimestamp: Timestamp,
toTimestamp: Timestamp,
filters: string | Array<string>,
options?: MRangeWithLabelsOptions
): RedisCommandArguments {
return pushMRangeWithLabelsArguments(
['TS.MRANGE'],
fromTimestamp,
toTimestamp,
filters,
options
);
}
export { transformMRangeWithLabelsReply as transformReply } from '.';

View File

@@ -0,0 +1,50 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './MREVRANGE';
import { TimeSeriesAggregationType, TimeSeriesReducers } from '.';
describe('MREVRANGE', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('-', '+', 'label=value', {
FILTER_BY_TS: [0],
FILTER_BY_VALUE: {
min: 0,
max: 1
},
COUNT: 1,
ALIGN: '-',
AGGREGATION: {
type: TimeSeriesAggregationType.AVARAGE,
timeBucket: 1
},
GROUPBY: {
label: 'label',
reducer: TimeSeriesReducers.SUM
},
}),
['TS.MREVRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1',
'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'avg', '1', 'FILTER', 'label=value',
'GROUPBY', 'label', 'REDUCE', 'sum']
);
});
testUtils.testWithClient('client.ts.mRevRange', async client => {
await client.ts.add('key', 0, 0, {
LABELS: { label: 'value'}
});
assert.deepEqual(
await client.ts.mRevRange('-', '+', 'label=value', {
COUNT: 1
}),
[{
key: 'key',
samples: [{
timestamp: 0,
value: 0
}]
}]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,21 @@
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { MRangeOptions, Timestamp, pushMRangeArguments, Filter } from '.';
export const IS_READ_ONLY = true;
export function transformArguments(
fromTimestamp: Timestamp,
toTimestamp: Timestamp,
filters: Filter,
options?: MRangeOptions
): RedisCommandArguments {
return pushMRangeArguments(
['TS.MREVRANGE'],
fromTimestamp,
toTimestamp,
filters,
options
);
}
export { transformMRangeReply as transformReply } from '.';

View File

@@ -0,0 +1,52 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './MREVRANGE_WITHLABELS';
import { TimeSeriesAggregationType, TimeSeriesReducers } from '.';
describe('MREVRANGE_WITHLABELS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('-', '+', 'label=value', {
FILTER_BY_TS: [0],
FILTER_BY_VALUE: {
min: 0,
max: 1
},
SELECTED_LABELS: ['label'],
COUNT: 1,
ALIGN: '-',
AGGREGATION: {
type: TimeSeriesAggregationType.AVARAGE,
timeBucket: 1
},
GROUPBY: {
label: 'label',
reducer: TimeSeriesReducers.SUM
},
}),
['TS.MREVRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1',
'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'avg', '1', 'SELECTED_LABELS', 'label',
'FILTER', 'label=value', 'GROUPBY', 'label', 'REDUCE', 'sum']
);
});
testUtils.testWithClient('client.ts.mRevRangeWithLabels', async client => {
await client.ts.add('key', 0, 0, {
LABELS: { label: 'value'}
});
assert.deepEqual(
await client.ts.mRevRangeWithLabels('-', '+', 'label=value', {
COUNT: 1
}),
[{
key: 'key',
labels: { label: 'value' },
samples: [{
timestamp: 0,
value: 0
}]
}]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,21 @@
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { Timestamp, MRangeWithLabelsOptions, pushMRangeWithLabelsArguments, Filter } from '.';
export const IS_READ_ONLY = true;
export function transformArguments(
fromTimestamp: Timestamp,
toTimestamp: Timestamp,
filters: Filter,
options?: MRangeWithLabelsOptions
): RedisCommandArguments {
return pushMRangeWithLabelsArguments(
['TS.MREVRANGE'],
fromTimestamp,
toTimestamp,
filters,
options
);
}
export { transformMRangeWithLabelsReply as transformReply } from '.';

View File

@@ -0,0 +1,27 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './QUERYINDEX';
describe('QUERYINDEX', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('*'),
['TS.QUERYINDEX', '*']
);
});
testUtils.testWithClient('client.ts.queryIndex', async client => {
await Promise.all([
client.ts.create('key', {
LABELS: {
label: 'value'
}
})
]);
assert.deepEqual(
await client.ts.queryIndex('label=value'),
['key']
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,38 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './RANGE';
import { TimeSeriesAggregationType } from '.';
describe('RANGE', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', '-', '+', {
FILTER_BY_TS: [0],
FILTER_BY_VALUE: {
min: 1,
max: 2
},
COUNT: 1,
ALIGN: '-',
AGGREGATION: {
type: TimeSeriesAggregationType.AVARAGE,
timeBucket: 1
}
}),
['TS.RANGE', 'key', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE',
'1', '2', 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'avg', '1']
);
});
testUtils.testWithClient('client.ts.range', async client => {
await client.ts.add('key', 1, 2);
assert.deepEqual(
await client.ts.range('key', '-', '+'),
[{
timestamp: 1,
value: 2
}]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,21 +1,24 @@
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands'; import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { RangeOptions, Timestamp, pushRangeArguments } from '.'; import { RangeOptions, Timestamp, pushRangeArguments, SampleRawReply, SampleReply, transformRangeReply } from '.';
export const FIRST_KEY_INDEX = 1; export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true; export const IS_READ_ONLY = true;
export function transformArguments( export function transformArguments(
key: string,
fromTimestamp: Timestamp, fromTimestamp: Timestamp,
toTimestamp: Timestamp, toTimestamp: Timestamp,
options?: RangeOptions options?: RangeOptions
): RedisCommandArguments { ): RedisCommandArguments {
return pushRangeArguments( return pushRangeArguments(
['TS.RANGE'], ['TS.RANGE', key],
fromTimestamp, fromTimestamp,
toTimestamp, toTimestamp,
options options
); );
} }
export { transformRangeReply } from '.'; export function transformReply(reply: Array<SampleRawReply>): Array<SampleReply> {
return transformRangeReply(reply);
}

View File

@@ -0,0 +1,106 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './REVRANGE';
import { TimeSeriesAggregationType } from '.';
describe('REVRANGE', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('key', '-', '+'),
['TS.REVRANGE', 'key', '-', '+']
);
});
it('with FILTER_BY_TS', () => {
assert.deepEqual(
transformArguments('key', '-', '+', {
FILTER_BY_TS: [0]
}),
['TS.REVRANGE', 'key', '-', '+', 'FILTER_BY_TS', '0']
);
});
it('with FILTER_BY_VALUE', () => {
assert.deepEqual(
transformArguments('key', '-', '+', {
FILTER_BY_VALUE: {
min: 1,
max: 2
}
}),
['TS.REVRANGE', 'key', '-', '+', 'FILTER_BY_VALUE', '1', '2']
);
});
it('with COUNT', () => {
assert.deepEqual(
transformArguments('key', '-', '+', {
COUNT: 1
}),
['TS.REVRANGE', 'key', '-', '+', 'COUNT', '1']
);
});
it('with ALIGN', () => {
assert.deepEqual(
transformArguments('key', '-', '+', {
ALIGN: '-'
}),
['TS.REVRANGE', 'key', '-', '+', 'ALIGN', '-']
);
});
it('with AGGREGATION', () => {
assert.deepEqual(
transformArguments('key', '-', '+', {
AGGREGATION: {
type: TimeSeriesAggregationType.AVARAGE,
timeBucket: 1
}
}),
['TS.REVRANGE', 'key', '-', '+', 'AGGREGATION', 'avg', '1']
);
});
it('with FILTER_BY_TS, FILTER_BY_VALUE, COUNT, ALIGN, AGGREGATION', () => {
assert.deepEqual(
transformArguments('key', '-', '+', {
FILTER_BY_TS: [0],
FILTER_BY_VALUE: {
min: 1,
max: 2
},
COUNT: 1,
ALIGN: '-',
AGGREGATION: {
type: TimeSeriesAggregationType.AVARAGE,
timeBucket: 1
}
}),
[
'TS.REVRANGE', 'key', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE',
'1', '2', 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'avg', '1'
]
);
});
});
testUtils.testWithClient('client.ts.revRange', async client => {
await Promise.all([
client.ts.add('key', 0, 1),
client.ts.add('key', 1, 2)
]);
assert.deepEqual(
await client.ts.revRange('key', '-', '+'),
[{
timestamp: 1,
value: 2
}, {
timestamp: 0,
value: 1
}]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,21 +1,24 @@
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands'; import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { RangeOptions, Timestamp, pushRangeArguments } from '.'; import { RangeOptions, Timestamp, pushRangeArguments, SampleRawReply, SampleReply, transformRangeReply } from '.';
export const FIRST_KEY_INDEX = 1; export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true; export const IS_READ_ONLY = true;
export function transformArguments( export function transformArguments(
key: string,
fromTimestamp: Timestamp, fromTimestamp: Timestamp,
toTimestamp: Timestamp, toTimestamp: Timestamp,
options?: RangeOptions options?: RangeOptions
): RedisCommandArguments { ): RedisCommandArguments {
return pushRangeArguments( return pushRangeArguments(
['TS.REVRANGE'], ['TS.REVRANGE', key],
fromTimestamp, fromTimestamp,
toTimestamp, toTimestamp,
options options
); );
} }
export { transformRangeReply } from '.'; export function transformReply(reply: Array<SampleRawReply>): Array<SampleReply> {
return transformRangeReply(reply);
}

View File

@@ -9,9 +9,17 @@ import {
pushLabelsArgument, pushLabelsArgument,
transformIncrDecrArguments, transformIncrDecrArguments,
transformSampleReply, transformSampleReply,
TimeSeriesAggregationType,
pushRangeArguments, pushRangeArguments,
pushMRangeGroupByArguments,
TimeSeriesReducers,
pushFilterArgument,
pushMRangeArguments,
pushWithLabelsArgument,
pushMRangeWithLabelsArguments,
transformRangeReply, transformRangeReply,
TimeSeriesAggregationType transformMRangeReply,
transformMRangeWithLabelsReply
} from '.'; } from '.';
describe('transformTimestampArgument', () => { describe('transformTimestampArgument', () => {
@@ -115,6 +123,15 @@ describe('transformIncrDecrArguments', () => {
['TS.INCRBY', 'key', '1', 'UNCOMPRESSED'] ['TS.INCRBY', 'key', '1', 'UNCOMPRESSED']
); );
}); });
it('with UNCOMPRESSED false', () => {
assert.deepEqual(
transformIncrDecrArguments('TS.INCRBY', 'key', 1, {
UNCOMPRESSED: false
}),
['TS.INCRBY', 'key', '1']
);
});
}); });
it('transformSampleReply', () => { it('transformSampleReply', () => {
@@ -139,7 +156,7 @@ describe('pushRangeArguments', () => {
it('string', () => { it('string', () => {
assert.deepEqual( assert.deepEqual(
pushRangeArguments([], '-', '+', { pushRangeArguments([], '-', '+', {
FILTER_BY_TS: 'ts' FILTER_BY_TS: ['ts']
}), }),
['-', '+', 'FILTER_BY_TS', 'ts'] ['-', '+', 'FILTER_BY_TS', 'ts']
); );
@@ -200,7 +217,7 @@ describe('pushRangeArguments', () => {
it('with FILTER_BY_TS, FILTER_BY_VALUE, COUNT, ALIGN, AGGREGATION', () => { it('with FILTER_BY_TS, FILTER_BY_VALUE, COUNT, ALIGN, AGGREGATION', () => {
assert.deepEqual( assert.deepEqual(
pushRangeArguments([], '-', '+', { pushRangeArguments([], '-', '+', {
FILTER_BY_TS: 'ts', FILTER_BY_TS: ['ts'],
FILTER_BY_VALUE: { FILTER_BY_VALUE: {
min: 1, min: 1,
max: 2 max: 2
@@ -212,11 +229,91 @@ describe('pushRangeArguments', () => {
timeBucket: 1 timeBucket: 1
} }
}), }),
['-', '+', 'FILTER_BY_TS', 'ts', 'FILTER_BY_VALUE', '1', '2', 'COUNT', '1', 'ALIGN', '1', 'AGGREGATION', 'first', '1'] ['-', '+', 'FILTER_BY_TS', 'ts', 'FILTER_BY_VALUE', '1', '2',
'COUNT', '1', 'ALIGN', '1', 'AGGREGATION', 'first', '1']
); );
}); });
}); });
describe('pushMRangeGroupByArguments', () => {
it('undefined', () => {
assert.deepEqual(
pushMRangeGroupByArguments([]),
[]
);
});
it('with GROUPBY', () => {
assert.deepEqual(
pushMRangeGroupByArguments([], {
label: 'label',
reducer: TimeSeriesReducers.MAXIMUM
}),
['GROUPBY', 'label', 'REDUCE', 'max']
);
});
});
describe('pushFilterArgument', () => {
it('string', () => {
assert.deepEqual(
pushFilterArgument([], 'label=value'),
['FILTER', 'label=value']
);
});
it('Array', () => {
assert.deepEqual(
pushFilterArgument([], ['1=1', '2=2']),
['FILTER', '1=1', '2=2']
);
});
});
describe('pushMRangeArguments', () => {
it('without options', () => {
assert.deepEqual(
pushMRangeArguments([], '-', '+', 'label=value'),
['-', '+', 'FILTER', 'label=value']
);
});
it('with GROUPBY', () => {
assert.deepEqual(
pushMRangeArguments([], '-', '+', 'label=value', {
GROUPBY: {
label: 'label',
reducer: TimeSeriesReducers.MAXIMUM
}
}),
['-', '+', 'FILTER', 'label=value', 'GROUPBY', 'label', 'REDUCE', 'max']
);
});
});
describe('pushWithLabelsArgument', () => {
it('without selected labels', () => {
assert.deepEqual(
pushWithLabelsArgument([]),
['WITHLABELS']
);
});
it('with selected labels', () => {
assert.deepEqual(
pushWithLabelsArgument([], ['label']),
['SELECTED_LABELS', 'label']
);
});
});
it('pushMRangeWithLabelsArguments', () => {
assert.deepEqual(
pushMRangeWithLabelsArguments([], '-', '+', 'label=value'),
['-', '+', 'WITHLABELS', 'FILTER', 'label=value']
);
});
it('transformRangeReply', () => { it('transformRangeReply', () => {
assert.deepEqual( assert.deepEqual(
transformRangeReply([[1, '1.1'], [2, '2.2']]), transformRangeReply([[1, '1.1'], [2, '2.2']]),
@@ -229,3 +326,46 @@ it('transformRangeReply', () => {
}] }]
); );
}); });
describe('transformMRangeReply', () => {
assert.deepEqual(
transformMRangeReply([[
'key',
[],
[[1, '1.1'], [2, '2.2']]
]]),
[{
key: 'key',
samples: [{
timestamp: 1,
value: 1.1
}, {
timestamp: 2,
value: 2.2
}]
}]
);
});
describe('transformMRangeWithLabelsReply', () => {
assert.deepEqual(
transformMRangeWithLabelsReply([[
'key',
[['label', 'value']],
[[1, '1.1'], [2, '2.2']]
]]),
[{
key: 'key',
labels: {
label: 'value'
},
samples: [{
timestamp: 1,
value: 1.1
}, {
timestamp: 2,
value: 2.2
}]
}]
);
});

View File

@@ -11,9 +11,14 @@ import * as INFO_DEBUG from './INFO_DEBUG';
import * as INFO from './INFO'; import * as INFO from './INFO';
import * as MADD from './MADD'; import * as MADD from './MADD';
import * as MGET from './MGET'; import * as MGET from './MGET';
import * as MGET_WITHLABELS from './MGET_WITHLABELS';
import * as QUERYINDEX from './QUERYINDEX'; import * as QUERYINDEX from './QUERYINDEX';
import * as RANGE from './RANGE'; import * as RANGE from './RANGE';
import * as REVRANGE from './REVRANGE'; import * as REVRANGE from './REVRANGE';
import * as MRANGE from './MRANGE';
import * as MRANGE_WITHLABELS from './MRANGE_WITHLABELS';
import * as MREVRANGE from './MREVRANGE';
import * as MREVRANGE_WITHLABELS from './MREVRANGE_WITHLABELS';
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands'; import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { pushVerdictArguments } from '@node-redis/client/lib/commands/generic-transformers'; import { pushVerdictArguments } from '@node-redis/client/lib/commands/generic-transformers';
@@ -44,12 +49,22 @@ export default {
mAdd: MADD, mAdd: MADD,
MGET, MGET,
mGet: MGET, mGet: MGET,
MGET_WITHLABELS,
mGetWithLabels: MGET_WITHLABELS,
QUERYINDEX, QUERYINDEX,
queryIndex: QUERYINDEX, queryIndex: QUERYINDEX,
RANGE, RANGE,
range: RANGE, range: RANGE,
REVRANGE, REVRANGE,
revRange: REVRANGE revRange: REVRANGE,
MRANGE,
mRange: MRANGE,
MRANGE_WITHLABELS,
mRangeWithLabels: MRANGE_WITHLABELS,
MREVRANGE,
mRevRange: MREVRANGE,
MREVRANGE_WITHLABELS,
mRevRangeWithLabels: MREVRANGE_WITHLABELS
}; };
export enum TimeSeriesAggregationType { export enum TimeSeriesAggregationType {
@@ -67,6 +82,21 @@ export enum TimeSeriesAggregationType {
VAR_S = 'var.s' VAR_S = 'var.s'
} }
export enum TimeSeriesDuplicatePolicies {
BLOCK = 'BLOCK',
FIRST = 'FIRST',
LAST = 'LAST',
MIN = 'MIN',
MAX = 'MAX',
SUM = 'SUM'
}
export enum TimeSeriesReducers {
SUM = 'sum',
MINIMUM = 'min',
MAXIMUM = 'max',
}
export type Timestamp = number | Date | string; export type Timestamp = number | Date | string;
export function transformTimestampArgument(timestamp: Timestamp): string { export function transformTimestampArgument(timestamp: Timestamp): string {
@@ -117,19 +147,22 @@ export function pushChunkSizeArgument(args: RedisCommandArguments, chunkSize?: n
return args; return args;
} }
export enum TimeSeriesDuplicatePolicies { export type RawLabels = Array<[label: string, value: string]>;
BLOCK = 'BLOCK',
FIRST = 'FIRST',
LAST = 'LAST',
MIN = 'MIN',
MAX = 'MAX',
SUM = 'SUM'
}
export type Labels = { export type Labels = {
[label: string]: string; [label: string]: string;
}; };
export function transformLablesReply(reply: RawLabels): Labels {
const labels: Labels = {};
for (const [key, value] of reply) {
labels[key] = value;
}
return labels
}
export function pushLabelsArgument(args: RedisCommandArguments, labels?: Labels): RedisCommandArguments { export function pushLabelsArgument(args: RedisCommandArguments, labels?: Labels): RedisCommandArguments {
if (labels) { if (labels) {
args.push('LABELS'); args.push('LABELS');
@@ -162,7 +195,7 @@ export function transformIncrDecrArguments(
value.toString() value.toString()
]; ];
if (options?.TIMESTAMP) { if (options?.TIMESTAMP !== undefined && options?.TIMESTAMP !== null) {
args.push('TIMESTAMP', transformTimestampArgument(options.TIMESTAMP)); args.push('TIMESTAMP', transformTimestampArgument(options.TIMESTAMP));
} }
@@ -194,7 +227,7 @@ export function transformSampleReply(reply: SampleRawReply): SampleReply {
} }
export interface RangeOptions { export interface RangeOptions {
FILTER_BY_TS?: string | Array<string>; FILTER_BY_TS?: Array<Timestamp>;
FILTER_BY_VALUE?: { FILTER_BY_VALUE?: {
min: number; min: number;
max: number; max: number;
@@ -220,7 +253,9 @@ export function pushRangeArguments(
if (options?.FILTER_BY_TS) { if (options?.FILTER_BY_TS) {
args.push('FILTER_BY_TS'); args.push('FILTER_BY_TS');
pushVerdictArguments(args, options.FILTER_BY_TS); for(const ts of options.FILTER_BY_TS) {
args.push(transformTimestampArgument(ts));
}
} }
if (options?.FILTER_BY_VALUE) { if (options?.FILTER_BY_VALUE) {
@@ -256,6 +291,121 @@ export function pushRangeArguments(
return args; return args;
} }
interface MRangeGroupBy {
label: string;
reducer: TimeSeriesReducers;
}
export function pushMRangeGroupByArguments(args: RedisCommandArguments, groupBy?: MRangeGroupBy): RedisCommandArguments {
if (groupBy) {
args.push(
'GROUPBY',
groupBy.label,
'REDUCE',
groupBy.reducer
);
}
return args;
}
export type Filter = string | Array<string>;
export function pushFilterArgument(args: RedisCommandArguments, filter: string | Array<string>): RedisCommandArguments {
args.push('FILTER');
pushVerdictArguments(args, filter);
return args;
}
export interface MRangeOptions extends RangeOptions {
GROUPBY?: MRangeGroupBy;
}
export function pushMRangeArguments(
args: RedisCommandArguments,
fromTimestamp: Timestamp,
toTimestamp: Timestamp,
filter: Filter,
options?: MRangeOptions
): RedisCommandArguments {
pushRangeArguments(args, fromTimestamp, toTimestamp, options);
pushFilterArgument(args, filter);
pushMRangeGroupByArguments(args, options?.GROUPBY);
return args;
}
export type SelectedLabels = string | Array<string>;
export function pushWithLabelsArgument(args: RedisCommandArguments, selectedLabels?: SelectedLabels): RedisCommandArguments {
if (!selectedLabels) {
args.push('WITHLABELS');
} else {
args.push('SELECTED_LABELS');
pushVerdictArguments(args, selectedLabels);
}
return args;
}
export interface MRangeWithLabelsOptions extends MRangeOptions {
SELECTED_LABELS?: SelectedLabels;
}
export function pushMRangeWithLabelsArguments(
args: RedisCommandArguments,
fromTimestamp: Timestamp,
toTimestamp: Timestamp,
filter: Filter,
options?: MRangeWithLabelsOptions
): RedisCommandArguments {
pushRangeArguments(args, fromTimestamp, toTimestamp, options);
pushWithLabelsArgument(args, options?.SELECTED_LABELS);
pushFilterArgument(args, filter);
pushMRangeGroupByArguments(args, options?.GROUPBY);
return args;
}
export function transformRangeReply(reply: Array<SampleRawReply>): Array<SampleReply> { export function transformRangeReply(reply: Array<SampleRawReply>): Array<SampleReply> {
return reply.map(transformSampleReply); return reply.map(transformSampleReply);
} }
type MRangeRawReply = Array<[
key: string,
labels: RawLabels,
samples: Array<SampleRawReply>
]>;
interface MRangeReplyItem {
key: string;
samples: Array<SampleReply>;
}
export function transformMRangeReply(reply: MRangeRawReply): Array<MRangeReplyItem> {
const args = [];
for (const [key, _, sample] of reply) {
args.push({
key,
samples: sample.map(transformSampleReply)
});
}
return args;
}
export interface MRangeWithLabelsReplyItem extends MRangeReplyItem {
labels: Labels;
}
export function transformMRangeWithLabelsReply(reply: MRangeRawReply): Array<MRangeWithLabelsReplyItem> {
const args = [];
for (const [key, labels, samples] of reply) {
args.push({
key,
labels: transformLablesReply(labels),
samples: samples.map(transformSampleReply)
});
}
return args;
}