Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions packages/time-series/lib/commands/NRANGE.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import NRANGE from './NRANGE';
import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
import { TIME_SERIES_BUCKET_TIMESTAMP } from './RANGE';
import { parseArgs } from '@redis/client/lib/commands/generic-transformers';

describe('TS.NRANGE', () => {
it('transformArguments (minimal)', () => {
assert.deepEqual(
parseArgs(NRANGE, ['a', 'b', 'c'], '-', '+'),
['TS.NRANGE', '3', 'a', 'b', 'c', '-', '+']
);
});

it('transformArguments preserves key order and duplicates', () => {
assert.deepEqual(
parseArgs(NRANGE, ['k', 'k'], '-', '+'),
['TS.NRANGE', '2', 'k', 'k', '-', '+']
);
});

it('transformArguments (all options, aggregators as separate tokens)', () => {
assert.deepEqual(
parseArgs(NRANGE, ['a', 'b', 'c'], '-', '+', {
LATEST: true,
FILTER_BY_TS: [0, 1],
FILTER_BY_VALUE: {
min: 1,
max: 2
},
COUNT: 1,
ALIGN: '-',
AGGREGATION: {
types: [
TIME_SERIES_AGGREGATION_TYPE.FIRST,
TIME_SERIES_AGGREGATION_TYPE.MAX,
TIME_SERIES_AGGREGATION_TYPE.MIN
],
timeBucket: 10000,
BUCKETTIMESTAMP: TIME_SERIES_BUCKET_TIMESTAMP.LOW,
EMPTY: true
}
}),
[
'TS.NRANGE', '3', 'a', 'b', 'c', '-', '+', 'LATEST',
'FILTER_BY_TS', '0', '1', 'FILTER_BY_VALUE', '1', '2', 'COUNT', '1',
'ALIGN', '-', 'AGGREGATION', 'FIRST', 'MAX', 'MIN', '10000',
'BUCKETTIMESTAMP', '-', 'EMPTY'
]
);
});

testUtils.testWithClient('client.ts.nRange (raw)', async client => {
await Promise.all([
client.ts.create('{t}:1'),
client.ts.create('{t}:2')
]);
await Promise.all([
client.ts.add('{t}:1', 1000, 10),
client.ts.add('{t}:1', 2000, 12),
client.ts.add('{t}:2', 1000, 13)
]);

assert.deepEqual(
await client.ts.nRange(['{t}:1', '{t}:2'], '-', '+'),
[
{ timestamp: 1000, values: [10, 13] },
{ timestamp: 2000, values: [12, NaN] }
]
);
}, {
...GLOBAL.SERVERS.OPEN,
minimumDockerVersion: [8, 10]
});

testUtils.testWithClient('client.ts.nRange (aggregation)', async client => {
await Promise.all([
client.ts.create('{t}:1'),
client.ts.create('{t}:2')
]);
await Promise.all([
client.ts.add('{t}:1', 1000, 10),
client.ts.add('{t}:1', 1500, 20),
client.ts.add('{t}:2', 1000, 5)
]);

const reply = await client.ts.nRange(['{t}:1', '{t}:2'], 0, 3000, {
AGGREGATION: {
types: [
TIME_SERIES_AGGREGATION_TYPE.MAX,
TIME_SERIES_AGGREGATION_TYPE.MIN
],
timeBucket: 1000
}
});

assert.deepEqual(reply, [
{ timestamp: 1000, values: [20, 5] }
]);
}, {
...GLOBAL.SERVERS.OPEN,
minimumDockerVersion: [8, 10]
});

testUtils.testWithClient('client.ts.nRange (RESP2 surfaces missing cell as NaN)', async client => {
await Promise.all([
client.ts.create('{t}:1'),
client.ts.create('{t}:2')
]);
await Promise.all([
client.ts.add('{t}:1', 1000, 10),
client.ts.add('{t}:1', 2000, 12),
client.ts.add('{t}:2', 1000, 13)
]);

assert.deepEqual(
await client.ts.nRange(['{t}:1', '{t}:2'], '-', '+'),
[
{ timestamp: 1000, values: [10, 13] },
{ timestamp: 2000, values: [12, NaN] }
]
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
...GLOBAL.SERVERS.OPEN.clientOptions,
RESP: 2
},
minimumDockerVersion: [8, 10]
});

testUtils.testWithClient('client.ts.nRange (empty result)', async client => {
await client.ts.create('{t}:1');

assert.deepEqual(
await client.ts.nRange(['{t}:1'], 5000, 6000),
[]
);
}, {
...GLOBAL.SERVERS.OPEN,
minimumDockerVersion: [8, 10]
});
});
79 changes: 79 additions & 0 deletions packages/time-series/lib/commands/NRANGE.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { CommandParser } from '@redis/client/dist/lib/client/parser';
import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
import {
Timestamp,
transformTimestampArgument,
TsRangeCommonOptions,
parseRangeCommonArguments,
transformPivotSamplesReply
} from './helpers';
import { TimeSeriesBucketTimestamp } from './RANGE';
import { TimeSeriesAggregationTypeList } from './RANGE_MULTIAGGR';

export interface TsNRangeOptions extends TsRangeCommonOptions {
ALIGN?: Timestamp;
AGGREGATION?: {
/**
* One aggregator per key argument; length must equal the key list length.
* Emitted as separate tokens (never the comma-joined form).
*/
types: TimeSeriesAggregationTypeList;
timeBucket: Timestamp;
BUCKETTIMESTAMP?: TimeSeriesBucketTimestamp;
EMPTY?: boolean;
};
}

export function parseNRangeArguments(
parser: CommandParser,
fromTimestamp: Timestamp,
toTimestamp: Timestamp,
options?: TsNRangeOptions
) {
parseRangeCommonArguments(parser, fromTimestamp, toTimestamp, options);

if (options?.AGGREGATION) {
if (options?.ALIGN !== undefined) {
parser.push('ALIGN', transformTimestampArgument(options.ALIGN));
}

parser.push('AGGREGATION');
for (const type of options.AGGREGATION.types) {
parser.push(type);
}
parser.push(transformTimestampArgument(options.AGGREGATION.timeBucket));

if (options.AGGREGATION.BUCKETTIMESTAMP) {
parser.push(
'BUCKETTIMESTAMP',
options.AGGREGATION.BUCKETTIMESTAMP
);
}

if (options.AGGREGATION.EMPTY) {
parser.push('EMPTY');
}
}
}

export function transformNRangeArguments(
parser: CommandParser,
keys: Array<RedisArgument>,
fromTimestamp: Timestamp,
toTimestamp: Timestamp,
options?: TsNRangeOptions
) {
parser.pushKeysLength(keys);
parseNRangeArguments(parser, fromTimestamp, toTimestamp, options);
}

export default {
IS_READ_ONLY: true,
parseCommand(...args: Parameters<typeof transformNRangeArguments>) {
const parser = args[0];

parser.push('TS.NRANGE');
transformNRangeArguments(...args);
},
transformReply: transformPivotSamplesReply
} as const satisfies Command;
55 changes: 55 additions & 0 deletions packages/time-series/lib/commands/NREVRANGE.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import NREVRANGE from './NREVRANGE';
import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
import { parseArgs } from '@redis/client/lib/commands/generic-transformers';

describe('TS.NREVRANGE', () => {
it('transformArguments (minimal)', () => {
assert.deepEqual(
parseArgs(NREVRANGE, ['a', 'b', 'c'], '-', '+'),
['TS.NREVRANGE', '3', 'a', 'b', 'c', '-', '+']
);
});

it('transformArguments (aggregators as separate tokens)', () => {
assert.deepEqual(
parseArgs(NREVRANGE, ['a', 'b'], '-', '+', {
AGGREGATION: {
types: [
TIME_SERIES_AGGREGATION_TYPE.MIN,
TIME_SERIES_AGGREGATION_TYPE.MAX
],
timeBucket: 1000
}
}),
[
'TS.NREVRANGE', '2', 'a', 'b', '-', '+',
'AGGREGATION', 'MIN', 'MAX', '1000'
]
);
});

testUtils.testWithClient('client.ts.nRevRange (reverse order)', async client => {
await Promise.all([
client.ts.create('{t}:1'),
client.ts.create('{t}:2')
]);
await Promise.all([
client.ts.add('{t}:1', 1000, 10),
client.ts.add('{t}:1', 2000, 12),
client.ts.add('{t}:2', 1000, 13)
]);

assert.deepEqual(
await client.ts.nRevRange(['{t}:1', '{t}:2'], '-', '+'),
[
{ timestamp: 2000, values: [12, NaN] },
{ timestamp: 1000, values: [10, 13] }
]
);
}, {
...GLOBAL.SERVERS.OPEN,
minimumDockerVersion: [8, 10]
});
});
13 changes: 13 additions & 0 deletions packages/time-series/lib/commands/NREVRANGE.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Command } from '@redis/client/dist/lib/RESP/types';
import NRANGE, { transformNRangeArguments } from './NRANGE';

export default {
IS_READ_ONLY: NRANGE.IS_READ_ONLY,
parseCommand(...args: Parameters<typeof transformNRangeArguments>) {
const parser = args[0];

parser.push('TS.NREVRANGE');
transformNRangeArguments(...args);
},
transformReply: NRANGE.transformReply
} as const satisfies Command;
39 changes: 3 additions & 36 deletions packages/time-series/lib/commands/RANGE.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CommandParser } from '@redis/client/dist/lib/client/parser';
import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
import { Timestamp, transformTimestampArgument, SamplesRawReply, transformSamplesReply } from './helpers';
import { Timestamp, transformTimestampArgument, TsRangeCommonOptions, parseRangeCommonArguments, SamplesRawReply, transformSamplesReply } from './helpers';
import { TimeSeriesAggregationType } from './CREATERULE';
import { Resp2Reply } from '@redis/client/dist/lib/RESP/types';

Expand All @@ -12,14 +12,7 @@ export const TIME_SERIES_BUCKET_TIMESTAMP = {

export type TimeSeriesBucketTimestamp = typeof TIME_SERIES_BUCKET_TIMESTAMP[keyof typeof TIME_SERIES_BUCKET_TIMESTAMP];

export interface TsRangeOptions {
LATEST?: boolean;
FILTER_BY_TS?: Array<Timestamp>;
FILTER_BY_VALUE?: {
min: number;
max: number;
};
COUNT?: number;
export interface TsRangeOptions extends TsRangeCommonOptions {
ALIGN?: Timestamp;
AGGREGATION?: {
ALIGN?: Timestamp;
Expand All @@ -36,33 +29,7 @@ export function parseRangeArguments(
toTimestamp: Timestamp,
options?: TsRangeOptions
) {
parser.push(
transformTimestampArgument(fromTimestamp),
transformTimestampArgument(toTimestamp)
);

if (options?.LATEST) {
parser.push('LATEST');
}

if (options?.FILTER_BY_TS) {
parser.push('FILTER_BY_TS');
for (const timestamp of options.FILTER_BY_TS) {
parser.push(transformTimestampArgument(timestamp));
}
}

if (options?.FILTER_BY_VALUE) {
parser.push(
'FILTER_BY_VALUE',
options.FILTER_BY_VALUE.min.toString(),
options.FILTER_BY_VALUE.max.toString()
);
}

if (options?.COUNT !== undefined) {
parser.push('COUNT', options.COUNT.toString());
}
parseRangeCommonArguments(parser, fromTimestamp, toTimestamp, options);

if (options?.AGGREGATION) {
if (options?.ALIGN !== undefined) {
Expand Down
Loading