diff --git a/packages/time-series/lib/commands/MRANGE.spec.ts b/packages/time-series/lib/commands/MRANGE.spec.ts index 893d3b02303..fdda2410904 100644 --- a/packages/time-series/lib/commands/MRANGE.spec.ts +++ b/packages/time-series/lib/commands/MRANGE.spec.ts @@ -34,6 +34,41 @@ describe('TS.MRANGE', () => { ); }); + it('transformArguments with EXCLUDEEMPTY', () => { + assert.deepEqual( + parseArgs(MRANGE, '-', '+', 'label=value', { + COUNT: 1, + EXCLUDEEMPTY: true + }), + [ + 'TS.MRANGE', '-', '+', + 'COUNT', '1', + 'EXCLUDEEMPTY', + 'FILTER', 'label=value' + ] + ); + }); + + testUtils.testWithClient('client.ts.mRange EXCLUDEEMPTY omits empty series', async client => { + await Promise.all([ + client.ts.add('s', 100, 100, { LABELS: { sensor: '1' } }), + client.ts.add('t', 100, 100, { LABELS: { sensor: '1' } }), + client.ts.create('u', { LABELS: { sensor: '1' } }) + ]); + await client.ts.add('u', 2000, 2000); + + const reply = await client.ts.mRange('-', 500, 'sensor=1', { + EXCLUDEEMPTY: true + }); + + assert.ok('s' in reply); + assert.ok('t' in reply); + assert.ok(!('u' in reply), 'series "u" has no samples in range and must be omitted'); + }, { + ...GLOBAL.SERVERS.OPEN, + minimumDockerVersion: [8, 10] + }); + testUtils.testWithClient('client.ts.mRange', async client => { const [, reply] = await Promise.all([ client.ts.add('key', 0, 0, { diff --git a/packages/time-series/lib/commands/MRANGE.ts b/packages/time-series/lib/commands/MRANGE.ts index fd99fc0969d..b73eadd4993 100644 --- a/packages/time-series/lib/commands/MRANGE.ts +++ b/packages/time-series/lib/commands/MRANGE.ts @@ -1,8 +1,8 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers'; -import { TsRangeOptions, parseRangeArguments } from './RANGE'; +import { parseExcludeEmptyArgument, resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers'; +import { TsMRangeOptions, parseRangeArguments } from './RANGE'; import { parseFilterArgument } from './MGET'; export type TsMRangeRawReply2 = ArrayReply< @@ -32,7 +32,7 @@ export function createTransformMRangeArguments(command: RedisArgument) { fromTimestamp: Timestamp, toTimestamp: Timestamp, filter: RedisVariadicArgument, - options?: TsRangeOptions + options?: TsMRangeOptions ) => { parser.push(command); parseRangeArguments( @@ -41,7 +41,9 @@ export function createTransformMRangeArguments(command: RedisArgument) { toTimestamp, options ); - + + parseExcludeEmptyArgument(parser, options?.EXCLUDEEMPTY); + parseFilterArgument(parser, filter); }; } diff --git a/packages/time-series/lib/commands/MRANGE_MULTIAGGR.spec.ts b/packages/time-series/lib/commands/MRANGE_MULTIAGGR.spec.ts index 87713dabfcb..00b237bb970 100644 --- a/packages/time-series/lib/commands/MRANGE_MULTIAGGR.spec.ts +++ b/packages/time-series/lib/commands/MRANGE_MULTIAGGR.spec.ts @@ -37,6 +37,27 @@ describe('TS.MRANGE_MULTIAGGR', () => { ); }); + it('transformArguments with EXCLUDEEMPTY', () => { + assert.deepEqual( + parseArgs(MRANGE_MULTIAGGR, '-', '+', 'label=value', { + AGGREGATION: { + types: [ + TIME_SERIES_AGGREGATION_TYPE.MIN, + TIME_SERIES_AGGREGATION_TYPE.MAX + ], + timeBucket: 1 + }, + EXCLUDEEMPTY: true + }), + [ + 'TS.MRANGE', '-', '+', + 'AGGREGATION', 'MIN,MAX', '1', + 'EXCLUDEEMPTY', + 'FILTER', 'label=value' + ] + ); + }); + testUtils.testWithClient('client.ts.mRangeMultiAggr', async client => { await client.ts.add('mrange-multi', 1000, 0, { LABELS: { diff --git a/packages/time-series/lib/commands/MRANGE_MULTIAGGR.ts b/packages/time-series/lib/commands/MRANGE_MULTIAGGR.ts index 470816030d7..bdb2dc5093d 100644 --- a/packages/time-series/lib/commands/MRANGE_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MRANGE_MULTIAGGR.ts @@ -2,13 +2,14 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { + parseExcludeEmptyArgument, resp2MapToValue, resp3MapToValue, MultiAggregationSampleRawReply, Timestamp, transformMultiAggregationSamplesReply } from './helpers'; -import { TsRangeMultiAggrOptions, parseRangeMultiArguments } from './RANGE_MULTIAGGR'; +import { TsMRangeMultiAggrOptions, parseRangeMultiArguments } from './RANGE_MULTIAGGR'; import { parseFilterArgument } from './MGET'; export type TsMRangeMultiRawReply2 = ArrayReply< @@ -38,7 +39,7 @@ export function createTransformMRangeMultiArguments(command: RedisArgument) { fromTimestamp: Timestamp, toTimestamp: Timestamp, filter: RedisVariadicArgument, - options: TsRangeMultiAggrOptions + options: TsMRangeMultiAggrOptions ) => { parser.push(command); parseRangeMultiArguments( @@ -48,6 +49,8 @@ export function createTransformMRangeMultiArguments(command: RedisArgument) { options ); + parseExcludeEmptyArgument(parser, options?.EXCLUDEEMPTY); + parseFilterArgument(parser, filter); }; } diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts index 353d23b5b7e..addf9073b00 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts @@ -1,8 +1,8 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, NullReply, RedisArgument } from '@redis/client/dist/lib/RESP/types'; import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { parseSelectedLabelsArguments, resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2Labels, transformSamplesReply } from './helpers'; -import { TsRangeOptions, parseRangeArguments } from './RANGE'; +import { parseExcludeEmptyArgument, parseSelectedLabelsArguments, resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2Labels, transformSamplesReply } from './helpers'; +import { TsMRangeOptions, parseRangeArguments } from './RANGE'; import { parseFilterArgument } from './MGET'; export type TsMRangeSelectedLabelsRawReply2 = ArrayReply< @@ -38,7 +38,7 @@ export function createTransformMRangeSelectedLabelsArguments(command: RedisArgum toTimestamp: Timestamp, selectedLabels: RedisVariadicArgument, filter: RedisVariadicArgument, - options?: TsRangeOptions + options?: TsMRangeOptions ) => { parser.push(command); parseRangeArguments( @@ -47,7 +47,9 @@ export function createTransformMRangeSelectedLabelsArguments(command: RedisArgum toTimestamp, options ); - + + parseExcludeEmptyArgument(parser, options?.EXCLUDEEMPTY); + parseSelectedLabelsArguments(parser, selectedLabels); parseFilterArgument(parser, filter); diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_MULTIAGGR.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_MULTIAGGR.ts index 78676db5119..d95297ceba4 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_MULTIAGGR.ts @@ -2,6 +2,7 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, NullReply, RedisArgument } from '@redis/client/dist/lib/RESP/types'; import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { + parseExcludeEmptyArgument, parseSelectedLabelsArguments, resp2MapToValue, resp3MapToValue, @@ -10,7 +11,7 @@ import { transformRESP2Labels, transformMultiAggregationSamplesReply } from './helpers'; -import { TsRangeMultiAggrOptions, parseRangeMultiArguments } from './RANGE_MULTIAGGR'; +import { TsMRangeMultiAggrOptions, parseRangeMultiArguments } from './RANGE_MULTIAGGR'; import { parseFilterArgument } from './MGET'; export type TsMRangeSelectedLabelsMultiRawReply2 = ArrayReply< @@ -44,7 +45,7 @@ export function createTransformMRangeSelectedLabelsMultiArguments(command: Redis toTimestamp: Timestamp, selectedLabels: RedisVariadicArgument, filter: RedisVariadicArgument, - options: TsRangeMultiAggrOptions + options: TsMRangeMultiAggrOptions ) => { parser.push(command); parseRangeMultiArguments( @@ -54,6 +55,8 @@ export function createTransformMRangeSelectedLabelsMultiArguments(command: Redis options ); + parseExcludeEmptyArgument(parser, options?.EXCLUDEEMPTY); + parseSelectedLabelsArguments(parser, selectedLabels); parseFilterArgument(parser, filter); diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts index 8fd68cbd6e8..050478cf0d2 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts @@ -35,6 +35,20 @@ describe('TS.MRANGE_WITHLABELS', () => { ); }); + it('transformArguments with EXCLUDEEMPTY', () => { + assert.deepEqual( + parseArgs(MRANGE_WITHLABELS, '-', '+', 'label=value', { + EXCLUDEEMPTY: true + }), + [ + 'TS.MRANGE', '-', '+', + 'EXCLUDEEMPTY', + 'WITHLABELS', + 'FILTER', 'label=value' + ] + ); + }); + testUtils.testWithClient('client.ts.mRangeWithLabels', async client => { const [, reply] = await Promise.all([ client.ts.add('key', 0, 0, { diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts index 925b7c4fb73..ddf4a6d68e4 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts @@ -1,8 +1,8 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { Command, UnwrapReply, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers'; -import { TsRangeOptions, parseRangeArguments } from './RANGE'; +import { parseExcludeEmptyArgument, resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers'; +import { TsMRangeOptions, parseRangeArguments } from './RANGE'; import { parseFilterArgument } from './MGET'; export type TsMRangeWithLabelsRawReply2 = ArrayReply< @@ -35,7 +35,7 @@ export function createTransformMRangeWithLabelsArguments(command: RedisArgument) fromTimestamp: Timestamp, toTimestamp: Timestamp, filter: RedisVariadicArgument, - options?: TsRangeOptions + options?: TsMRangeOptions ) => { parser.push(command); parseRangeArguments( @@ -45,6 +45,8 @@ export function createTransformMRangeWithLabelsArguments(command: RedisArgument) options ); + parseExcludeEmptyArgument(parser, options?.EXCLUDEEMPTY); + parser.push('WITHLABELS'); parseFilterArgument(parser, filter); diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS_MULTIAGGR.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS_MULTIAGGR.ts index 0192e9c97c8..b1b0f4bd7a6 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_MULTIAGGR.ts @@ -2,13 +2,14 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { Command, UnwrapReply, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { + parseExcludeEmptyArgument, resp2MapToValue, resp3MapToValue, MultiAggregationSampleRawReply, Timestamp, transformMultiAggregationSamplesReply } from './helpers'; -import { TsRangeMultiAggrOptions, parseRangeMultiArguments } from './RANGE_MULTIAGGR'; +import { TsMRangeMultiAggrOptions, parseRangeMultiArguments } from './RANGE_MULTIAGGR'; import { parseFilterArgument } from './MGET'; export type TsMRangeWithLabelsMultiRawReply2 = ArrayReply< @@ -41,7 +42,7 @@ export function createTransformMRangeWithLabelsMultiArguments(command: RedisArgu fromTimestamp: Timestamp, toTimestamp: Timestamp, filter: RedisVariadicArgument, - options: TsRangeMultiAggrOptions + options: TsMRangeMultiAggrOptions ) => { parser.push(command); parseRangeMultiArguments( @@ -51,6 +52,8 @@ export function createTransformMRangeWithLabelsMultiArguments(command: RedisArgu options ); + parseExcludeEmptyArgument(parser, options?.EXCLUDEEMPTY); + parser.push('WITHLABELS'); parseFilterArgument(parser, filter); diff --git a/packages/time-series/lib/commands/MREVRANGE.spec.ts b/packages/time-series/lib/commands/MREVRANGE.spec.ts index 174621118e9..03a8844e82e 100644 --- a/packages/time-series/lib/commands/MREVRANGE.spec.ts +++ b/packages/time-series/lib/commands/MREVRANGE.spec.ts @@ -34,6 +34,41 @@ describe('TS.MREVRANGE', () => { ); }); + it('transformArguments with EXCLUDEEMPTY', () => { + assert.deepEqual( + parseArgs(MREVRANGE, '-', '+', 'label=value', { + COUNT: 1, + EXCLUDEEMPTY: true + }), + [ + 'TS.MREVRANGE', '-', '+', + 'COUNT', '1', + 'EXCLUDEEMPTY', + 'FILTER', 'label=value' + ] + ); + }); + + testUtils.testWithClient('client.ts.mRevRange EXCLUDEEMPTY omits empty series', async client => { + await Promise.all([ + client.ts.add('s', 100, 100, { LABELS: { sensor: '1' } }), + client.ts.add('t', 100, 100, { LABELS: { sensor: '1' } }), + client.ts.create('u', { LABELS: { sensor: '1' } }) + ]); + await client.ts.add('u', 2000, 2000); + + const reply = await client.ts.mRevRange('-', 500, 'sensor=1', { + EXCLUDEEMPTY: true + }); + + assert.ok('s' in reply); + assert.ok('t' in reply); + assert.ok(!('u' in reply), 'series "u" has no samples in range and must be omitted'); + }, { + ...GLOBAL.SERVERS.OPEN, + minimumDockerVersion: [8, 10] + }); + testUtils.testWithClient('client.ts.mRevRange', async client => { const [, reply] = await Promise.all([ client.ts.add('key', 0, 0, { diff --git a/packages/time-series/lib/commands/RANGE.ts b/packages/time-series/lib/commands/RANGE.ts index 43c1357f88f..c318d555a10 100644 --- a/packages/time-series/lib/commands/RANGE.ts +++ b/packages/time-series/lib/commands/RANGE.ts @@ -23,6 +23,20 @@ export interface TsRangeOptions extends TsRangeCommonOptions { }; } +/** + * `TS.MRANGE`/`TS.MREVRANGE` (non-`GROUPBY`) options: the single-key range options + * plus the multi-range-only `EXCLUDEEMPTY` flag. + */ +export interface TsMRangeOptions extends TsRangeOptions { + /** + * Omit matching series whose reported samples array is empty from the reply. + * Cannot be combined with `GROUPBY`. + * + * @since Redis 8.10 + */ + EXCLUDEEMPTY?: boolean; +} + export function parseRangeArguments( parser: CommandParser, fromTimestamp: Timestamp, diff --git a/packages/time-series/lib/commands/RANGE_MULTIAGGR.ts b/packages/time-series/lib/commands/RANGE_MULTIAGGR.ts index dc32ff94684..2fe7aea6906 100644 --- a/packages/time-series/lib/commands/RANGE_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/RANGE_MULTIAGGR.ts @@ -24,6 +24,20 @@ export interface TsRangeMultiAggrOptions extends TsRangeCommonOptions { }; } +/** + * `TS.MRANGE`/`TS.MREVRANGE` multi-aggregation (non-`GROUPBY`) options: the + * multi-aggregation range options plus the multi-range-only `EXCLUDEEMPTY` flag. + */ +export interface TsMRangeMultiAggrOptions extends TsRangeMultiAggrOptions { + /** + * Omit matching series whose reported samples array is empty from the reply. + * Cannot be combined with `GROUPBY`. + * + * @since Redis 8.10 + */ + EXCLUDEEMPTY?: boolean; +} + export function parseRangeMultiArguments( parser: CommandParser, fromTimestamp: Timestamp, diff --git a/packages/time-series/lib/commands/helpers.ts b/packages/time-series/lib/commands/helpers.ts index 7c31a441d86..f7ae3811830 100644 --- a/packages/time-series/lib/commands/helpers.ts +++ b/packages/time-series/lib/commands/helpers.ts @@ -118,6 +118,19 @@ export function parseRangeCommonArguments( } } +/** + * Pushes the bare `EXCLUDEEMPTY` flag for `TS.MRANGE`/`TS.MREVRANGE` when requested. + * The flag omits matching series with an empty samples array from the reply. It is + * not valid on single-key `TS.RANGE`/`TS.REVRANGE` nor combinable with `GROUPBY`. + * + * @since Redis 8.10 + */ +export function parseExcludeEmptyArgument(parser: CommandParser, excludeEmpty?: boolean) { + if (excludeEmpty) { + parser.push('EXCLUDEEMPTY'); + } +} + export type Labels = { [label: string]: string; };